From cc6af03adf6208974b0afcd7684358dd83fcfc8b Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:48:03 -0500 Subject: [PATCH 01/44] Lib.SysModule: Proper HLE implementation (#4102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace sysmodule enums with table Dumped this from the 12.52 module, using a script I created. * Better documentation * Separate from system libraries That system folder is going to be getting quite large if I left all the sysmodule stuff in there. * More arrays from library * Found another preload list Ghidra really hates decompiling libSceSysmodule, so I didn't notice this one at first. Also documented specific versions tied to each preload list. * Start work on implementation * Some basic implementations * Initial stub for module loading Just enough to see if the logic functions appropriately. * Clang * sceSysmoduleLoadModule Now I need to get sceSysmodulePreloadModuleForLibkernel done so that we don't have bugs from not loading internal LLEs. * sceSysmoduleLoadModuleInternal * sceSysmodulePreloadModuleForLibkernel I have successfully broken quite literally everything. I shall debug this tomorrow. * Slight fix * Maybe fix? * Change log Enjoy the log spam 😄 * Increased defined stub count Now that libc and libSceLibcInternal loads later, all the auto stubs are getting consumed by it. * sceSysmoduleUnloadModule stub Also a couple fixes. Sysmodule does pass argc and argv to game modules, but only after loading them once to check binaries. Shouldn't matter for the most part. * Clang * Less stubs 2 thousand is seemingly enough. * sceSysmoduleLoadModuleInternalWithArg Doesn't hurt to have, since Apex Legends calls it. * Oops * Oops 2 * Rename isModuleLoaded to getModuleHandle Review comment * Remove debug game module loads These cases only trigger when specific sceRegMgr key values are set, and for our purposes, we can treat that case as false. * Allow preloading to fail For kalaposfos * Clang --- CMakeLists.txt | 9 +- src/common/elf_info.h | 3 + src/core/aerolib/stubs.cpp | 5 +- src/core/libraries/kernel/orbis_error.h | 2 + src/core/libraries/libs.cpp | 2 +- src/core/libraries/sysmodule/sysmodule.cpp | 204 ++++++ src/core/libraries/sysmodule/sysmodule.h | 38 + .../libraries/sysmodule/sysmodule_error.h | 10 + .../sysmodule/sysmodule_internal.cpp | 440 +++++++++++ .../libraries/sysmodule/sysmodule_internal.h | 20 + .../libraries/sysmodule/sysmodule_table.h | 684 ++++++++++++++++++ src/core/libraries/system/sysmodule.cpp | 169 ----- src/core/libraries/system/sysmodule.h | 194 ----- src/core/libraries/system/system_error.h | 8 - src/core/linker.cpp | 8 +- src/emulator.cpp | 67 -- 16 files changed, 1416 insertions(+), 447 deletions(-) create mode 100644 src/core/libraries/sysmodule/sysmodule.cpp create mode 100644 src/core/libraries/sysmodule/sysmodule.h create mode 100644 src/core/libraries/sysmodule/sysmodule_error.h create mode 100644 src/core/libraries/sysmodule/sysmodule_internal.cpp create mode 100644 src/core/libraries/sysmodule/sysmodule_internal.h create mode 100644 src/core/libraries/sysmodule/sysmodule_table.h delete mode 100644 src/core/libraries/system/sysmodule.cpp delete mode 100644 src/core/libraries/system/sysmodule.h delete mode 100644 src/core/libraries/system/system_error.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b07dcea87..dd02a6378 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -414,9 +414,12 @@ set(SYSTEM_LIBS src/core/libraries/system/commondialog.cpp src/core/libraries/save_data/dialog/savedatadialog.h src/core/libraries/save_data/dialog/savedatadialog_ui.cpp src/core/libraries/save_data/dialog/savedatadialog_ui.h - src/core/libraries/system/sysmodule.cpp - src/core/libraries/system/sysmodule.h - src/core/libraries/system/system_error.h + src/core/libraries/sysmodule/sysmodule.cpp + src/core/libraries/sysmodule/sysmodule.h + src/core/libraries/sysmodule/sysmodule_internal.cpp + src/core/libraries/sysmodule/sysmodule_internal.h + src/core/libraries/sysmodule/sysmodule_error.h + src/core/libraries/sysmodule/sysmodule_table.h src/core/libraries/system/systemservice.cpp src/core/libraries/system/systemservice.h src/core/libraries/system/systemservice_error.h diff --git a/src/common/elf_info.h b/src/common/elf_info.h index 0f2311cb0..b84f36ecb 100644 --- a/src/common/elf_info.h +++ b/src/common/elf_info.h @@ -88,7 +88,10 @@ public: static constexpr u32 FW_50 = 0x5000000; static constexpr u32 FW_55 = 0x5500000; static constexpr u32 FW_60 = 0x6000000; + static constexpr u32 FW_70 = 0x7000000; + static constexpr u32 FW_75 = 0x7500000; static constexpr u32 FW_80 = 0x8000000; + static constexpr u32 FW_115 = 0x11500000; static ElfInfo& Instance() { return *Singleton::Instance(); diff --git a/src/core/aerolib/stubs.cpp b/src/core/aerolib/stubs.cpp index 2634fc46a..7023144d7 100644 --- a/src/core/aerolib/stubs.cpp +++ b/src/core/aerolib/stubs.cpp @@ -19,7 +19,7 @@ namespace Core::AeroLib { // and to longer compile / CI times // // Must match STUBS_LIST define -constexpr u32 MAX_STUBS = 1024; +constexpr u32 MAX_STUBS = 2048; u64 UnresolvedStub() { LOG_ERROR(Core, "Returning zero to {}", __builtin_return_address(0)); @@ -61,8 +61,9 @@ static u32 UsedStubEntries; #define XREP_256(x) XREP_128(x) XREP_128(x + 128) #define XREP_512(x) XREP_256(x) XREP_256(x + 256) #define XREP_1024(x) XREP_512(x) XREP_512(x + 512) +#define XREP_2048(x) XREP_1024(x) XREP_1024(x + 1024) -#define STUBS_LIST XREP_1024(0) +#define STUBS_LIST XREP_2048(0) static u64 (*stub_handlers[MAX_STUBS])() = {STUBS_LIST}; diff --git a/src/core/libraries/kernel/orbis_error.h b/src/core/libraries/kernel/orbis_error.h index d19b3f3f1..6ebff0ba3 100644 --- a/src/core/libraries/kernel/orbis_error.h +++ b/src/core/libraries/kernel/orbis_error.h @@ -106,3 +106,5 @@ constexpr int ORBIS_KERNEL_ERROR_ECAPMODE = 0x8002005E; constexpr int ORBIS_KERNEL_ERROR_ENOBLK = 0x8002005F; constexpr int ORBIS_KERNEL_ERROR_EICV = 0x80020060; constexpr int ORBIS_KERNEL_ERROR_ENOPLAYGOENT = 0x80020061; +constexpr int ORBIS_KERNEL_ERROR_ESDKVERSION = 0x80020063; +constexpr int ORBIS_KERNEL_ERROR_ESTART = 0x80020064; \ No newline at end of file diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index eebb991dc..ac35c4b63 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -57,10 +57,10 @@ #include "core/libraries/screenshot/screenshot.h" #include "core/libraries/share_play/shareplay.h" #include "core/libraries/signin_dialog/signindialog.h" +#include "core/libraries/sysmodule/sysmodule.h" #include "core/libraries/system/commondialog.h" #include "core/libraries/system/msgdialog.h" #include "core/libraries/system/posix.h" -#include "core/libraries/system/sysmodule.h" #include "core/libraries/system/systemservice.h" #include "core/libraries/system/userservice.h" #include "core/libraries/system_gesture/system_gesture.h" diff --git a/src/core/libraries/sysmodule/sysmodule.cpp b/src/core/libraries/sysmodule/sysmodule.cpp new file mode 100644 index 000000000..1ad9075e7 --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule.cpp @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "common/elf_info.h" +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/kernel/process.h" +#include "core/libraries/libs.h" +#include "core/libraries/sysmodule/sysmodule.h" +#include "core/libraries/sysmodule/sysmodule_error.h" +#include "core/libraries/sysmodule/sysmodule_internal.h" +#include "core/linker.h" + +namespace Libraries::SysModule { + +static std::mutex g_mutex{}; + +s32 PS4_SYSV_ABI sceSysmoduleGetModuleHandleInternal(OrbisSysModuleInternal id, s32* handle) { + LOG_INFO(Lib_SysModule, "called"); + if ((id & 0x7fffffff) == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return getModuleHandle(id, handle); +} + +s32 PS4_SYSV_ABI sceSysmoduleGetModuleInfoForUnwind(VAddr addr, s32 flags, + Kernel::OrbisModuleInfoForUnwind* info) { + LOG_TRACE(Lib_SysModule, "sceSysmoduleGetModuleInfoForUnwind called"); + s32 res = Kernel::sceKernelGetModuleInfoForUnwind(addr, flags, info); + if (res != ORBIS_OK) { + return res; + } + + if (shouldHideName(info->name.data())) { + std::ranges::fill(info->name, '\0'); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleIsCalledFromSysModule() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleIsCameraPreloaded() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleIsLoaded(OrbisSysModule id) { + if (id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return getModuleHandle(id, nullptr); +} + +s32 PS4_SYSV_ABI sceSysmoduleIsLoadedInternal(OrbisSysModuleInternal id) { + if ((id & 0x7fffffff) == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return getModuleHandle(id, nullptr); +} + +s32 PS4_SYSV_ABI sceSysmoduleLoadModule(OrbisSysModule id) { + LOG_INFO(Lib_SysModule, "called, id = {:#x}", id); + s32 result = validateModuleId(id); + if (result < ORBIS_OK) { + return result; + } + + // Only locks for internal loadModule call. + { + std::scoped_lock lk{g_mutex}; + result = loadModule(id, 0, nullptr, nullptr); + } + + if (result == ORBIS_KERNEL_ERROR_ESTART) { + s32 sdk_ver = 0; + result = Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver); + if (sdk_ver < Common::ElfInfo::FW_115 || result != ORBIS_OK) { + return ORBIS_KERNEL_ERROR_EINVAL; + } else { + return ORBIS_KERNEL_ERROR_ESTART; + } + } + + // The real library has some weird workaround for CUSA01478 and CUSA01495 here. + // Unless this is proven necessary, I don't plan to handle this. + return result; +} + +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleByNameInternal() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleInternal(OrbisSysModuleInternal id) { + LOG_INFO(Lib_SysModule, "called, id = {:#x}", id); + s32 result = validateModuleId(id); + if (result < ORBIS_OK) { + return result; + } + + // This specific module ID is loaded unlocked. + if (id == 0x80000039) { + return loadModule(id, 0, nullptr, nullptr); + } + std::scoped_lock lk{g_mutex}; + return loadModule(id, 0, nullptr, nullptr); +} + +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleInternalWithArg(OrbisSysModuleInternal id, s32 argc, + const void* argv, u64 unk, s32* res_out) { + LOG_INFO(Lib_SysModule, "called, id = {:#x}", id); + s32 result = validateModuleId(id); + if (result < ORBIS_OK) { + return result; + } + + if (unk != 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return loadModule(id, argc, argv, res_out); +} + +s32 PS4_SYSV_ABI sceSysmoduleMapLibcForLibkernel() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmodulePreloadModuleForLibkernel() { + LOG_DEBUG(Lib_SysModule, "called"); + return preloadModulesForLibkernel(); +} + +s32 PS4_SYSV_ABI sceSysmoduleUnloadModule(OrbisSysModule id) { + LOG_ERROR(Lib_SysModule, "(STUBBED) called, id = {:#x}", id); + if (id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return unloadModule(id, 0, nullptr, nullptr, false); +} + +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleByNameInternal() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleInternal() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleInternalWithArg() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("D8cuU4d72xM", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleGetModuleHandleInternal); + LIB_FUNCTION("4fU5yvOkVG4", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleGetModuleInfoForUnwind); + LIB_FUNCTION("ctfO7dQ7geg", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleIsCalledFromSysModule); + LIB_FUNCTION("no6T3EfiS3E", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleIsCameraPreloaded); + LIB_FUNCTION("fMP5NHUOaMk", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleIsLoaded); + LIB_FUNCTION("ynFKQ5bfGks", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleIsLoadedInternal); + LIB_FUNCTION("g8cM39EUZ6o", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleLoadModule); + LIB_FUNCTION("CU8m+Qs+HN4", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleLoadModuleByNameInternal); + LIB_FUNCTION("39iV5E1HoCk", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleLoadModuleInternal); + LIB_FUNCTION("hHrGoGoNf+s", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleLoadModuleInternalWithArg); + LIB_FUNCTION("lZ6RvVl0vo0", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleMapLibcForLibkernel); + LIB_FUNCTION("DOO+zuW1lrE", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmodulePreloadModuleForLibkernel); + LIB_FUNCTION("eR2bZFAAU0Q", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleUnloadModule); + LIB_FUNCTION("vpTHmA6Knvg", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleUnloadModuleByNameInternal); + LIB_FUNCTION("vXZhrtJxkGc", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleUnloadModuleInternal); + LIB_FUNCTION("aKa6YfBKZs4", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleUnloadModuleInternalWithArg); +}; + +} // namespace Libraries::SysModule diff --git a/src/core/libraries/sysmodule/sysmodule.h b/src/core/libraries/sysmodule/sysmodule.h new file mode 100644 index 000000000..17ac3188f --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/kernel/process.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::SysModule { + +using OrbisSysModule = u16; +using OrbisSysModuleInternal = u32; + +s32 PS4_SYSV_ABI sceSysmoduleGetModuleHandleInternal(OrbisSysModuleInternal id, s32* handle); +s32 PS4_SYSV_ABI sceSysmoduleGetModuleInfoForUnwind(VAddr addr, s32 flags, + Kernel::OrbisModuleInfoForUnwind* info); +s32 PS4_SYSV_ABI sceSysmoduleIsCalledFromSysModule(); +s32 PS4_SYSV_ABI sceSysmoduleIsCameraPreloaded(); +s32 PS4_SYSV_ABI sceSysmoduleIsLoaded(OrbisSysModule id); +s32 PS4_SYSV_ABI sceSysmoduleIsLoadedInternal(OrbisSysModuleInternal id); +s32 PS4_SYSV_ABI sceSysmoduleLoadModule(OrbisSysModule id); +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleByNameInternal(); +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleInternal(OrbisSysModuleInternal id); +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleInternalWithArg(OrbisSysModuleInternal id, s32 argc, + const void* argv, u64 unk, s32* res_out); +s32 PS4_SYSV_ABI sceSysmoduleMapLibcForLibkernel(); +s32 PS4_SYSV_ABI sceSysmodulePreloadModuleForLibkernel(); +s32 PS4_SYSV_ABI sceSysmoduleUnloadModule(OrbisSysModule id); +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleByNameInternal(); +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleInternal(); +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleInternalWithArg(); + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::SysModule diff --git a/src/core/libraries/sysmodule/sysmodule_error.h b/src/core/libraries/sysmodule/sysmodule_error.h new file mode 100644 index 000000000..aee14b9df --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule_error.h @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +constexpr s32 ORBIS_SYSMODULE_INVALID_ID = 0x805A1000; +constexpr s32 ORBIS_SYSMODULE_NOT_LOADED = 0x805A1001; +constexpr s32 ORBIS_SYSMODULE_LOCK_FAILED = 0x805A10FF; \ No newline at end of file diff --git a/src/core/libraries/sysmodule/sysmodule_internal.cpp b/src/core/libraries/sysmodule/sysmodule_internal.cpp new file mode 100644 index 000000000..55acded94 --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule_internal.cpp @@ -0,0 +1,440 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "common/config.h" +#include "common/elf_info.h" +#include "common/logging/log.h" +#include "core/file_sys/fs.h" +#include "core/libraries/disc_map/disc_map.h" +#include "core/libraries/font/font.h" +#include "core/libraries/font/fontft.h" +#include "core/libraries/jpeg/jpegenc.h" +#include "core/libraries/kernel/kernel.h" +#include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/libc_internal/libc_internal.h" +#include "core/libraries/libpng/pngenc.h" +#include "core/libraries/libs.h" +#include "core/libraries/ngs2/ngs2.h" +#include "core/libraries/rtc/rtc.h" +#include "core/libraries/sysmodule/sysmodule_error.h" +#include "core/libraries/sysmodule/sysmodule_internal.h" +#include "core/libraries/sysmodule/sysmodule_table.h" +#include "core/linker.h" +#include "emulator.h" + +namespace Libraries::SysModule { + +s32 getModuleHandle(s32 id, s32* handle) { + if (id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + for (OrbisSysmoduleModuleInternal mod : g_modules_array) { + if (mod.id != id) { + continue; + } + if (mod.is_loaded < 1) { + return ORBIS_SYSMODULE_NOT_LOADED; + } + if (handle != nullptr) { + *handle = mod.handle; + } + return ORBIS_OK; + } + return ORBIS_SYSMODULE_INVALID_ID; +} + +bool shouldHideName(const char* module_name) { + for (u64 i = 0; i < g_num_modules; i++) { + OrbisSysmoduleModuleInternal mod = g_modules_array[i]; + if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsGame) == 0) { + continue; + } + u64 name_length = std::strlen(mod.name); + char name_copy[0x100]; + std::strncpy(name_copy, mod.name, sizeof(name_copy)); + // Module table stores names without extensions, so check with .prx appended to the name. + std::strncpy(&name_copy[name_length], ".prx", 4); + s32 result = std::strncmp(module_name, name_copy, sizeof(name_copy)); + if (result == 0) { + return true; + } + + // libSceFios2 and libc are checked as both sprx or prx modules. + if (i == 3) { + result = std::strncmp(module_name, "libSceFios2.sprx", sizeof(name_copy)); + } else if (i == 4) { + result = std::strncmp(module_name, "libc.sprx", sizeof(name_copy)); + } + + if (result == 0) { + return true; + } + } + return false; +} + +bool isDebugModule(s32 id) { + for (OrbisSysmoduleModuleInternal mod : g_modules_array) { + if (mod.id == id && (mod.flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0) { + return true; + } + } + return false; +} + +bool validateModuleId(s32 id) { + if ((id & 0x7fffffff) == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + s32 sdk_ver = 0; + ASSERT_MSG(!Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver), + "Failed to retrieve compiled SDK version"); + + // libSceGameCustomDialog isn't loadable on SDK >= 7.50 + if (id == 0xb8 && sdk_ver >= Common::ElfInfo::FW_75) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // libSceNpSnsFacebookDialog isn't loadable on SDK >= 7.00 + if (id == 0xb0 && sdk_ver >= Common::ElfInfo::FW_70) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // libSceJson isn't loadable on SDK >= 3.00 + if (id == 0x80 && sdk_ver >= Common::ElfInfo::FW_30) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // Cannot load debug modules on retail hardware. + if (isDebugModule(id) && !Config::isDevKitConsole()) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + return ORBIS_OK; +} + +s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { + auto* mnt = Common::Singleton::Instance(); + auto* linker = Common::Singleton::Instance(); + auto* game_info = Common::Singleton::Instance(); + + // If the module is already loaded, increment is_loaded and return ORBIS_OK. + OrbisSysmoduleModuleInternal& mod = g_modules_array[index]; + if (mod.is_loaded > 0) { + mod.is_loaded++; + return ORBIS_OK; + } + + s32 start_result = 0; + // Most of the logic the actual module has here is to get the correct location of this module. + // Since we only care about a small subset of LLEs, we can simplify this logic. + if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsGame) != 0) { + std::string guest_path = std::string("/app0/sce_module/").append(mod.name); + guest_path.append(".prx"); + const auto& host_path = mnt->GetHostPath(guest_path); + + // For convenience, load through linker directly instead of loading through libkernel calls. + s32 result = linker->LoadAndStartModule(host_path, argc, argv, &start_result); + // If the module is missing, the library prints a very helpful message for developers. + // We'll just log an error. + if (result < 0) { + LOG_ERROR(Lib_SysModule, "Failed to load game library {}", guest_path); + return result; + } else { + // On success, the library validates module params and the module SDK version. + // We don't store the information this uses, so skip the proper checks. + mod.handle = result; + mod.is_loaded++; + } + } else { + // This is not a game library. We'll need to perform some checks, + // but we don't need to perform the path resolution logic the actual library has. + std::string mod_name = std::string(mod.name); + + // libSceGnmDriver case + if (index == 0xd && Config::isDevKitConsole()) { + // There are some other checks involved here that I am not familiar with. + // Since we're not exactly running libSceGnmDriver LLE, this shouldn't matter too much. + mod_name.append("_padebug"); + } + + // libSceSsl2 case + if (index == 0x27 && false /*needs legacy ssl*/) { + // Replaces module name with libSceSsl (index 0x15) + mod_name.clear(); + mod_name.append(g_modules_array[0x15].name); + } + + // libSceVrTracker case + if (index == 0xb3 && Config::isDevKitConsole()) { + mod_name.append("_debug"); + } + + if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsNeo) == 0 && + (mod.flags & OrbisSysmoduleModuleInternalFlags::IsNeoMode) != 0 && + Kernel::sceKernelIsNeoMode() == 1) { + // PS4 Pro running in enhanced mode + mod_name.append("ForNeoMode"); + } else if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsNeo) != 0 && + Config::isNeoModeConsole()) { + // PS4 Pro running in base mode + mod_name.append("ForNeo"); + } + + // Append .sprx extension. + mod_name.append(".sprx"); + + // Now we need to check if the requested library is allowed to LLE. + // First, we allow all modules from game-specific sys_modules + const auto& sys_module_path = Config::getSysModulesPath(); + const auto& game_specific_module_path = + sys_module_path / game_info->GameSerial() / mod_name; + if (std::filesystem::exists(game_specific_module_path)) { + // The requested module is present in the game-specific sys_modules, load it. + LOG_INFO(Loader, "Loading {} from game serial file {}", mod_name, + game_info->GameSerial()); + s32 handle = + linker->LoadAndStartModule(game_specific_module_path, argc, argv, &start_result); + ASSERT_MSG(handle >= 0, "Failed to load module {}", mod_name); + mod.handle = handle; + mod.is_loaded++; + if (res_out != nullptr) { + *res_out = start_result; + } + return ORBIS_OK; + } + + // We need to check a few things here. + // First, check if this is a module we allow LLE for. + static s32 stub_handle = 100; + constexpr auto ModulesToLoad = std::to_array( + {{"libSceNgs2.sprx", &Libraries::Ngs2::RegisterLib}, + {"libSceUlt.sprx", nullptr}, + {"libSceRtc.sprx", &Libraries::Rtc::RegisterLib}, + {"libSceJpegDec.sprx", nullptr}, + {"libSceJpegEnc.sprx", &Libraries::JpegEnc::RegisterLib}, + {"libScePngEnc.sprx", &Libraries::PngEnc::RegisterLib}, + {"libSceJson.sprx", nullptr}, + {"libSceJson2.sprx", nullptr}, + {"libSceLibcInternal.sprx", &Libraries::LibcInternal::RegisterLib}, + {"libSceCesCs.sprx", nullptr}, + {"libSceAudiodec.sprx", nullptr}, + {"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont}, + {"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt}, + {"libSceFreeTypeOt.sprx", nullptr}}); + + // Iterate through the allowed array + const auto it = std::ranges::find_if( + ModulesToLoad, [&](Core::SysModules module) { return mod_name == module.module_name; }); + if (it == ModulesToLoad.end()) { + // Not an allowed LLE, stub success without warning. + mod.is_loaded++; + // Some internal checks rely on a handle, stub a valid one. + mod.handle = stub_handle++; + if (res_out != nullptr) { + *res_out = ORBIS_OK; + } + return ORBIS_OK; + } + + // Allowed module, check if it exists + const auto& module_path = sys_module_path / mod_name; + if (std::filesystem::exists(module_path)) { + LOG_INFO(Loader, "Loading {}", mod_name); + s32 handle = linker->LoadAndStartModule(module_path, argc, argv, &start_result); + ASSERT_MSG(handle >= 0, "Failed to load module {}", mod_name); + mod.handle = handle; + } else { + // Allowed LLE that isn't present, log message + auto& [name, init_func] = *it; + if (init_func) { + LOG_INFO(Loader, "Can't Load {} switching to HLE", mod_name); + init_func(&linker->GetHLESymbols()); + } else { + LOG_INFO(Loader, "No HLE available for {} module", mod_name); + } + mod.handle = stub_handle++; + } + + // Mark module as loaded. + mod.is_loaded++; + } + + // Only successful loads will reach here + if (res_out != nullptr) { + *res_out = start_result; + } + + return ORBIS_OK; +} + +s32 loadModule(s32 id, s32 argc, const void* argv, s32* res_out) { + // Retrieve the module to load from the table + OrbisSysmoduleModuleInternal requested_module{}; + for (OrbisSysmoduleModuleInternal mod : g_modules_array) { + if (mod.id == id) { + requested_module = mod; + break; + } + } + if (requested_module.id != id || requested_module.id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // Every module has a pointer to an array of indexes to modules that need loading. + if (requested_module.to_load == nullptr) { + // Seems like ORBIS_SYSMODULE_LOCK_FAILED is a generic internal error. + return ORBIS_SYSMODULE_LOCK_FAILED; + } + + LOG_INFO(Lib_SysModule, "Loading {}", requested_module.name); + + // Loop through every module that requires loading, in reverse order + for (s64 i = requested_module.num_to_load - 1; i >= 0; i--) { + // Modules flagged as debug modules only load for devkits + u32 mod_index = requested_module.to_load[i]; + if ((!Config::isDevKitConsole() && + g_modules_array[mod_index].flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0) { + continue; + } + + // Arguments and result should only be applied to the requested module + // Dependencies don't receive these values. + s32 result = 0; + if (i != 0) { + result = loadModuleInternal(mod_index, 0, nullptr, nullptr); + } else { + result = loadModuleInternal(mod_index, argc, argv, res_out); + } + + // If loading any module fails, abort there. + if (result != ORBIS_OK) { + return result; + } + } + return ORBIS_OK; +} + +s32 unloadModule(s32 id, s32 argc, const void* argv, s32* res_out, bool is_internal) { + OrbisSysmoduleModuleInternal mod{}; + for (s32 i = 0; i < g_modules_array.size(); i++) { + mod = g_modules_array[i]; + if (mod.id != id) { + continue; + } + + // Skips checking libSceDiscMap + if (i == 0x22) { + continue; + } + + // If the module is loaded once, and is part of the second preload list, + // then return OK and do nothing. + for (s32 index : g_preload_list_2) { + if (index == i && mod.is_loaded == 1) { + return ORBIS_OK; + } + } + + // Found the correct module. + break; + } + + // If we failed to locate the module, return invalid id. + if (mod.id != id || mod.id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // If the module has no dependencies, then return an internal error. + if (mod.num_to_load == 0 || mod.to_load == nullptr) { + return ORBIS_SYSMODULE_LOCK_FAILED; + } + + // Unload the module and it's dependencies + for (s64 i = 0; i < mod.num_to_load; i++) { + OrbisSysmoduleModuleInternal dep_mod = g_modules_array[mod.to_load[i]]; + // If this is a debug module and we're not emulating a devkit, skip it. + if ((dep_mod.flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0 && + !Config::isDevKitConsole()) { + continue; + } + + // If the module to unload is marked as unloaded, then return not loaded + if (dep_mod.is_loaded == 0) { + return ORBIS_SYSMODULE_NOT_LOADED; + } + + // By this point, all necessary checks are performed, decrement the load count. + dep_mod.is_loaded--; + + // Normally, this is where the real library would actually unload the module, + // through a call to sceKernelStopUnloadModule. + // As we don't implement module unloading, this behavior is skipped. + + // Stub success during requested module unload. + if (i == 0 && res_out != nullptr) { + *res_out = ORBIS_OK; + } + } + return ORBIS_OK; +} + +s32 preloadModulesForLibkernel() { + // For now, default to loading g_preload_list_3. + // As far as I can tell, g_preload_list_1 seems to be some sort of list with libs + // that games don't typically use, and g_preload_list_2 is just a reorganized version of 3. + s32 sdk_ver = 0; + ASSERT_MSG(Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver) == 0, + "Failed to get compiled SDK version"); + for (u32 module_index : g_preload_list_3) { + // As per usual, these are arrays of indexes for g_modules_array + // libSceDbg, libScePerf, libSceMat, and libSceRazorCpu_debug. + // These are skipped unless this console is a devkit. + if ((module_index == 0x12 || module_index == 0x1e || module_index == 0x24 || + module_index == 0x26) && + !Config::isDevKitConsole()) { + continue; + } + + // libSceDiscMap case, skipped on newer SDK versions. + if (module_index == 0x22 && sdk_ver >= Common::ElfInfo::FW_20) { + continue; + } + + // libSceDbgAssist is skipped on non-testkit consoles. + // For now, stub check to non-devkit. + if (module_index == 0x23 && !Config::isDevKitConsole()) { + continue; + } + + // libSceRazorCpu, skipped for old non-devkit consoles. + if (module_index == 0x25 && sdk_ver < Common::ElfInfo::FW_45 && + !Config::isDevKitConsole()) { + continue; + } + + // libSceHttp2, skipped for SDK versions below 7.00. + if (module_index == 0x28 && sdk_ver < Common::ElfInfo::FW_70) { + continue; + } + + // libSceNpWebApi2 and libSceNpGameIntent, skipped for SDK versions below 7.50 + if ((module_index == 0x29 || module_index == 0x2a) && sdk_ver < Common::ElfInfo::FW_75) { + continue; + } + + // Load the actual module + s32 result = loadModuleInternal(module_index, 0, nullptr, nullptr); + if (result != ORBIS_OK) { + // On real hardware, module preloading must succeed or the game will abort. + // To enable users to test homebrew easier, we'll log a critical error instead. + LOG_CRITICAL(Lib_SysModule, "Failed to preload {}, expect crashes", + g_modules_array[module_index].name); + } + } + return ORBIS_OK; +} + +} // namespace Libraries::SysModule diff --git a/src/core/libraries/sysmodule/sysmodule_internal.h b/src/core/libraries/sysmodule/sysmodule_internal.h new file mode 100644 index 000000000..8f88f85ea --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule_internal.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/kernel/process.h" + +namespace Libraries::SysModule { + +s32 getModuleHandle(s32 id, s32* handle); +bool shouldHideName(const char* module_name); +bool isDebugModule(s32 id); +bool validateModuleId(s32 id); +s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out); +s32 loadModule(s32 id, s32 argc, const void* argv, s32* res_out); +s32 unloadModule(s32 id, s32 argc, const void* argv, s32* res_out, bool is_internal); +s32 preloadModulesForLibkernel(); + +} // namespace Libraries::SysModule \ No newline at end of file diff --git a/src/core/libraries/sysmodule/sysmodule_table.h b/src/core/libraries/sysmodule/sysmodule_table.h new file mode 100644 index 000000000..bd27a8aae --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule_table.h @@ -0,0 +1,684 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Libraries::SysModule { + +/** + * libSceSysmodule hardcodes an array of valuable data about loading each PS4 module. + * This header stores the contents of this array, as dumped from 12.52's libSceSysmodule, + * and altered to fit within my simplified internal module struct. + */ + +// This is an internal struct. Doesn't match the real one exactly. +struct OrbisSysmoduleModuleInternal { + u32 id; // User requested ID + s32 handle; // Handle of the module, once loaded + s32 is_loaded; // 0 by default, set to 1 once loaded. + s32 flags; // Miscellaneous details about the module + const char* name; // Name of the actual SPRX/PRX library + const u16* to_load; // Pointer to an array of modules to load + s32 num_to_load; // Number of indicies in the array of modules +}; + +// This enum contains helpful identifiers for some bits used in the flags of a module. +enum OrbisSysmoduleModuleInternalFlags : s32 { + IsCommon = 1, // Module is located in /system/common/lib + IsPriv = 2, // Module is located in /system/priv/lib + IsGame = 4, // Module is located in /app0/sce_module + IsDebug = 8, // Module should only be loaded on devkit/testkit consoles + IsNeo = 0x200, // Module should only be loaded on PS4 Pro consoles + IsNeoMode = 0x400, // Module should only be loaded for PS4 Pro running in enhanced mode + IsCommonEx = 0x1000, // Module is located in /system_ex/common_ex/lib + IsPrivEx = 0x2000, // Module is located in /system_ex/priv_ex/lib +}; + +// Array of module indexes to load in sceSysmodulePreloadModuleForLibkernel. +// The library has three versions of this array +u32 g_preload_list_1[36] = {0x24, 3, 4, 5, 6, 7, 8, 9, 0x25, 0xb, 0xc, 0xd, + 0xe, 0xf, 0x10, 0x11, 0x1f, 0x12, 0x13, 0x14, 0x27, 0x28, 0x16, 0x17, + 0x2a, 0x18, 0x29, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x26, 0x1e, 0x20, 0x21}; +u32 g_preload_list_2[38] = {1, 2, 0x24, 0x22, 3, 4, 5, 6, 7, 8, + 9, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x1f, 0x12, + 0x23, 0x13, 0x14, 0x27, 0x28, 0x16, 0x17, 0x2a, 0x18, 0x29, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x25, 0x26, 0x1e}; +u32 g_preload_list_3[38] = {1, 2, 0x24, 0x22, 3, 4, 5, 6, 7, 8, + 9, 0x25, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x1f, + 0x12, 0x23, 0x13, 0x14, 0x27, 0x28, 0x16, 0x17, 0x2a, 0x18, + 0x29, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x26, 0x1e}; + +// Arrays of modules to load for each module. +// The stored values are valid indices to modules in g_modules_array. +u16 g_libSceNet_modules[1] = {5}; +u16 g_libSceIpmi_modules[1] = {6}; +u16 g_libSceMbus_modules[2] = {7, 6}; +u16 g_libSceRegMgr_modules[1] = {8}; +u16 g_libSceRtc_modules[1] = {9}; +u16 g_libSceAvSetting_modules[3] = {11, 7, 6}; +u16 g_libSceVideoOut_modules[3] = {12, 11, 7}; +u16 g_libSceGnmDriver_modules[4] = {13, 12, 8, 37}; +u16 g_libSceAudioOut_modules[4] = {14, 11, 7, 6}; +u16 g_libSceAudioIn_modules[4] = {15, 14, 7, 6}; +u16 g_libSceAjm_modules[1] = {16}; +u16 g_libScePad_modules[2] = {17, 7}; +u16 g_libSceDbg_debug_modules[1] = {18}; +u16 g_libSceNetCtl_modules[2] = {19, 6}; +u16 g_libSceHttp_modules[5] = {20, 39, 9, 19, 5}; +u16 g_libSceSsl_modules[3] = {21, 9, 5}; +u16 g_libSceNpCommon_modules[8] = {22, 20, 39, 19, 9, 8, 6, 5}; +u16 g_libSceNpManager_modules[7] = {23, 22, 20, 39, 19, 9, 5}; +u16 g_libSceNpWebApi_modules[7] = {24, 23, 22, 20, 39, 9, 5}; +u16 g_libSceSaveData_modules[4] = {25, 27, 9, 6}; +u16 g_libSceSystemService_modules[3] = {26, 8, 6}; +u16 g_libSceUserService_modules[2] = {27, 6}; +u16 g_libSceCommonDialog_modules[1] = {28}; +u16 g_libSceSysUtil_modules[2] = {29, 8}; +u16 g_libScePerf_debug_modules[3] = {30, 38, 37}; +u16 g_libSceCamera_modules[2] = {31, 7}; +u16 g_libSceDiscMap_modules[1] = {34}; +u16 g_libSceDbgAssist_modules[1] = {35}; +u16 g_libSceMat_debug_modules[1] = {36}; +u16 g_libSceRazorCpu_modules[1] = {37}; +u16 g_libSceRazorCpu_debug_debug_modules[2] = {38, 37}; +u16 g_libSceSsl2_modules[3] = {39, 9, 5}; +u16 g_libSceHttp2_modules[13] = {40, 20, 39, 9, 19, 5, 39, 9, 5, 9, 19, 6, 5}; +u16 g_libSceNpWebApi2_modules[39] = {41, 23, 22, 20, 39, 19, 9, 5, 22, 20, 39, 19, 9, + 8, 6, 5, 40, 20, 39, 9, 19, 5, 39, 9, 5, 9, + 19, 6, 5, 20, 39, 9, 19, 5, 39, 9, 5, 9, 5}; +u16 g_libSceNpGameIntent_modules[10] = {42, 22, 20, 39, 19, 9, 8, 6, 5, 6}; +u16 g_libSceFiber_modules[5] = {49, 114, 30, 38, 37}; +u16 g_libSceUlt_modules[6] = {50, 49, 114, 30, 38, 37}; +u16 g_libSceNgs2_modules[2] = {51, 16}; +u16 g_libSceXml_modules[1] = {52}; +u16 g_libSceNpUtility_modules[5] = {53, 22, 20, 19, 5}; +u16 g_libSceVoice_modules[4] = {54, 16, 15, 14}; +u16 g_libSceNpMatching2_modules[7] = {55, 23, 22, 20, 39, 19, 5}; +u16 g_libSceNpScoreRanking_modules[3] = {56, 23, 22}; +u16 g_libSceRudp_modules[1] = {57}; +u16 g_libSceNpTus_modules[3] = {58, 23, 22}; +u16 g_libSceFace_modules[1] = {59}; +u16 g_libSceSmart_modules[1] = {60}; +u16 g_libSceJson_modules[1] = {61}; +u16 g_libSceGameLiveStreaming_modules[2] = {62, 6}; +u16 g_libSceCompanionUtil_modules[3] = {63, 7, 6}; +u16 g_libScePlayGo_modules[1] = {64}; +u16 g_libSceFont_modules[1] = {65}; +u16 g_libSceVideoRecording_modules[2] = {66, 82}; +u16 g_libSceAudiodec_modules[2] = {67, 16}; +u16 g_libSceJpegDec_modules[1] = {68}; +u16 g_libSceJpegEnc_modules[1] = {69}; +u16 g_libScePngDec_modules[1] = {70}; +u16 g_libScePngEnc_modules[1] = {71}; +u16 g_libSceVideodec_modules[3] = {72, 80, 161}; +u16 g_libSceMove_modules[1] = {73}; +u16 g_libScePadTracker_modules[2] = {75, 17}; +u16 g_libSceDepth_modules[2] = {76, 31}; +u16 g_libSceHand_modules[1] = {77}; +u16 g_libSceIme_modules[2] = {78, 6}; +u16 g_libSceImeDialog_modules[2] = {79, 6}; +u16 g_libSceVdecCore_modules[1] = {80}; +u16 g_libSceNpParty_modules[2] = {81, 6}; +u16 g_libSceAvcap_modules[2] = {82, 6}; +u16 g_libSceFontFt_modules[1] = {83}; +u16 g_libSceFreeTypeOt_modules[1] = {84}; +u16 g_libSceFreeTypeOl_modules[1] = {85}; +u16 g_libSceFreeTypeOptOl_modules[1] = {86}; +u16 g_libSceScreenShot_modules[3] = {87, 29, 6}; +u16 g_libSceNpAuth_modules[3] = {88, 22, 23}; +u16 g_libSceVoiceQos_modules[5] = {89, 54, 16, 15, 14}; +u16 g_libSceSysCore_modules[2] = {90, 6}; +u16 g_libSceM4aacEnc_modules[2] = {91, 16}; +u16 g_libSceAudiodecCpu_modules[1] = {92}; +u16 g_libSceCdlgUtilServer_modules[2] = {93, 26}; +u16 g_libSceSulpha_debug_modules[1] = {94}; +u16 g_libSceSaveDataDialog_modules[4] = {95, 9, 28, 26}; +u16 g_libSceInvitationDialog_modules[1] = {96}; +u16 g_libSceKeyboard_debug_modules[1] = {97}; +u16 g_libSceKeyboard_modules[1] = {98}; +u16 g_libSceMsgDialog_modules[1] = {99}; +u16 g_libSceAvPlayer_modules[1] = {100}; +u16 g_libSceContentExport_modules[1] = {101}; +u16 g_libSceVisionManager_modules[1] = {102}; +u16 g_libSceAc3Enc_modules[2] = {103, 16}; +u16 g_libSceAppInstUtil_modules[1] = {104}; +u16 g_libSceVencCore_modules[1] = {105}; +u16 g_libSceAudio3d_modules[1] = {106}; +u16 g_libSceNpCommerce_modules[1] = {107}; +u16 g_libSceHidControl_modules[1] = {108}; +u16 g_libSceMouse_modules[1] = {109}; +u16 g_libSceCompanionHttpd_modules[1] = {110}; +u16 g_libSceWebBrowserDialog_modules[1] = {111}; +u16 g_libSceErrorDialog_modules[1] = {112}; +u16 g_libSceNpTrophy_modules[1] = {113}; +u16 g_ulobjmgr_modules[1] = {114}; +u16 g_libSceVideoCoreInterface_modules[1] = {115}; +u16 g_libSceVideoCoreServerInterface_modules[1] = {116}; +u16 g_libSceNpSns_modules[1] = {117}; +u16 g_libSceNpSnsFacebookDialog_modules[2] = {118, 117}; +u16 g_libSceMoveTracker_modules[1] = {119}; +u16 g_libSceNpProfileDialog_modules[1] = {120}; +u16 g_libSceNpFriendListDialog_modules[1] = {121}; +u16 g_libSceAppContent_modules[1] = {122}; +u16 g_libSceMarlin_modules[1] = {123}; +u16 g_libSceDtsEnc_modules[2] = {124, 16}; +u16 g_libSceNpSignaling_modules[1] = {125}; +u16 g_libSceRemoteplay_modules[1] = {126}; +u16 g_libSceUsbd_modules[1] = {127}; +u16 g_libSceGameCustomDataDialog_modules[1] = {128}; +u16 g_libSceNpEulaDialog_modules[1] = {129}; +u16 g_libSceRandom_modules[1] = {130}; +u16 g_libSceDipsw_modules[1] = {131}; +u16 g_libSceS3DConversion_modules[1] = {132}; +u16 g_libSceOttvCapture_debug_modules[1] = {133}; +u16 g_libSceBgft_modules[1] = {134}; +u16 g_libSceAudiodecCpuDdp_modules[1] = {135}; +u16 g_libSceAudiodecCpuM4aac_modules[1] = {136}; +u16 g_libSceAudiodecCpuDts_modules[1] = {137}; +u16 g_libSceAudiodecCpuDtsHdLbr_modules[1] = {138}; +u16 g_libSceAudiodecCpuDtsHdMa_modules[1] = {139}; +u16 g_libSceAudiodecCpuLpcm_modules[1] = {140}; +u16 g_libSceBemp2sys_modules[1] = {141}; +u16 g_libSceBeisobmf_modules[1] = {142}; +u16 g_libScePlayReady_modules[1] = {143}; +u16 g_libSceVideoNativeExtEssential_modules[1] = {144}; +u16 g_libSceZlib_modules[1] = {145}; +u16 g_libSceIduUtil_modules[1] = {146}; +u16 g_libScePsm_modules[1] = {147}; +u16 g_libSceDtcpIp_modules[1] = {148}; +u16 g_libSceKbEmulate_modules[1] = {149}; +u16 g_libSceAppChecker_modules[1] = {150}; +u16 g_libSceNpGriefReport_modules[1] = {151}; +u16 g_libSceContentSearch_modules[1] = {152}; +u16 g_libSceShareUtility_modules[1] = {153}; +u16 g_libSceWeb_modules[6] = {154, 155, 147, 192, 27, 6}; +u16 g_libSceWebKit2_modules[30] = {155, 266, 90, 6, 8, 255, 192, 116, 266, 90, 6, 8, 12, 11, 7, + 17, 7, 26, 8, 6, 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceDeci4h_debug_modules[1] = {156}; +u16 g_libSceHeadTracker_modules[1] = {157}; +u16 g_libSceGameUpdate_modules[2] = {158, 6}; +u16 g_libSceAutoMounterClient_modules[2] = {159, 6}; +u16 g_libSceSystemGesture_modules[1] = {160}; +u16 g_libSceVdecSavc_modules[1] = {161}; +u16 g_libSceVdecSavc2_modules[1] = {162}; +u16 g_libSceVideodec2_modules[3] = {163, 80, 162}; +u16 g_libSceVdecwrap_modules[2] = {164, 80}; +u16 g_libSceVshctl_modules[1] = {165}; +u16 g_libSceAt9Enc_modules[1] = {166}; +u16 g_libSceConvertKeycode_modules[1] = {167}; +u16 g_libSceGpuException_modules[1] = {168}; +u16 g_libSceSharePlay_modules[1] = {169}; +u16 g_libSceAudiodReport_modules[1] = {170}; +u16 g_libSceSulphaDrv_modules[1] = {171}; +u16 g_libSceHmd_modules[1] = {172}; +u16 g_libSceUsbStorage_modules[2] = {173, 6}; +u16 g_libSceVdecShevc_modules[1] = {174}; +u16 g_libSceUsbStorageDialog_modules[1] = {175}; +u16 g_libSceFaceTracker_modules[2] = {176, 59}; +u16 g_libSceHandTracker_modules[1] = {177}; +u16 g_libSceNpSnsYouTubeDialog_modules[2] = {178, 117}; +u16 g_libSceVrTracker_modules[6] = {179, 6, 172, 31, 17, 73}; +u16 g_libSceProfileCacheExternal_modules[2] = {180, 6}; +u16 g_libSceBackupRestoreUtil_modules[1] = {181}; +u16 g_libSceMusicPlayerService_modules[2] = {182, 183}; +u16 g_libSceMusicCoreServerClientJsEx_modules[1] = {183}; +u16 g_libSceSpSysCallWrapper_modules[3] = {184, 19, 6}; +u16 g_libScePs2EmuMenuDialog_modules[1] = {185}; +u16 g_libSceNpSnsDailyMotionDialog_modules[1] = {186}; +u16 g_libSceAudiodecCpuHevag_modules[1] = {187}; +u16 g_libSceLoginDialog_modules[2] = {188, 6}; +u16 g_libSceLoginService_modules[2] = {189, 6}; +u16 g_libSceSigninDialog_modules[2] = {190, 6}; +u16 g_libSceVdecsw_modules[3] = {191, 80, 162}; +u16 g_libSceOrbisCompat_modules[24] = {192, 116, 266, 90, 6, 8, 12, 11, 7, 17, 7, 26, + 8, 6, 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceCoreIPC_modules[1] = {193}; +u16 g_libSceCustomMusicCore_modules[12] = {194, 29, 8, 27, 6, 14, 11, 7, 6, 11, 7, 6}; +u16 g_libSceJson2_modules[1] = {195}; +u16 g_libSceAudioLatencyEstimation_modules[1] = {196}; +u16 g_libSceWkFontConfig_modules[1] = {197}; +u16 g_libSceVorbisDec_modules[3] = {198, 67, 16}; +u16 g_libSceTtsCoreEnUs_modules[1] = {199}; +u16 g_libSceTtsCoreJp_modules[1] = {200}; +u16 g_libSceOpusCeltEnc_modules[2] = {201, 16}; +u16 g_libSceOpusCeltDec_modules[2] = {202, 16}; +u16 g_libSceLoginMgrServer_modules[1] = {203}; +u16 g_libSceHmdSetupDialog_modules[1] = {204}; +u16 g_libSceVideoOutSecondary_modules[6] = {205, 82, 6, 12, 11, 7}; +u16 g_libSceContentDelete_modules[1] = {206}; +u16 g_libSceImeBackend_modules[1] = {207}; +u16 g_libSceNetCtlApDialog_modules[1] = {208}; +u16 g_libSceGnmResourceRegistration_modules[1] = {209}; +u16 g_libScePlayGoDialog_modules[1] = {210}; +u16 g_libSceSocialScreen_modules[7] = {211, 205, 82, 6, 12, 11, 7}; +u16 g_libSceEditMp4_modules[1] = {212}; +u16 g_libScePsmKitSystem_modules[1] = {221}; +u16 g_libSceTextToSpeech_modules[1] = {222}; +u16 g_libSceNpToolkit_modules[1] = {223}; +u16 g_libSceCustomMusicService_modules[2] = {224, 183}; +u16 g_libSceClSysCallWrapper_modules[11] = {225, 20, 39, 9, 19, 5, 39, 9, 5, 67, 16}; +u16 g_libSceScm_modules[1] = {226}; +u16 g_libSceSystemLogger_modules[2] = {227, 6}; +u16 g_libSceBluetoothHid_modules[1] = {228}; +u16 g_libSceAvPlayerStreaming_modules[1] = {229}; +u16 g_libSceAudiodecCpuAlac_modules[1] = {230}; +u16 g_libSceVideoDecoderArbitration_modules[1] = {231}; +u16 g_libSceVrServiceDialog_modules[1] = {232}; +u16 g_libSceJobManager_modules[2] = {233, 114}; +u16 g_libSceAudiodecCpuFlac_modules[1] = {234}; +u16 g_libSceSrcUtl_modules[2] = {235, 16}; +u16 g_libSceS3da_modules[1] = {236}; +u16 g_libSceDseehx_modules[1] = {237}; +u16 g_libSceShareFactoryUtil_modules[1] = {238}; +u16 g_libSceDataTransfer_modules[1] = {239}; +u16 g_libSceSocialScreenDialog_modules[1] = {240}; +u16 g_libSceAbstractStorage_modules[1] = {241}; +u16 g_libSceImageUtil_modules[1] = {242}; +u16 g_libSceMetadataReaderWriter_modules[1] = {243}; +u16 g_libSceJpegParser_modules[1] = {244}; +u16 g_libSceGvMp4Parser_modules[1] = {245}; +u16 g_libScePngParser_modules[1] = {246}; +u16 g_libSceGifParser_modules[1] = {247}; +u16 g_libSceNpSnsDialog_modules[2] = {248, 117}; +u16 g_libSceAbstractLocal_modules[1] = {249}; +u16 g_libSceAbstractFacebook_modules[1] = {250}; +u16 g_libSceAbstractYoutube_modules[1] = {251}; +u16 g_libSceAbstractTwitter_modules[1] = {252}; +u16 g_libSceAbstractDailymotion_modules[1] = {253}; +u16 g_libSceNpToolkit2_modules[1] = {254}; +u16 g_libScePrecompiledShaders_modules[1] = {255}; +u16 g_libSceDiscId_modules[1] = {256}; +u16 g_libSceLibreSsl_modules[2] = {257, 130}; +u16 g_libSceFsInternalForVsh_modules[1] = {258}; +u16 g_libSceNpUniversalDataSystem_modules[1] = {259}; +u16 g_libSceDolbyVision_modules[1] = {260}; +u16 g_libSceOpusSilkEnc_modules[2] = {261, 16}; +u16 g_libSceOpusDec_modules[2] = {262, 16}; +u16 g_libSceWebKit2Secure_modules[34] = {263, 265, 26, 8, 6, 266, 90, 6, 8, 255, 192, 116, + 266, 90, 6, 8, 12, 11, 7, 17, 7, 26, 8, 6, + 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceJscCompiler_modules[1] = {264}; +u16 g_libSceJitBridge_modules[4] = {265, 26, 8, 6}; +u16 g_libScePigletv2VSH_modules[4] = {266, 90, 6, 8}; +u16 g_libSceJitBridge_common_ex_modules[4] = {267, 26, 8, 6}; +u16 g_libSceJscCompiler_common_ex_modules[1] = {268}; +u16 g_libSceOrbisCompat_common_ex_modules[24] = {269, 116, 266, 90, 6, 8, 12, 11, 7, 17, 7, 26, + 8, 6, 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceWeb_common_ex_modules[6] = {270, 271, 147, 269, 27, 6}; +u16 g_libSceWebKit2_common_ex_modules[30] = {271, 266, 90, 6, 8, 273, 269, 116, 266, 90, + 6, 8, 12, 11, 7, 17, 7, 26, 8, 6, + 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceWebKit2Secure_common_ex_modules[34] = { + 272, 267, 26, 8, 6, 266, 90, 6, 8, 273, 269, 116, 266, 90, 6, 8, 12, + 11, 7, 17, 7, 26, 8, 6, 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libScePrecompiledShaders_common_ex_modules[1] = {273}; +u16 g_libSceGic_modules[1] = {274}; +u16 g_libSceRnpsAppMgr_modules[1] = {275}; +u16 g_libSceAsyncStorageInternal_modules[1] = {276}; +u16 g_libSceHttpCache_modules[1] = {277}; +u16 g_libScePlayReady2_modules[1] = {278}; +u16 g_libSceHdrScopes_debug_modules[1] = {279}; +u16 g_libSceNKWeb_modules[1] = {280}; +u16 g_libSceNKWebKit_modules[2] = {281, 282}; +u16 g_libSceNKWebKitRequirements_modules[1] = {282}; +u16 g_libSceVnaInternal_modules[1] = {283}; +u16 g_libSceVnaWebsocket_modules[1] = {284}; +u16 g_libSceCesCs_modules[1] = {285}; +u16 g_libSceComposite_modules[1] = {286}; +u16 g_libSceCompositeExt_modules[1] = {287}; +u16 g_libSceHubAppUtil_modules[1] = {288}; +u16 g_libScePosixForWebKit_modules[1] = {289}; +u16 g_libSceNpPartner001_modules[1] = {290}; +u16 g_libSceNpSessionSignaling_modules[75] = { + 291, 41, 23, 22, 20, 39, 19, 9, 5, 22, 20, 39, 19, 9, 8, 6, 5, 40, 20, 39, 9, 19, 5, 39, 9, + 5, 9, 19, 6, 5, 20, 39, 9, 19, 5, 39, 9, 5, 9, 5, 22, 20, 39, 19, 9, 8, 6, 5, 23, 22, + 20, 39, 19, 9, 5, 40, 20, 39, 9, 19, 5, 39, 9, 5, 9, 19, 6, 5, 39, 9, 5, 19, 6, 5, 9}; +u16 g_libScePlayerInvitationDialog_modules[1] = {292}; +u16 g_libSceNpCppWebApi_modules[42] = {293, 195, 41, 23, 22, 20, 39, 19, 9, 5, 22, 20, 39, 19, + 9, 8, 6, 5, 40, 20, 39, 9, 19, 5, 39, 9, 5, 9, + 19, 6, 5, 20, 39, 9, 19, 5, 39, 9, 5, 9, 5, 9}; +u16 g_libSceNpEntitlementAccess_modules[1] = {294}; +u16 g_libSceNpRemotePlaySessionSignaling_modules[76] = { + 295, 291, 41, 23, 22, 20, 39, 19, 9, 5, 22, 20, 39, 19, 9, 8, 6, 5, 40, + 20, 39, 9, 19, 5, 39, 9, 5, 9, 19, 6, 5, 20, 39, 9, 19, 5, 39, 9, + 5, 9, 5, 22, 20, 39, 19, 9, 8, 6, 5, 23, 22, 20, 39, 19, 9, 5, 40, + 20, 39, 9, 19, 5, 39, 9, 5, 9, 19, 6, 5, 39, 9, 5, 19, 6, 5, 9}; +u16 g_libSceLibreSsl3_modules[2] = {296, 130}; +u16 g_libcurl_modules[2] = {297, 289}; +u16 g_libicu_modules[2] = {298, 289}; +u16 g_libcairo_modules[9] = {299, 300, 301, 302, 303, 289, 298, 289, 289}; +u16 g_libfontconfig_modules[1] = {300}; +u16 g_libfreetype_modules[1] = {301}; +u16 g_libharfbuzz_modules[1] = {302}; +u16 g_libpng16_modules[2] = {303, 289}; +u16 g_libSceFontGs_modules[1] = {304}; +u16 g_libSceGLSlimClientVSH_modules[1] = {305}; +u16 g_libSceGLSlimServerVSH_modules[1] = {306}; +u16 g_libSceFontGsm_modules[1] = {307}; +u16 g_libSceNpPartnerSubscription_modules[1] = {308}; +u16 g_libSceNpAuthAuthorizedAppDialog_modules[1] = {309}; + +// This is the actual array of modules. +constexpr u64 g_num_modules = 310; +std::array g_modules_array = std::to_array< + OrbisSysmoduleModuleInternal>( + {{0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 1, "libkernel", nullptr, 0}, + {0x0, -1, 0, 1, "libSceLibcInternal", nullptr, 0}, + {0x0, -1, 0, 4, "libSceFios2", nullptr, 0}, + {0x0, -1, 0, 4, "libc", nullptr, 0}, + {0x8000001c, -1, 0, 1, "libSceNet", g_libSceNet_modules, 1}, + {0x8000001d, -1, 0, 1, "libSceIpmi", g_libSceIpmi_modules, 1}, + {0x8000001e, -1, 0, 1, "libSceMbus", g_libSceMbus_modules, 2}, + {0x8000001f, -1, 0, 1, "libSceRegMgr", g_libSceRegMgr_modules, 1}, + {0x80000020, -1, 0, 1, "libSceRtc", g_libSceRtc_modules, 1}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x80000021, -1, 0, 1, "libSceAvSetting", g_libSceAvSetting_modules, 3}, + {0x80000022, -1, 0, 1, "libSceVideoOut", g_libSceVideoOut_modules, 3}, + {0x80000052, -1, 0, 1025, "libSceGnmDriver", g_libSceGnmDriver_modules, 4}, + {0x80000001, -1, 0, 1, "libSceAudioOut", g_libSceAudioOut_modules, 4}, + {0x80000002, -1, 0, 1, "libSceAudioIn", g_libSceAudioIn_modules, 4}, + {0x80000023, -1, 0, 1, "libSceAjm", g_libSceAjm_modules, 1}, + {0x80000024, -1, 0, 1, "libScePad", g_libScePad_modules, 2}, + {0x80000025, -1, 0, 9, "libSceDbg", g_libSceDbg_debug_modules, 1}, + {0x80000009, -1, 0, 1, "libSceNetCtl", g_libSceNetCtl_modules, 2}, + {0x8000000a, -1, 0, 1, "libSceHttp", g_libSceHttp_modules, 5}, + {0x0, -1, 0, 1, "libSceSsl", g_libSceSsl_modules, 3}, + {0x8000000c, -1, 0, 1, "libSceNpCommon", g_libSceNpCommon_modules, 8}, + {0x8000000d, -1, 0, 1, "libSceNpManager", g_libSceNpManager_modules, 7}, + {0x8000000e, -1, 0, 1, "libSceNpWebApi", g_libSceNpWebApi_modules, 7}, + {0x8000000f, -1, 0, 1, "libSceSaveData", g_libSceSaveData_modules, 4}, + {0x80000010, -1, 0, 1, "libSceSystemService", g_libSceSystemService_modules, 3}, + {0x80000011, -1, 0, 1, "libSceUserService", g_libSceUserService_modules, 2}, + {0x80000018, -1, 0, 1, "libSceCommonDialog", g_libSceCommonDialog_modules, 1}, + {0x80000026, -1, 0, 1, "libSceSysUtil", g_libSceSysUtil_modules, 2}, + {0x80000019, -1, 0, 9, "libScePerf", g_libScePerf_debug_modules, 3}, + {0x8000001a, -1, 0, 1, "libSceCamera", g_libSceCamera_modules, 2}, + {0x0, -1, 0, 1, "libSceWebKit2ForVideoService", nullptr, 0}, + {0x0, -1, 0, 1, "libSceOrbisCompatForVideoService", nullptr, 0}, + {0xd7, -1, 0, 1, "libSceDiscMap", g_libSceDiscMap_modules, 1}, + {0x8000003d, -1, 0, 129, "libSceDbgAssist", g_libSceDbgAssist_modules, 1}, + {0x80000048, -1, 0, 9, "libSceMat", g_libSceMat_debug_modules, 1}, + {0x0, -1, 0, 1, "libSceRazorCpu", g_libSceRazorCpu_modules, 1}, + {0x80000075, -1, 0, 9, "libSceRazorCpu_debug", g_libSceRazorCpu_debug_debug_modules, 2}, + {0x8000000b, -1, 0, 1, "libSceSsl2", g_libSceSsl2_modules, 3}, + {0x8000008c, -1, 0, 1, "libSceHttp2", g_libSceHttp2_modules, 13}, + {0x8000008f, -1, 0, 1, "libSceNpWebApi2", g_libSceNpWebApi2_modules, 39}, + {0x8000008d, -1, 0, 1, "libSceNpGameIntent", g_libSceNpGameIntent_modules, 10}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x6, -1, 0, 1, "libSceFiber", g_libSceFiber_modules, 5}, + {0x7, -1, 0, 1, "libSceUlt", g_libSceUlt_modules, 6}, + {0xb, -1, 0, 1, "libSceNgs2", g_libSceNgs2_modules, 2}, + {0x17, -1, 0, 1, "libSceXml", g_libSceXml_modules, 1}, + {0x19, -1, 0, 1, "libSceNpUtility", g_libSceNpUtility_modules, 5}, + {0x1a, -1, 0, 1, "libSceVoice", g_libSceVoice_modules, 4}, + {0x1c, -1, 0, 1, "libSceNpMatching2", g_libSceNpMatching2_modules, 7}, + {0x1e, -1, 0, 1, "libSceNpScoreRanking", g_libSceNpScoreRanking_modules, 3}, + {0x21, -1, 0, 1, "libSceRudp", g_libSceRudp_modules, 1}, + {0x2c, -1, 0, 1, "libSceNpTus", g_libSceNpTus_modules, 3}, + {0x38, -1, 0, 4, "libSceFace", g_libSceFace_modules, 1}, + {0x39, -1, 0, 4, "libSceSmart", g_libSceSmart_modules, 1}, + {0x80, -1, 0, 1, "libSceJson", g_libSceJson_modules, 1}, + {0x81, -1, 0, 1, "libSceGameLiveStreaming", g_libSceGameLiveStreaming_modules, 2}, + {0x82, -1, 0, 1, "libSceCompanionUtil", g_libSceCompanionUtil_modules, 3}, + {0x83, -1, 0, 1, "libScePlayGo", g_libScePlayGo_modules, 1}, + {0x84, -1, 0, 1, "libSceFont", g_libSceFont_modules, 1}, + {0x85, -1, 0, 1, "libSceVideoRecording", g_libSceVideoRecording_modules, 2}, + {0x88, -1, 0, 1, "libSceAudiodec", g_libSceAudiodec_modules, 2}, + {0x8a, -1, 0, 1, "libSceJpegDec", g_libSceJpegDec_modules, 1}, + {0x8b, -1, 0, 1, "libSceJpegEnc", g_libSceJpegEnc_modules, 1}, + {0x8c, -1, 0, 1, "libScePngDec", g_libScePngDec_modules, 1}, + {0x8d, -1, 0, 1, "libScePngEnc", g_libScePngEnc_modules, 1}, + {0x8e, -1, 0, 2049, "libSceVideodec", g_libSceVideodec_modules, 3}, + {0x8f, -1, 0, 1, "libSceMove", g_libSceMove_modules, 1}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x91, -1, 0, 1, "libScePadTracker", g_libScePadTracker_modules, 2}, + {0x92, -1, 0, 1, "libSceDepth", g_libSceDepth_modules, 2}, + {0x93, -1, 0, 4, "libSceHand", g_libSceHand_modules, 1}, + {0x95, -1, 0, 1, "libSceIme", g_libSceIme_modules, 2}, + {0x96, -1, 0, 1, "libSceImeDialog", g_libSceImeDialog_modules, 2}, + {0x80000015, -1, 0, 1, "libSceVdecCore", g_libSceVdecCore_modules, 1}, + {0x97, -1, 0, 1, "libSceNpParty", g_libSceNpParty_modules, 2}, + {0x80000003, -1, 0, 1, "libSceAvcap", g_libSceAvcap_modules, 2}, + {0x98, -1, 0, 1, "libSceFontFt", g_libSceFontFt_modules, 1}, + {0x99, -1, 0, 1, "libSceFreeTypeOt", g_libSceFreeTypeOt_modules, 1}, + {0x9a, -1, 0, 1, "libSceFreeTypeOl", g_libSceFreeTypeOl_modules, 1}, + {0x9b, -1, 0, 1, "libSceFreeTypeOptOl", g_libSceFreeTypeOptOl_modules, 1}, + {0x9c, -1, 0, 1, "libSceScreenShot", g_libSceScreenShot_modules, 3}, + {0x9d, -1, 0, 1, "libSceNpAuth", g_libSceNpAuth_modules, 3}, + {0x1b, -1, 0, 1, "libSceVoiceQos", g_libSceVoiceQos_modules, 5}, + {0x80000004, -1, 0, 1, "libSceSysCore", g_libSceSysCore_modules, 2}, + {0xbc, -1, 0, 1, "libSceM4aacEnc", g_libSceM4aacEnc_modules, 2}, + {0xbd, -1, 0, 1, "libSceAudiodecCpu", g_libSceAudiodecCpu_modules, 1}, + {0x80000007, -1, 0, 1, "libSceCdlgUtilServer", g_libSceCdlgUtilServer_modules, 2}, + {0x9f, -1, 0, 9, "libSceSulpha", g_libSceSulpha_debug_modules, 1}, + {0xa0, -1, 0, 1, "libSceSaveDataDialog", g_libSceSaveDataDialog_modules, 4}, + {0xa2, -1, 0, 1, "libSceInvitationDialog", g_libSceInvitationDialog_modules, 1}, + {0xa3, -1, 0, 2057, "libSceKeyboard", g_libSceKeyboard_debug_modules, 1}, + {0x106, -1, 0, 2049, "libSceKeyboard", g_libSceKeyboard_modules, 1}, + {0xa4, -1, 0, 1, "libSceMsgDialog", g_libSceMsgDialog_modules, 1}, + {0xa5, -1, 0, 1, "libSceAvPlayer", g_libSceAvPlayer_modules, 1}, + {0xa6, -1, 0, 1, "libSceContentExport", g_libSceContentExport_modules, 1}, + {0x80000012, -1, 0, 2, "libSceVisionManager", g_libSceVisionManager_modules, 1}, + {0x80000013, -1, 0, 2, "libSceAc3Enc", g_libSceAc3Enc_modules, 2}, + {0x80000014, -1, 0, 1, "libSceAppInstUtil", g_libSceAppInstUtil_modules, 1}, + {0x80000016, -1, 0, 514, "libSceVencCore", g_libSceVencCore_modules, 1}, + {0xa7, -1, 0, 1, "libSceAudio3d", g_libSceAudio3d_modules, 1}, + {0xa8, -1, 0, 1, "libSceNpCommerce", g_libSceNpCommerce_modules, 1}, + {0x80000017, -1, 0, 1, "libSceHidControl", g_libSceHidControl_modules, 1}, + {0xa9, -1, 0, 1, "libSceMouse", g_libSceMouse_modules, 1}, + {0xaa, -1, 0, 1, "libSceCompanionHttpd", g_libSceCompanionHttpd_modules, 1}, + {0xab, -1, 0, 1, "libSceWebBrowserDialog", g_libSceWebBrowserDialog_modules, 1}, + {0xac, -1, 0, 1, "libSceErrorDialog", g_libSceErrorDialog_modules, 1}, + {0xad, -1, 0, 1, "libSceNpTrophy", g_libSceNpTrophy_modules, 1}, + {0x0, -1, 0, 1, "ulobjmgr", g_ulobjmgr_modules, 1}, + {0xae, -1, 0, 1, "libSceVideoCoreInterface", g_libSceVideoCoreInterface_modules, 1}, + {0xaf, -1, 0, 1, "libSceVideoCoreServerInterface", g_libSceVideoCoreServerInterface_modules, + 1}, + {0x8000001b, -1, 0, 1, "libSceNpSns", g_libSceNpSns_modules, 1}, + {0xb0, -1, 0, 1, "libSceNpSnsFacebookDialog", g_libSceNpSnsFacebookDialog_modules, 2}, + {0xb1, -1, 0, 1, "libSceMoveTracker", g_libSceMoveTracker_modules, 1}, + {0xb2, -1, 0, 1, "libSceNpProfileDialog", g_libSceNpProfileDialog_modules, 1}, + {0xb3, -1, 0, 1, "libSceNpFriendListDialog", g_libSceNpFriendListDialog_modules, 1}, + {0xb4, -1, 0, 1, "libSceAppContent", g_libSceAppContent_modules, 1}, + {0x80000027, -1, 0, 2, "libSceMarlin", g_libSceMarlin_modules, 1}, + {0x80000028, -1, 0, 2, "libSceDtsEnc", g_libSceDtsEnc_modules, 2}, + {0xb5, -1, 0, 1, "libSceNpSignaling", g_libSceNpSignaling_modules, 1}, + {0xb6, -1, 0, 1, "libSceRemoteplay", g_libSceRemoteplay_modules, 1}, + {0xb7, -1, 0, 1, "libSceUsbd", g_libSceUsbd_modules, 1}, + {0xb8, -1, 0, 1, "libSceGameCustomDataDialog", g_libSceGameCustomDataDialog_modules, 1}, + {0xb9, -1, 0, 1, "libSceNpEulaDialog", g_libSceNpEulaDialog_modules, 1}, + {0xba, -1, 0, 1, "libSceRandom", g_libSceRandom_modules, 1}, + {0x80000029, -1, 0, 2, "libSceDipsw", g_libSceDipsw_modules, 1}, + {0x86, -1, 0, 4, "libSceS3DConversion", g_libSceS3DConversion_modules, 1}, + {0x8000003e, -1, 0, 9, "libSceOttvCapture", g_libSceOttvCapture_debug_modules, 1}, + {0x8000002a, -1, 0, 1, "libSceBgft", g_libSceBgft_modules, 1}, + {0xbe, -1, 0, 1, "libSceAudiodecCpuDdp", g_libSceAudiodecCpuDdp_modules, 1}, + {0xc0, -1, 0, 1, "libSceAudiodecCpuM4aac", g_libSceAudiodecCpuM4aac_modules, 1}, + {0x8000002b, -1, 0, 2, "libSceAudiodecCpuDts", g_libSceAudiodecCpuDts_modules, 1}, + {0xc9, -1, 0, 1, "libSceAudiodecCpuDtsHdLbr", g_libSceAudiodecCpuDtsHdLbr_modules, 1}, + {0x8000002d, -1, 0, 2, "libSceAudiodecCpuDtsHdMa", g_libSceAudiodecCpuDtsHdMa_modules, 1}, + {0x8000002e, -1, 0, 2, "libSceAudiodecCpuLpcm", g_libSceAudiodecCpuLpcm_modules, 1}, + {0xc1, -1, 0, 1, "libSceBemp2sys", g_libSceBemp2sys_modules, 1}, + {0xc2, -1, 0, 1, "libSceBeisobmf", g_libSceBeisobmf_modules, 1}, + {0xc3, -1, 0, 1, "libScePlayReady", g_libScePlayReady_modules, 1}, + {0xc4, -1, 0, 1, "libSceVideoNativeExtEssential", g_libSceVideoNativeExtEssential_modules, 1}, + {0xc5, -1, 0, 1, "libSceZlib", g_libSceZlib_modules, 1}, + {0x8000002f, -1, 0, 1, "libSceIduUtil", g_libSceIduUtil_modules, 1}, + {0x80000030, -1, 0, 1, "libScePsm", g_libScePsm_modules, 1}, + {0xc6, -1, 0, 1, "libSceDtcpIp", g_libSceDtcpIp_modules, 1}, + {0x80000031, -1, 0, 1, "libSceKbEmulate", g_libSceKbEmulate_modules, 1}, + {0x80000032, -1, 0, 2, "libSceAppChecker", g_libSceAppChecker_modules, 1}, + {0x80000033, -1, 0, 1, "libSceNpGriefReport", g_libSceNpGriefReport_modules, 1}, + {0xc7, -1, 0, 1, "libSceContentSearch", g_libSceContentSearch_modules, 1}, + {0xc8, -1, 0, 1, "libSceShareUtility", g_libSceShareUtility_modules, 1}, + {0x80000034, -1, 0, 1, "libSceWeb", g_libSceWeb_modules, 6}, + {0x8000006a, -1, 0, 1, "libSceWebKit2", g_libSceWebKit2_modules, 30}, + {0xca, -1, 0, 9, "libSceDeci4h", g_libSceDeci4h_debug_modules, 1}, + {0xcb, -1, 0, 4, "libSceHeadTracker", g_libSceHeadTracker_modules, 1}, + {0xcc, -1, 0, 1, "libSceGameUpdate", g_libSceGameUpdate_modules, 2}, + {0xcd, -1, 0, 1, "libSceAutoMounterClient", g_libSceAutoMounterClient_modules, 2}, + {0xce, -1, 0, 1, "libSceSystemGesture", g_libSceSystemGesture_modules, 1}, + {0x80000035, -1, 0, 1, "libSceVdecSavc", g_libSceVdecSavc_modules, 1}, + {0x80000036, -1, 0, 1, "libSceVdecSavc2", g_libSceVdecSavc2_modules, 1}, + {0xcf, -1, 0, 2049, "libSceVideodec2", g_libSceVideodec2_modules, 3}, + {0xd0, -1, 0, 1, "libSceVdecwrap", g_libSceVdecwrap_modules, 2}, + {0x80000037, -1, 0, 1, "libSceVshctl", g_libSceVshctl_modules, 1}, + {0xd1, -1, 0, 1, "libSceAt9Enc", g_libSceAt9Enc_modules, 1}, + {0xd2, -1, 0, 1, "libSceConvertKeycode", g_libSceConvertKeycode_modules, 1}, + {0x80000039, -1, 0, 1, "libSceGpuException", g_libSceGpuException_modules, 1}, + {0xd3, -1, 0, 1, "libSceSharePlay", g_libSceSharePlay_modules, 1}, + {0x8000003a, -1, 0, 2, "libSceAudiodReport", g_libSceAudiodReport_modules, 1}, + {0x8000003b, -1, 0, 2, "libSceSulphaDrv", g_libSceSulphaDrv_modules, 1}, + {0xd4, -1, 0, 1, "libSceHmd", g_libSceHmd_modules, 1}, + {0xd5, -1, 0, 1, "libSceUsbStorage", g_libSceUsbStorage_modules, 2}, + {0x8000003c, -1, 0, 1, "libSceVdecShevc", g_libSceVdecShevc_modules, 1}, + {0xd6, -1, 0, 1, "libSceUsbStorageDialog", g_libSceUsbStorageDialog_modules, 1}, + {0xd8, -1, 0, 4, "libSceFaceTracker", g_libSceFaceTracker_modules, 2}, + {0xd9, -1, 0, 4, "libSceHandTracker", g_libSceHandTracker_modules, 1}, + {0xda, -1, 0, 1, "libSceNpSnsYouTubeDialog", g_libSceNpSnsYouTubeDialog_modules, 2}, + {0xed, -1, 0, 1, "libSceVrTracker", g_libSceVrTracker_modules, 6}, + {0xdc, -1, 0, 1, "libSceProfileCacheExternal", g_libSceProfileCacheExternal_modules, 2}, + {0x8000003f, -1, 0, 1, "libSceBackupRestoreUtil", g_libSceBackupRestoreUtil_modules, 1}, + {0xdd, -1, 0, 1, "libSceMusicPlayerService", g_libSceMusicPlayerService_modules, 2}, + {0x0, -1, 0, 1, "libSceMusicCoreServerClientJsEx", g_libSceMusicCoreServerClientJsEx_modules, + 1}, + {0xde, -1, 0, 1, "libSceSpSysCallWrapper", g_libSceSpSysCallWrapper_modules, 3}, + {0xdf, -1, 0, 1, "libScePs2EmuMenuDialog", g_libScePs2EmuMenuDialog_modules, 1}, + {0xe0, -1, 0, 1, "libSceNpSnsDailyMotionDialog", g_libSceNpSnsDailyMotionDialog_modules, 1}, + {0xe1, -1, 0, 1, "libSceAudiodecCpuHevag", g_libSceAudiodecCpuHevag_modules, 1}, + {0xe2, -1, 0, 1, "libSceLoginDialog", g_libSceLoginDialog_modules, 2}, + {0xe3, -1, 0, 1, "libSceLoginService", g_libSceLoginService_modules, 2}, + {0xe4, -1, 0, 1, "libSceSigninDialog", g_libSceSigninDialog_modules, 2}, + {0xe5, -1, 0, 1, "libSceVdecsw", g_libSceVdecsw_modules, 3}, + {0x8000006d, -1, 0, 1, "libSceOrbisCompat", g_libSceOrbisCompat_modules, 24}, + {0x0, -1, 0, 1, "libSceCoreIPC", g_libSceCoreIPC_modules, 1}, + {0xe6, -1, 0, 1, "libSceCustomMusicCore", g_libSceCustomMusicCore_modules, 12}, + {0xe7, -1, 0, 1, "libSceJson2", g_libSceJson2_modules, 1}, + {0xe8, -1, 0, 4, "libSceAudioLatencyEstimation", g_libSceAudioLatencyEstimation_modules, 1}, + {0xe9, -1, 0, 1, "libSceWkFontConfig", g_libSceWkFontConfig_modules, 1}, + {0xea, -1, 0, 2, "libSceVorbisDec", g_libSceVorbisDec_modules, 3}, + {0x80000041, -1, 0, 1, "libSceTtsCoreEnUs", g_libSceTtsCoreEnUs_modules, 1}, + {0x80000042, -1, 0, 1, "libSceTtsCoreJp", g_libSceTtsCoreJp_modules, 1}, + {0x80000043, -1, 0, 1, "libSceOpusCeltEnc", g_libSceOpusCeltEnc_modules, 2}, + {0x80000044, -1, 0, 1, "libSceOpusCeltDec", g_libSceOpusCeltDec_modules, 2}, + {0x80000045, -1, 0, 2, "libSceLoginMgrServer", g_libSceLoginMgrServer_modules, 1}, + {0xeb, -1, 0, 1, "libSceHmdSetupDialog", g_libSceHmdSetupDialog_modules, 1}, + {0x80000046, -1, 0, 1, "libSceVideoOutSecondary", g_libSceVideoOutSecondary_modules, 6}, + {0xee, -1, 0, 1, "libSceContentDelete", g_libSceContentDelete_modules, 1}, + {0xef, -1, 0, 1, "libSceImeBackend", g_libSceImeBackend_modules, 1}, + {0xf0, -1, 0, 1, "libSceNetCtlApDialog", g_libSceNetCtlApDialog_modules, 1}, + {0x80000047, -1, 0, 1, "libSceGnmResourceRegistration", + g_libSceGnmResourceRegistration_modules, 1}, + {0xf1, -1, 0, 1, "libScePlayGoDialog", g_libScePlayGoDialog_modules, 1}, + {0xf2, -1, 0, 1, "libSceSocialScreen", g_libSceSocialScreen_modules, 7}, + {0xf3, -1, 0, 1, "libSceEditMp4", g_libSceEditMp4_modules, 1}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0xf5, -1, 0, 1, "libScePsmKitSystem", g_libScePsmKitSystem_modules, 1}, + {0xf6, -1, 0, 1, "libSceTextToSpeech", g_libSceTextToSpeech_modules, 1}, + {0xf7, -1, 0, 2052, "libSceNpToolkit", g_libSceNpToolkit_modules, 1}, + {0xf8, -1, 0, 1, "libSceCustomMusicService", g_libSceCustomMusicService_modules, 2}, + {0xf9, -1, 0, 1, "libSceClSysCallWrapper", g_libSceClSysCallWrapper_modules, 11}, + {0x80000049, -1, 0, 1, "libSceScm", g_libSceScm_modules, 1}, + {0xfa, -1, 0, 1, "libSceSystemLogger", g_libSceSystemLogger_modules, 2}, + {0xfb, -1, 0, 1, "libSceBluetoothHid", g_libSceBluetoothHid_modules, 1}, + {0x80000050, -1, 0, 1, "libSceAvPlayerStreaming", g_libSceAvPlayerStreaming_modules, 1}, + {0x80000051, -1, 0, 2, "libSceAudiodecCpuAlac", g_libSceAudiodecCpuAlac_modules, 1}, + {0xfc, -1, 0, 1, "libSceVideoDecoderArbitration", g_libSceVideoDecoderArbitration_modules, 1}, + {0xfd, -1, 0, 1, "libSceVrServiceDialog", g_libSceVrServiceDialog_modules, 1}, + {0xfe, -1, 0, 4, "libSceJobManager", g_libSceJobManager_modules, 2}, + {0x80000053, -1, 0, 2, "libSceAudiodecCpuFlac", g_libSceAudiodecCpuFlac_modules, 1}, + {0x103, -1, 0, 1, "libSceSrcUtl", g_libSceSrcUtl_modules, 2}, + {0x80000055, -1, 0, 2, "libSceS3da", g_libSceS3da_modules, 1}, + {0x80000056, -1, 0, 2, "libSceDseehx", g_libSceDseehx_modules, 1}, + {0xff, -1, 0, 1, "libSceShareFactoryUtil", g_libSceShareFactoryUtil_modules, 1}, + {0x80000057, -1, 0, 1, "libSceDataTransfer", g_libSceDataTransfer_modules, 1}, + {0x100, -1, 0, 1, "libSceSocialScreenDialog", g_libSceSocialScreenDialog_modules, 1}, + {0x80000058, -1, 0, 1, "libSceAbstractStorage", g_libSceAbstractStorage_modules, 1}, + {0x80000059, -1, 0, 1, "libSceImageUtil", g_libSceImageUtil_modules, 1}, + {0x8000005a, -1, 0, 1, "libSceMetadataReaderWriter", g_libSceMetadataReaderWriter_modules, 1}, + {0x8000005b, -1, 0, 1, "libSceJpegParser", g_libSceJpegParser_modules, 1}, + {0x8000005c, -1, 0, 1, "libSceGvMp4Parser", g_libSceGvMp4Parser_modules, 1}, + {0x8000005d, -1, 0, 1, "libScePngParser", g_libScePngParser_modules, 1}, + {0x8000005e, -1, 0, 1, "libSceGifParser", g_libSceGifParser_modules, 1}, + {0x101, -1, 0, 1, "libSceNpSnsDialog", g_libSceNpSnsDialog_modules, 2}, + {0x8000005f, -1, 0, 1, "libSceAbstractLocal", g_libSceAbstractLocal_modules, 1}, + {0x80000060, -1, 0, 1, "libSceAbstractFacebook", g_libSceAbstractFacebook_modules, 1}, + {0x80000061, -1, 0, 1, "libSceAbstractYoutube", g_libSceAbstractYoutube_modules, 1}, + {0x80000062, -1, 0, 1, "libSceAbstractTwitter", g_libSceAbstractTwitter_modules, 1}, + {0x80000063, -1, 0, 1, "libSceAbstractDailymotion", g_libSceAbstractDailymotion_modules, 1}, + {0x102, -1, 0, 2052, "libSceNpToolkit2", g_libSceNpToolkit2_modules, 1}, + {0x80000064, -1, 0, 1, "libScePrecompiledShaders", g_libScePrecompiledShaders_modules, 1}, + {0x104, -1, 0, 1, "libSceDiscId", g_libSceDiscId_modules, 1}, + {0x80000065, -1, 0, 1, "libSceLibreSsl", g_libSceLibreSsl_modules, 2}, + {0x80000066, -1, 0, 2, "libSceFsInternalForVsh", g_libSceFsInternalForVsh_modules, 1}, + {0x105, -1, 0, 1, "libSceNpUniversalDataSystem", g_libSceNpUniversalDataSystem_modules, 1}, + {0x80000067, -1, 0, 1, "libSceDolbyVision", g_libSceDolbyVision_modules, 1}, + {0x80000068, -1, 0, 1, "libSceOpusSilkEnc", g_libSceOpusSilkEnc_modules, 2}, + {0x80000069, -1, 0, 1, "libSceOpusDec", g_libSceOpusDec_modules, 2}, + {0x8000006b, -1, 0, 1, "libSceWebKit2Secure", g_libSceWebKit2Secure_modules, 34}, + {0x8000006c, -1, 0, 1, "libSceJscCompiler", g_libSceJscCompiler_modules, 1}, + {0x8000006e, -1, 0, 1, "libSceJitBridge", g_libSceJitBridge_modules, 4}, + {0x0, -1, 0, 1, "libScePigletv2VSH", g_libScePigletv2VSH_modules, 4}, + {0x8000006f, -1, 0, 4096, "libSceJitBridge", g_libSceJitBridge_common_ex_modules, 4}, + {0x80000070, -1, 0, 4096, "libSceJscCompiler", g_libSceJscCompiler_common_ex_modules, 1}, + {0x80000071, -1, 0, 4096, "libSceOrbisCompat", g_libSceOrbisCompat_common_ex_modules, 24}, + {0x80000072, -1, 0, 4096, "libSceWeb", g_libSceWeb_common_ex_modules, 6}, + {0x80000073, -1, 0, 4096, "libSceWebKit2", g_libSceWebKit2_common_ex_modules, 30}, + {0x80000074, -1, 0, 4096, "libSceWebKit2Secure", g_libSceWebKit2Secure_common_ex_modules, 34}, + {0x0, -1, 0, 4096, "libScePrecompiledShaders", g_libScePrecompiledShaders_common_ex_modules, + 1}, + {0x107, -1, 0, 1, "libSceGic", g_libSceGic_modules, 1}, + {0x80000076, -1, 0, 1, "libSceRnpsAppMgr", g_libSceRnpsAppMgr_modules, 1}, + {0x80000077, -1, 0, 1, "libSceAsyncStorageInternal", g_libSceAsyncStorageInternal_modules, 1}, + {0x80000078, -1, 0, 1, "libSceHttpCache", g_libSceHttpCache_modules, 1}, + {0x108, -1, 0, 1, "libScePlayReady2", g_libScePlayReady2_modules, 1}, + {0x109, -1, 0, 9, "libSceHdrScopes", g_libSceHdrScopes_debug_modules, 1}, + {0x80000079, -1, 0, 1, "libSceNKWeb", g_libSceNKWeb_modules, 1}, + {0x8000007a, -1, 0, 1, "libSceNKWebKit", g_libSceNKWebKit_modules, 2}, + {0x0, -1, 0, 1, "libSceNKWebKitRequirements", g_libSceNKWebKitRequirements_modules, 1}, + {0x8000007c, -1, 0, 1, "libSceVnaInternal", g_libSceVnaInternal_modules, 1}, + {0x8000007d, -1, 0, 1, "libSceVnaWebsocket", g_libSceVnaWebsocket_modules, 1}, + {0x10c, -1, 0, 1, "libSceCesCs", g_libSceCesCs_modules, 1}, + {0x8000008a, -1, 0, 2, "libSceComposite", g_libSceComposite_modules, 1}, + {0x8000008b, -1, 0, 1, "libSceCompositeExt", g_libSceCompositeExt_modules, 1}, + {0x116, -1, 0, 1, "libSceHubAppUtil", g_libSceHubAppUtil_modules, 1}, + {0x80000098, -1, 0, 1, "libScePosixForWebKit", g_libScePosixForWebKit_modules, 1}, + {0x11a, -1, 0, 1, "libSceNpPartner001", g_libSceNpPartner001_modules, 1}, + {0x112, -1, 0, 1, "libSceNpSessionSignaling", g_libSceNpSessionSignaling_modules, 75}, + {0x10d, -1, 0, 1, "libScePlayerInvitationDialog", g_libScePlayerInvitationDialog_modules, 1}, + {0x115, -1, 0, 4, "libSceNpCppWebApi", g_libSceNpCppWebApi_modules, 42}, + {0x113, -1, 0, 1, "libSceNpEntitlementAccess", g_libSceNpEntitlementAccess_modules, 1}, + {0x8000009a, -1, 0, 2, "libSceNpRemotePlaySessionSignaling", + g_libSceNpRemotePlaySessionSignaling_modules, 76}, + {0x800000b8, -1, 0, 1, "libSceLibreSsl3", g_libSceLibreSsl3_modules, 2}, + {0x800000b1, -1, 0, 1, "libcurl", g_libcurl_modules, 2}, + {0x800000aa, -1, 0, 1, "libicu", g_libicu_modules, 2}, + {0x800000ac, -1, 0, 1, "libcairo", g_libcairo_modules, 9}, + {0x0, -1, 0, 1, "libfontconfig", g_libfontconfig_modules, 1}, + {0x0, -1, 0, 1, "libfreetype", g_libfreetype_modules, 1}, + {0x0, -1, 0, 1, "libharfbuzz", g_libharfbuzz_modules, 1}, + {0x800000ab, -1, 0, 1, "libpng16", g_libpng16_modules, 2}, + {0x12f, -1, 0, 1, "libSceFontGs", g_libSceFontGs_modules, 1}, + {0x800000c0, -1, 0, 1, "libSceGLSlimClientVSH", g_libSceGLSlimClientVSH_modules, 1}, + {0x800000c1, -1, 0, 1, "libSceGLSlimServerVSH", g_libSceGLSlimServerVSH_modules, 1}, + {0x135, -1, 0, 4, "libSceFontGsm", g_libSceFontGsm_modules, 1}, + {0x138, -1, 0, 1, "libSceNpPartnerSubscription", g_libSceNpPartnerSubscription_modules, 1}, + {0x139, -1, 0, 1, "libSceNpAuthAuthorizedAppDialog", g_libSceNpAuthAuthorizedAppDialog_modules, + 1}}); + +} // namespace Libraries::SysModule diff --git a/src/core/libraries/system/sysmodule.cpp b/src/core/libraries/system/sysmodule.cpp deleted file mode 100644 index 50d030065..000000000 --- a/src/core/libraries/system/sysmodule.cpp +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#define MAGIC_ENUM_RANGE_MIN 0 -#define MAGIC_ENUM_RANGE_MAX 300 -#include - -#include "common/logging/log.h" -#include "core/libraries/error_codes.h" -#include "core/libraries/kernel/process.h" -#include "core/libraries/libs.h" -#include "core/libraries/system/sysmodule.h" -#include "core/libraries/system/system_error.h" - -namespace Libraries::SysModule { - -int PS4_SYSV_ABI sceSysmoduleGetModuleHandleInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceSysmoduleGetModuleInfoForUnwind(VAddr addr, s32 flags, - Kernel::OrbisModuleInfoForUnwind* info) { - LOG_TRACE(Lib_SysModule, "sceSysmoduleGetModuleInfoForUnwind(addr=0x{:X}, flags=0x{:X})", addr, - flags); - - s32 res = Kernel::sceKernelGetModuleInfoForUnwind(addr, flags, info); - if (res != 0) { - return res; - } - - static constexpr std::array modules_to_hide = { - "libc.prx", - "libc.sprx", - "libSceAudioLatencyEstimation.prx", - "libSceFace.prx", - "libSceFaceTracker.prx", - "libSceFios2.prx", - "libSceFios2.sprx", - "libSceFontGsm.prx", - "libSceHand.prx", - "libSceHandTracker.prx", - "libSceHeadTracker.prx", - "libSceJobManager.prx", - "libSceNpCppWebApi.prx", - "libSceNpToolkit.prx", - "libSceNpToolkit2.prx", - "libSceS3DConversion.prx", - "libSceSmart.prx", - }; - - const std::string_view module_name = info->name.data(); - if (std::ranges::find(modules_to_hide, module_name) != modules_to_hide.end()) { - std::ranges::fill(info->name, '\0'); - } - return res; -} - -int PS4_SYSV_ABI sceSysmoduleIsCalledFromSysModule() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleIsCameraPreloaded() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleIsLoaded(OrbisSysModule id) { - LOG_ERROR(Lib_SysModule, "(DUMMY) called module = {}", magic_enum::enum_name(id)); - if (static_cast(id) == 0) { - LOG_ERROR(Lib_SysModule, "Invalid sysmodule ID: {:#x}", static_cast(id)); - return ORBIS_SYSMODULE_INVALID_ID; - } - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleIsLoadedInternal(OrbisSysModuleInternal id) { - LOG_ERROR(Lib_SysModule, "(DUMMY) called module = {:#x}", static_cast(id)); - if ((static_cast(id) & 0x7FFFFFFF) == 0) { - LOG_ERROR(Lib_SysModule, "Invalid internal sysmodule ID: {:#x}", static_cast(id)); - return ORBIS_SYSMODULE_INVALID_ID; - } - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleLoadModule(OrbisSysModule id) { - LOG_ERROR(Lib_SysModule, "(DUMMY) called module = {}", magic_enum::enum_name(id)); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleLoadModuleByNameInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleLoadModuleInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleLoadModuleInternalWithArg() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleMapLibcForLibkernel() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmodulePreloadModuleForLibkernel() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleUnloadModule() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleUnloadModuleByNameInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleUnloadModuleInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleUnloadModuleInternalWithArg() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -void RegisterLib(Core::Loader::SymbolsResolver* sym) { - LIB_FUNCTION("D8cuU4d72xM", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleGetModuleHandleInternal); - LIB_FUNCTION("4fU5yvOkVG4", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleGetModuleInfoForUnwind); - LIB_FUNCTION("ctfO7dQ7geg", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleIsCalledFromSysModule); - LIB_FUNCTION("no6T3EfiS3E", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleIsCameraPreloaded); - LIB_FUNCTION("fMP5NHUOaMk", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleIsLoaded); - LIB_FUNCTION("ynFKQ5bfGks", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleIsLoadedInternal); - LIB_FUNCTION("g8cM39EUZ6o", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleLoadModule); - LIB_FUNCTION("CU8m+Qs+HN4", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleLoadModuleByNameInternal); - LIB_FUNCTION("39iV5E1HoCk", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleLoadModuleInternal); - LIB_FUNCTION("hHrGoGoNf+s", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleLoadModuleInternalWithArg); - LIB_FUNCTION("lZ6RvVl0vo0", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleMapLibcForLibkernel); - LIB_FUNCTION("DOO+zuW1lrE", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmodulePreloadModuleForLibkernel); - LIB_FUNCTION("eR2bZFAAU0Q", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleUnloadModule); - LIB_FUNCTION("vpTHmA6Knvg", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleUnloadModuleByNameInternal); - LIB_FUNCTION("vXZhrtJxkGc", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleUnloadModuleInternal); - LIB_FUNCTION("aKa6YfBKZs4", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleUnloadModuleInternalWithArg); -}; - -} // namespace Libraries::SysModule diff --git a/src/core/libraries/system/sysmodule.h b/src/core/libraries/system/sysmodule.h deleted file mode 100644 index 3f1328e4b..000000000 --- a/src/core/libraries/system/sysmodule.h +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "common/types.h" -#include "core/libraries/kernel/process.h" - -namespace Core::Loader { -class SymbolsResolver; -} - -namespace Libraries::SysModule { - -enum class OrbisSysModule : u16 { - ORBIS_SYSMODULE_INVALID = 0x0000, - ORBIS_SYSMODULE_FIBER = 0x0006, // libSceFiber.sprx - ORBIS_SYSMODULE_ULT = 0x0007, // libSceUlt.sprx - ORBIS_SYSMODULE_NGS2 = 0x000B, // libSceNgs2.sprx - ORBIS_SYSMODULE_XML = 0x0017, // libSceXml.sprx - ORBIS_SYSMODULE_NP_UTILITY = 0x0019, // libSceNpUtility.sprx - ORBIS_SYSMODULE_VOICE = 0x001A, // libSceVoice.sprx - ORBIS_SYSMODULE_VOICEQOS = 0x001B, // libSceVoiceQos.sprx - ORBIS_SYSMODULE_NP_MATCHING2 = 0x001C, // libSceNpMatching2.sprx - ORBIS_SYSMODULE_NP_SCORE_RANKING = 0x001E, // libSceNpScoreRanking.sprx - ORBIS_SYSMODULE_RUDP = 0x0021, // libSceRudp.sprx - ORBIS_SYSMODULE_NP_TUS = 0x002C, // libSceNpTus.sprx - ORBIS_SYSMODULE_FACE = 0x0038, // libSceFace.sprx - ORBIS_SYSMODULE_SMART = 0x0039, // libSceSmart.sprx - ORBIS_SYSMODULE_JSON = 0x0080, // libSceJson.sprx - ORBIS_SYSMODULE_GAME_LIVE_STREAMING = 0x0081, // libSceGameLiveStreaming.sprx - ORBIS_SYSMODULE_COMPANION_UTIL = 0x0082, // libSceCompanionUtil.sprx - ORBIS_SYSMODULE_PLAYGO = 0x0083, // libScePlayGo.sprx - ORBIS_SYSMODULE_FONT = 0x0084, // libSceFont.sprx - ORBIS_SYSMODULE_VIDEO_RECORDING = 0x0085, // libSceVideoRecording.sprx - ORBIS_SYSMODULE_S3DCONVERSION = 0x0086, // libSceS3DConversion - ORBIS_SYSMODULE_AUDIODEC = 0x0088, // libSceAudiodec.sprx - ORBIS_SYSMODULE_JPEG_DEC = 0x008A, // libSceJpegDec.sprx - ORBIS_SYSMODULE_JPEG_ENC = 0x008B, // libSceJpegEnc.sprx - ORBIS_SYSMODULE_PNG_DEC = 0x008C, // libScePngDec.sprx - ORBIS_SYSMODULE_PNG_ENC = 0x008D, // libScePngEnc.sprx - ORBIS_SYSMODULE_VIDEODEC = 0x008E, // libSceVideodec.sprx - ORBIS_SYSMODULE_MOVE = 0x008F, // libSceMove.sprx - ORBIS_SYSMODULE_PAD_TRACKER = 0x0091, // libScePadTracker.sprx - ORBIS_SYSMODULE_DEPTH = 0x0092, // libSceDepth.sprx - ORBIS_SYSMODULE_HAND = 0x0093, // libSceHand.sprx - ORBIS_SYSMODULE_LIBIME = 0x0095, // libSceIme.sprx - ORBIS_SYSMODULE_IME_DIALOG = 0x0096, // libSceImeDialog.sprx - ORBIS_SYSMODULE_NP_PARTY = 0x0097, // libSceNpParty.sprx - ORBIS_SYSMODULE_FONT_FT = 0x0098, // libSceFontFt.sprx - ORBIS_SYSMODULE_FREETYPE_OT = 0x0099, // libSceFreeTypeOt.sprx - ORBIS_SYSMODULE_FREETYPE_OL = 0x009A, // libSceFreeTypeOl.sprx - ORBIS_SYSMODULE_FREETYPE_OPT_OL = 0x009B, // libSceFreeTypeOptOl.sprx - ORBIS_SYSMODULE_SCREEN_SHOT = 0x009C, // libSceScreenShot.sprx - ORBIS_SYSMODULE_NP_AUTH = 0x009D, // libSceNpAuth.sprx - ORBIS_SYSMODULE_SULPHA = 0x009F, - ORBIS_SYSMODULE_SAVE_DATA_DIALOG = 0x00A0, // libSceSaveDataDialog.sprx - ORBIS_SYSMODULE_INVITATION_DIALOG = 0x00A2, // libSceInvitationDialog.sprx - ORBIS_SYSMODULE_DEBUG_KEYBOARD = 0x00A3, - ORBIS_SYSMODULE_MESSAGE_DIALOG = 0x00A4, // libSceMsgDialog.sprx - ORBIS_SYSMODULE_AV_PLAYER = 0x00A5, // libSceAvPlayer.sprx - ORBIS_SYSMODULE_CONTENT_EXPORT = 0x00A6, // libSceContentExport.sprx - ORBIS_SYSMODULE_AUDIO_3D = 0x00A7, // libSceAudio3d.sprx - ORBIS_SYSMODULE_NP_COMMERCE = 0x00A8, // libSceNpCommerce.sprx - ORBIS_SYSMODULE_MOUSE = 0x00A9, // libSceMouse.sprx - ORBIS_SYSMODULE_COMPANION_HTTPD = 0x00AA, // libSceCompanionHttpd.sprx - ORBIS_SYSMODULE_WEB_BROWSER_DIALOG = 0x00AB, // libSceWebBrowserDialog.sprx - ORBIS_SYSMODULE_ERROR_DIALOG = 0x00AC, // libSceErrorDialog.sprx - ORBIS_SYSMODULE_NP_TROPHY = 0x00AD, // libSceNpTrophy.sprx - ORBIS_SYSMODULE_VIDEO_CORE_IF = 0x00AE, // libSceVideoCoreInterface.sprx - ORBIS_SYSMODULE_VIDEO_CORE_SERVER_IF = 0x00AF, // libSceVideoCoreServerInterface.sprx - ORBIS_SYSMODULE_NP_SNS_FACEBOOK = 0x00B0, // libSceNpSnsFacebookDialog.sprx - ORBIS_SYSMODULE_MOVE_TRACKER = 0x00B1, // libSceMoveTracker.sprx - ORBIS_SYSMODULE_NP_PROFILE_DIALOG = 0x00B2, // libSceNpProfileDialog.sprx - ORBIS_SYSMODULE_NP_FRIEND_LIST_DIALOG = 0x00B3, // libSceNpFriendListDialog.sprx - ORBIS_SYSMODULE_APP_CONTENT = 0x00B4, // libSceAppContent.sprx - ORBIS_SYSMODULE_NP_SIGNALING = 0x00B5, // libSceNpSignaling.sprx - ORBIS_SYSMODULE_REMOTE_PLAY = 0x00B6, // libSceRemoteplay.sprx - ORBIS_SYSMODULE_USBD = 0x00B7, // libSceUsbd.sprx - ORBIS_SYSMODULE_GAME_CUSTOM_DATA_DIALOG = 0x00B8, // libSceGameCustomDataDialog.sprx - ORBIS_SYSMODULE_NP_EULA_DIALOG = 0x00B9, // libSceNpEulaDialog.sprx - ORBIS_SYSMODULE_RANDOM = 0x00BA, // libSceRandom.sprx - ORBIS_SYSMODULE_RESERVED2 = 0x00BB, - ORBIS_SYSMODULE_M4AAC_ENC = 0x00BC, // libSceM4aacEnc.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU = 0x00BD, // libSceAudiodecCpu.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU_DDP = 0x00BE, // libSceAudiodecCpuDdp.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU_M4AAC = 0x00C0, // libSceAudiodecCpuM4aac.sprx - ORBIS_SYSMODULE_BEMP2_SYS = 0x00C1, // libSceBemp2sys.sprx - ORBIS_SYSMODULE_BEISOBMF = 0x00C2, // libSceBeisobmf.sprx - ORBIS_SYSMODULE_PLAY_READY = 0x00C3, // libScePlayReady.sprx - ORBIS_SYSMODULE_VIDEO_NATIVE_EXT_ESSENTIAL = 0x00C4, // libSceVideoNativeExtEssential.sprx - ORBIS_SYSMODULE_ZLIB = 0x00C5, // libSceZlib.sprx - ORBIS_SYSMODULE_DTCP_IP = 0x00C6, // libSceDtcpIp.sprx - ORBIS_SYSMODULE_CONTENT_SEARCH = 0x00C7, // libSceContentSearch.sprx - ORBIS_SYSMODULE_SHARE_UTILITY = 0x00C8, // libSceShareUtility.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU_DTS_HD_LBR = 0x00C9, // libSceAudiodecCpuDtsHdLbr.sprx - ORBIS_SYSMODULE_DECI4H = 0x00CA, - ORBIS_SYSMODULE_HEAD_TRACKER = 0x00CB, // libSceHeadTracker.sprx - ORBIS_SYSMODULE_GAME_UPDATE = 0x00CC, // libSceGameUpdate.sprx - ORBIS_SYSMODULE_AUTO_MOUNTER_CLIENT = 0x00CD, // libSceAutoMounterClient.sprx - ORBIS_SYSMODULE_SYSTEM_GESTURE = 0x00CE, // libSceSystemGesture.sprx - ORBIS_SYSMODULE_VIDEODEC2 = 0x00CF, // libSceVideodec2.sprx - ORBIS_SYSMODULE_VDECWRAP = 0x00D0, // libSceVdecwrap.sprx - ORBIS_SYSMODULE_AT9_ENC = 0x00D1, // libSceAt9Enc.sprx - ORBIS_SYSMODULE_CONVERT_KEYCODE = 0x00D2, // libSceConvertKeycode.sprx - ORBIS_SYSMODULE_SHARE_PLAY = 0x00D3, // libSceSharePlay.sprx - ORBIS_SYSMODULE_HMD = 0x00D4, // libSceHmd.sprx - ORBIS_SYSMODULE_USB_STORAGE = 0x00D5, // libSceUsbStorage.sprx - ORBIS_SYSMODULE_USB_STORAGE_DIALOG = 0x00D6, // libSceUsbStorageDialog.sprx - ORBIS_SYSMODULE_DISC_MAP = 0x00D7, // libSceDiscMap.sprx - ORBIS_SYSMODULE_FACE_TRACKER = 0x00D8, // libSceFaceTracker.sprx - ORBIS_SYSMODULE_HAND_TRACKER = 0x00D9, // libSceHandTracker.sprx - ORBIS_SYSMODULE_NP_SNS_YOUTUBE_DIALOG = 0x00DA, // libSceNpSnsYouTubeDialog.sprx - ORBIS_SYSMODULE_PROFILE_CACHE_EXTERNAL = 0x00DC, // libSceProfileCacheExternal.sprx - ORBIS_SYSMODULE_MUSIC_PLAYER_SERVICE = 0x00DD, // libSceMusicPlayerService.sprx - ORBIS_SYSMODULE_SP_SYS_CALL_WRAPPER = 0x00DE, // libSceSpSysCallWrapper.sprx - ORBIS_SYSMODULE_PS2_EMU_MENU_DIALOG = 0x00DF, // libScePs2EmuMenuDialog.sprx - ORBIS_SYSMODULE_NP_SNS_DAILYMOTION_DIALOG = 0x00E0, // libSceNpSnsDailyMotionDialog.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU_HEVAG = 0x00E1, // libSceAudiodecCpuHevag.sprx - ORBIS_SYSMODULE_LOGIN_DIALOG = 0x00E2, // libSceLoginDialog.sprx - ORBIS_SYSMODULE_LOGIN_SERVICE = 0x00E3, // libSceLoginService.sprx - ORBIS_SYSMODULE_SIGNIN_DIALOG = 0x00E4, // libSceSigninDialog.sprx - ORBIS_SYSMODULE_VDECSW = 0x00E5, // libSceVdecsw.sprx - ORBIS_SYSMODULE_CUSTOM_MUSIC_CORE = 0x00E6, // libSceCustomMusicCore.sprx - ORBIS_SYSMODULE_JSON2 = 0x00E7, // libSceJson2.sprx - ORBIS_SYSMODULE_AUDIO_LATENCY_ESTIMATION = 0x00E8, // libSceAudioLatencyEstimation.sprx - ORBIS_SYSMODULE_WK_FONT_CONFIG = 0x00E9, // libSceWkFontConfig.sprx - ORBIS_SYSMODULE_VORBIS_DEC = 0x00EA, // libSceVorbisDec.sprx - ORBIS_SYSMODULE_HMD_SETUP_DIALOG = 0x00EB, // libSceHmdSetupDialog.sprx - ORBIS_SYSMODULE_RESERVED28 = 0x00EC, - ORBIS_SYSMODULE_VR_TRACKER = 0x00ED, // libSceVrTracker.sprx - ORBIS_SYSMODULE_CONTENT_DELETE = 0x00EE, // libSceContentDelete.sprx - ORBIS_SYSMODULE_IME_BACKEND = 0x00EF, // libSceImeBackend.sprx - ORBIS_SYSMODULE_NET_CTL_AP_DIALOG = 0x00F0, // libSceNetCtlApDialog.sprx - ORBIS_SYSMODULE_PLAYGO_DIALOG = 0x00F1, // libScePlayGoDialog.sprx - ORBIS_SYSMODULE_SOCIAL_SCREEN = 0x00F2, // libSceSocialScreen.sprx - ORBIS_SYSMODULE_EDIT_MP4 = 0x00F3, // libSceEditMp4.sprx - ORBIS_SYSMODULE_PSM_KIT_SYSTEM = 0x00F5, // libScePsmKitSystem.sprx - ORBIS_SYSMODULE_TEXT_TO_SPEECH = 0x00F6, // libSceTextToSpeech.sprx - ORBIS_SYSMODULE_NP_TOOLKIT = 0x00F7, // libSceNpToolkit.sprx - ORBIS_SYSMODULE_CUSTOM_MUSIC_SERVICE = 0x00F8, // libSceCustomMusicService.sprx - ORBIS_SYSMODULE_CL_SYS_CALL_WRAPPER = 0x00F9, // libSceClSysCallWrapper.sprx - ORBIS_SYSMODULE_SYSTEM_LOGGER = 0x00FA, // libSceSystemLogger.sprx - ORBIS_SYSMODULE_BLUETOOTH_HID = 0x00FB, // libSceBluetoothHid.sprx - ORBIS_SYSMODULE_VIDEO_DECODER_ARBITRATION = 0x00FC, // libSceVideoDecoderArbitration.sprx - ORBIS_SYSMODULE_VR_SERVICE_DIALOG = 0x00FD, // libSceVrServiceDialog.sprx - ORBIS_SYSMODULE_JOB_MANAGER = 0x00FE, // libSceJobManager.sprx - ORBIS_SYSMODULE_SHARE_FACTORY_UTIL = 0x00FF, // libSceShareFactoryUtil.sprx - ORBIS_SYSMODULE_SOCIAL_SCREEN_DIALOG = 0x0100, // libSceSocialScreenDialog.sprx - ORBIS_SYSMODULE_NP_SNS_DIALOG = 0x0101, // libSceNpSnsDialog.sprx - ORBIS_SYSMODULE_NP_TOOLKIT2 = 0x0102, // libSceNpToolkit2.sprx - ORBIS_SYSMODULE_SRC_UTL = 0x0103, // libSceSrcUtl.sprx - ORBIS_SYSMODULE_DISC_ID = 0x0104, // libSceDiscId.sprx - ORBIS_SYSMODULE_NP_UNIVERSAL_DATA_SYSTEM = 0x0105, // libSceNpUniversalDataSystem.sprx - ORBIS_SYSMODULE_KEYBOARD = 0x0106, // libSceKeyboard.sprx - ORBIS_SYSMODULE_GIC = 0x0107, // libSceGic.sprx - ORBIS_SYSMODULE_PLAY_READY2 = 0x0108, // libScePlayReady2.sprx - ORBIS_SYSMODULE_CES_CS = 0x010c, // libSceCesCs.sprx - ORBIS_SYSMODULE_PLAYER_INVITATION_DIALOG = 0x010d, // libScePlayerInvitationDialog.sprx - ORBIS_SYSMODULE_NP_SESSION_SIGNALING = 0x0112, // libSceNpSessionSignaling.sprx - ORBIS_SYSMODULE_NP_ENTITLEMENT_ACCESS = 0x0113, // libSceNpEntitlementAccess.sprx - ORBIS_SYSMODULE_NP_CPP_WEB_API = 0x0115, // libSceNpCppWebApi.sprx - ORBIS_SYSMODULE_HUB_APP_UTIL = 0x0116, // libSceHubAppUtil.sprx - ORBIS_SYSMODULE_NP_PARTNER001 = 0x011a, // libSceNpPartner001.sprx - ORBIS_SYSMODULE_FONT_GS = 0x012f, // libSceFontGs.sprx - ORBIS_SYSMODULE_FONT_GSM = 0x0135, // libSceFontGsm.sprx - ORBIS_SYSMODULE_NP_PARTNER_SUBSCRIPTION = 0x0138, // libSceNpPartnerSubscription.sprx - ORBIS_SYSMODULE_NP_AUTH_AUTHORIZED_APP_DIALOG = 0x0139, // libSceNpAuthAuthorizedAppDialog.sprx -}; - -enum class OrbisSysModuleInternal : u32 { - ORBIS_SYSMODULE_INTERNAL_RAZOR_CPU = 0x80000019, // libSceRazorCpu.sprx -}; - -int PS4_SYSV_ABI sceSysmoduleGetModuleHandleInternal(); -s32 PS4_SYSV_ABI sceSysmoduleGetModuleInfoForUnwind(VAddr addr, s32 flags, - Kernel::OrbisModuleInfoForUnwind* info); -int PS4_SYSV_ABI sceSysmoduleIsCalledFromSysModule(); -int PS4_SYSV_ABI sceSysmoduleIsCameraPreloaded(); -int PS4_SYSV_ABI sceSysmoduleIsLoaded(OrbisSysModule id); -int PS4_SYSV_ABI sceSysmoduleIsLoadedInternal(OrbisSysModuleInternal id); -int PS4_SYSV_ABI sceSysmoduleLoadModule(OrbisSysModule id); -int PS4_SYSV_ABI sceSysmoduleLoadModuleByNameInternal(); -int PS4_SYSV_ABI sceSysmoduleLoadModuleInternal(); -int PS4_SYSV_ABI sceSysmoduleLoadModuleInternalWithArg(); -int PS4_SYSV_ABI sceSysmoduleMapLibcForLibkernel(); -int PS4_SYSV_ABI sceSysmodulePreloadModuleForLibkernel(); -int PS4_SYSV_ABI sceSysmoduleUnloadModule(); -int PS4_SYSV_ABI sceSysmoduleUnloadModuleByNameInternal(); -int PS4_SYSV_ABI sceSysmoduleUnloadModuleInternal(); -int PS4_SYSV_ABI sceSysmoduleUnloadModuleInternalWithArg(); - -void RegisterLib(Core::Loader::SymbolsResolver* sym); -} // namespace Libraries::SysModule diff --git a/src/core/libraries/system/system_error.h b/src/core/libraries/system/system_error.h deleted file mode 100644 index 615e4cd5f..000000000 --- a/src/core/libraries/system/system_error.h +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -constexpr int ORBIS_SYSMODULE_INVALID_ID = 0x805A1000; -constexpr int ORBIS_SYSMODULE_NOT_LOADED = 0x805A1001; -constexpr int ORBIS_SYSMODULE_LOCK_FAILED = 0x805A10FF; \ No newline at end of file diff --git a/src/core/linker.cpp b/src/core/linker.cpp index 0b80ecacc..d0d682043 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -16,6 +16,7 @@ #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/threads.h" +#include "core/libraries/sysmodule/sysmodule.h" #include "core/linker.h" #include "core/memory.h" #include "core/tls.h" @@ -110,7 +111,8 @@ void Linker::Execute(const std::vector& args) { ipc.WaitForStart(); } - LoadSharedLibraries(); + // Have libSceSysmodule preload our libraries. + Libraries::SysModule::sceSysmodulePreloadModuleForLibkernel(); // Simulate libSceGnmDriver initialization, which maps a chunk of direct memory. // Some games fail without accurately emulating this behavior. @@ -350,8 +352,8 @@ bool Linker::Resolve(const std::string& name, Loader::SymbolType sym_type, Modul return_info->virtual_address = AeroLib::GetStub(sr.name.c_str()); return_info->name = "Unknown !!!"; } - LOG_ERROR(Core_Linker, "Linker: Stub resolved {} as {} (lib: {}, mod: {})", sr.name, - return_info->name, library->name, module->name); + LOG_WARNING(Core_Linker, "Linker: Stub resolved {} as {} (lib: {}, mod: {})", sr.name, + return_info->name, library->name, module->name); return false; } diff --git a/src/emulator.cpp b/src/emulator.cpp index 18d3024dc..cc862f8ab 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -32,17 +32,9 @@ #include "core/file_format/psf.h" #include "core/file_format/trp.h" #include "core/file_sys/fs.h" -#include "core/libraries/disc_map/disc_map.h" -#include "core/libraries/font/font.h" -#include "core/libraries/font/fontft.h" -#include "core/libraries/jpeg/jpegenc.h" #include "core/libraries/kernel/kernel.h" -#include "core/libraries/libc_internal/libc_internal.h" -#include "core/libraries/libpng/pngenc.h" #include "core/libraries/libs.h" -#include "core/libraries/ngs2/ngs2.h" #include "core/libraries/np/np_trophy.h" -#include "core/libraries/rtc/rtc.h" #include "core/libraries/save_data/save_backup.h" #include "core/linker.h" #include "core/memory.h" @@ -405,17 +397,6 @@ void Emulator::Run(std::filesystem::path file, std::vector args, std::quick_exit(0); } - // check if we have system modules to load - LoadSystemModules(game_info.game_serial); - - // Load all prx from game's sce_module folder - mnt->IterateDirectory("/app0/sce_module", [this](const auto& path, const auto is_file) { - if (is_file) { - LOG_INFO(Loader, "Loading {}", fmt::UTF(path.u8string())); - linker->LoadModule(path); - } - }); - #ifdef ENABLE_DISCORD_RPC // Discord RPC if (Config::getEnableDiscordRPC()) { @@ -556,54 +537,6 @@ void Emulator::Restart(std::filesystem::path eboot_path, std::quick_exit(0); } -void Emulator::LoadSystemModules(const std::string& game_serial) { - constexpr auto ModulesToLoad = std::to_array( - {{"libSceNgs2.sprx", &Libraries::Ngs2::RegisterLib}, - {"libSceUlt.sprx", nullptr}, - {"libSceRtc.sprx", &Libraries::Rtc::RegisterLib}, - {"libSceJpegDec.sprx", nullptr}, - {"libSceJpegEnc.sprx", &Libraries::JpegEnc::RegisterLib}, - {"libScePngEnc.sprx", &Libraries::PngEnc::RegisterLib}, - {"libSceJson.sprx", nullptr}, - {"libSceJson2.sprx", nullptr}, - {"libSceLibcInternal.sprx", &Libraries::LibcInternal::RegisterLib}, - {"libSceCesCs.sprx", nullptr}, - {"libSceAudiodec.sprx", nullptr}, - {"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont}, - {"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt}, - {"libSceFreeTypeOt.sprx", nullptr}}); - - std::vector found_modules; - const auto& sys_module_path = Config::getSysModulesPath(); - for (const auto& entry : std::filesystem::directory_iterator(sys_module_path)) { - found_modules.push_back(entry.path()); - } - for (const auto& [module_name, init_func] : ModulesToLoad) { - const auto it = std::ranges::find_if( - found_modules, [&](const auto& path) { return path.filename() == module_name; }); - if (it != found_modules.end()) { - LOG_INFO(Loader, "Loading {}", it->string()); - if (linker->LoadModule(*it) != -1) { - continue; - } - } - if (init_func) { - LOG_INFO(Loader, "Can't Load {} switching to HLE", module_name); - init_func(&linker->GetHLESymbols()); - } else { - LOG_INFO(Loader, "No HLE available for {} module", module_name); - } - } - if (!game_serial.empty() && std::filesystem::exists(sys_module_path / game_serial)) { - for (const auto& entry : - std::filesystem::directory_iterator(sys_module_path / game_serial)) { - LOG_INFO(Loader, "Loading {} from game serial file {}", entry.path().string(), - game_serial); - linker->LoadModule(entry.path()); - } - } -} - void Emulator::UpdatePlayTime(const std::string& serial) { const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); const auto filePath = (user_dir / "play_time.txt").string(); From 0579569f139bd1da3d356d529a41b044e0f1eb77 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:20:14 +0100 Subject: [PATCH 02/44] Improve signal emulation (#4108) * improve signal emulation * make the sce function use the new posix ones * ifdefing away the issues * fix me being very tired yesterday night * let macOS handle SIGRT signals with the native sigaction call instead of an early error return * windows still has no clue what the fuck is going on * the loathsome clang-formatter * fix oact * return the guest handler, not the host one * Clear any existing signal mask for game threads. * don't rely on implementation specific things * Fix Windows support and sceKernelRaiseException bug * Review suggestions @kalaposfos13 suggested I push these. --------- Co-authored-by: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> --- src/core/cpu_patches.cpp | 13 +- .../libraries/kernel/threads/exception.cpp | 236 +++++++++++++----- src/core/libraries/kernel/threads/exception.h | 82 +++++- src/core/libraries/kernel/threads/pthread.cpp | 1 + src/core/linker.cpp | 9 + src/core/signals.cpp | 6 +- src/core/signals.h | 4 +- 7 files changed, 277 insertions(+), 74 deletions(-) diff --git a/src/core/cpu_patches.cpp b/src/core/cpu_patches.cpp index e303417c3..f6a4f7620 100644 --- a/src/core/cpu_patches.cpp +++ b/src/core/cpu_patches.cpp @@ -788,14 +788,11 @@ static bool PatchesIllegalInstructionHandler(void* context) { ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; const auto status = Common::Decoder::Instance()->decodeInstruction(instruction, operands, code_address); - if (ZYAN_SUCCESS(status) && instruction.mnemonic == ZydisMnemonic::ZYDIS_MNEMONIC_UD2) - [[unlikely]] { - UNREACHABLE_MSG("ud2 at code address {:#x}", reinterpret_cast(code_address)); - } - UNREACHABLE_MSG("Failed to patch address {:x} -- mnemonic: {}", - reinterpret_cast(code_address), - ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic) - : "Failed to decode"); + LOG_ERROR(Core, "Failed to patch address {:x} -- mnemonic: {}", + reinterpret_cast(code_address), + ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic) + : "Failed to decode"); + return false; } } diff --git a/src/core/libraries/kernel/threads/exception.cpp b/src/core/libraries/kernel/threads/exception.cpp index 247c387fe..e2fd032f5 100644 --- a/src/core/libraries/kernel/threads/exception.cpp +++ b/src/core/libraries/kernel/threads/exception.cpp @@ -2,7 +2,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" +#include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/kernel/posix_error.h" #include "core/libraries/kernel/threads/exception.h" #include "core/libraries/kernel/threads/pthread.h" #include "core/libraries/libs.h" @@ -13,23 +15,24 @@ #else #include #endif +#include namespace Libraries::Kernel { #ifdef _WIN32 // Windows doesn't have native versions of these, and we don't need to use them either. -static s32 NativeToOrbisSignal(s32 s) { +s32 NativeToOrbisSignal(s32 s) { return s; } -static s32 OrbisToNativeSignal(s32 s) { +s32 OrbisToNativeSignal(s32 s) { return s; } #else -static s32 NativeToOrbisSignal(s32 s) { +s32 NativeToOrbisSignal(s32 s) { switch (s) { case SIGHUP: return POSIX_SIGHUP; @@ -89,12 +92,21 @@ static s32 NativeToOrbisSignal(s32 s) { return POSIX_SIGUSR1; case SIGUSR2: return POSIX_SIGUSR2; + case _SIGEMT: + return POSIX_SIGEMT; + case _SIGINFO: + return POSIX_SIGINFO; + case 0: + return 128; default: + if (s > 0 && s < 128) { + return s; + } UNREACHABLE_MSG("Unknown signal {}", s); } } -static s32 OrbisToNativeSignal(s32 s) { +s32 OrbisToNativeSignal(s32 s) { switch (s) { case POSIX_SIGHUP: return SIGHUP; @@ -108,6 +120,8 @@ static s32 OrbisToNativeSignal(s32 s) { return SIGTRAP; case POSIX_SIGABRT: return SIGABRT; + case POSIX_SIGEMT: + return _SIGEMT; case POSIX_SIGFPE: return SIGFPE; case POSIX_SIGKILL: @@ -150,22 +164,33 @@ static s32 OrbisToNativeSignal(s32 s) { return SIGPROF; case POSIX_SIGWINCH: return SIGWINCH; + case POSIX_SIGINFO: + return _SIGINFO; case POSIX_SIGUSR1: return SIGUSR1; case POSIX_SIGUSR2: return SIGUSR2; + case 128: + return 0; default: + if (s > 0 && s < 128) { + return s; + } UNREACHABLE_MSG("Unknown signal {}", s); } } #endif -std::array Handlers{}; +#ifdef __APPLE__ +#define sigisemptyset(x) (*(x) == 0) +#endif + +std::array Handlers{}; #ifndef _WIN64 void SigactionHandler(int native_signum, siginfo_t* inf, ucontext_t* raw_context) { - const auto handler = Handlers[native_signum]; + const auto handler = Handlers[NativeToOrbisSignal(native_signum)]; if (handler) { auto ctx = Ucontext{}; #ifdef __APPLE__ @@ -214,6 +239,8 @@ void SigactionHandler(int native_signum, siginfo_t* inf, ucontext_t* raw_context ctx.uc_mcontext.mc_addr = reinterpret_cast(inf->si_addr); #endif handler(NativeToOrbisSignal(native_signum), &ctx); + } else { + UNREACHABLE_MSG("Unhandled exception"); } } #else @@ -221,7 +248,7 @@ void ExceptionHandler(void* arg1, void* arg2, void* arg3, PCONTEXT context) { const char* thrName = (char*)arg1; int native_signum = reinterpret_cast(arg2); LOG_INFO(Lib_Kernel, "Exception raised successfully on thread '{}'", thrName); - const auto handler = Handlers[native_signum]; + const auto handler = Handlers[NativeToOrbisSignal(native_signum)]; if (handler) { auto ctx = Ucontext{}; ctx.uc_mcontext.mc_r8 = context->R8; @@ -243,76 +270,105 @@ void ExceptionHandler(void* arg1, void* arg2, void* arg3, PCONTEXT context) { ctx.uc_mcontext.mc_fs = context->SegFs; ctx.uc_mcontext.mc_gs = context->SegGs; handler(NativeToOrbisSignal(native_signum), &ctx); + } else { + UNREACHABLE_MSG("Unhandled exception"); } } #endif -int PS4_SYSV_ABI sceKernelInstallExceptionHandler(s32 signum, SceKernelExceptionHandler handler) { - if (signum > POSIX_SIGUSR2) { - return ORBIS_KERNEL_ERROR_EINVAL; +s32 PS4_SYSV_ABI posix_sigemptyset(Sigset* s) { + s->bits[0] = 0; + s->bits[1] = 0; + return 0; +} + +bool PS4_SYSV_ABI posix_sigisemptyset(Sigset* s) { + return s->bits[0] == 0 && s->bits[1] == 0; +} + +s32 PS4_SYSV_ABI posix_sigaction(s32 sig, Sigaction* act, Sigaction* oact) { + if (sig < 1 || sig > 128 || sig == POSIX_SIGTHR || sig == POSIX_SIGKILL || + sig == POSIX_SIGSTOP) { + *__Error() = POSIX_EINVAL; + return ORBIS_FAIL; + } +#ifdef _WIN32 + LOG_ERROR(Lib_Kernel, "(STUBBED) called, sig: {}", sig); + Handlers[sig] = reinterpret_cast( + act ? act->__sigaction_handler.sigaction : nullptr); +#else + s32 native_sig = OrbisToNativeSignal(sig); + if (native_sig == SIGVTALRM) { + LOG_ERROR(Lib_Kernel, "Guest is attempting to use the HLE-reserved signal {}!", sig); + *__Error() = POSIX_EINVAL; + return ORBIS_FAIL; + } +#ifndef __APPLE__ + if (native_sig >= __SIGRTMIN && native_sig < SIGRTMIN) { + LOG_ERROR(Lib_Kernel, "Guest is attempting to use the HLE libc-reserved signal {}!", sig); + *__Error() = POSIX_EINVAL; + return ORBIS_FAIL; + } +#else + if (native_sig > SIGUSR2) { + LOG_ERROR(Lib_Kernel, + "Guest is attempting to use SIGRT signals, which aren't available on this " + "platform (signal: {})!", + sig); } - LOG_INFO(Lib_Kernel, "Installing signal handler for {}", signum); - int const native_signum = OrbisToNativeSignal(signum); -#ifdef __APPLE__ - ASSERT_MSG(native_signum != SIGVTALRM, "SIGVTALRM is HLE-reserved on macOS!"); #endif - ASSERT_MSG(!Handlers[native_signum], "Invalid parameters"); - Handlers[native_signum] = handler; -#ifndef _WIN64 - if (native_signum == SIGSEGV || native_signum == SIGBUS || native_signum == SIGILL) { + LOG_INFO(Lib_Kernel, "called, sig: {}, native sig: {}", sig, native_sig); + struct sigaction native_act{}; + if (act) { + native_act.sa_flags = act->sa_flags; // todo check compatibility, on Linux it seems fine + native_act.sa_sigaction = + reinterpret_cast(SigactionHandler); + if (!posix_sigisemptyset(&act->sa_mask)) { + LOG_ERROR(Lib_Kernel, "Unhandled sa_mask: {:x}", act->sa_mask.bits[0]); + } + } + auto const prev_handler = Handlers[sig]; + Handlers[sig] = reinterpret_cast( + act ? act->__sigaction_handler.sigaction : nullptr); + + if (native_sig == SIGSEGV || native_sig == SIGBUS || native_sig == SIGILL) { return ORBIS_OK; // These are handled in Core::SignalHandler } - struct sigaction act = {}; - act.sa_flags = SA_SIGINFO | SA_RESTART; - act.sa_sigaction = reinterpret_cast(SigactionHandler); - sigemptyset(&act.sa_mask); - sigaction(native_signum, &act, nullptr); -#endif - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceKernelRemoveExceptionHandler(s32 signum) { - if (signum > POSIX_SIGUSR2) { - return ORBIS_KERNEL_ERROR_EINVAL; + if (native_sig > 127) { + LOG_WARNING(Lib_Kernel, "We can't install a handler for native signal {}!", native_sig); + return ORBIS_OK; } - int const native_signum = OrbisToNativeSignal(signum); - if (!Handlers[native_signum]) { - LOG_WARNING(Lib_Kernel, "removing non-installed handler for signum {}", signum); - return ORBIS_KERNEL_ERROR_EINVAL; + struct sigaction native_oact{}; + s32 ret = sigaction(native_sig, act ? &native_act : nullptr, oact ? &native_oact : nullptr); + if (oact) { + oact->sa_flags = native_oact.sa_flags; + oact->__sigaction_handler.sigaction = + reinterpret_cast__sigaction_handler.sigaction)>(prev_handler); + if (!sigisemptyset(&native_oact.sa_mask)) { + LOG_ERROR(Lib_Kernel, "Unhandled sa_mask"); + } } - Handlers[native_signum] = nullptr; -#ifndef _WIN64 - if (native_signum == SIGSEGV || native_signum == SIGBUS || native_signum == SIGILL) { - struct sigaction action{}; - action.sa_sigaction = Core::SignalHandler; - action.sa_flags = SA_SIGINFO | SA_ONSTACK; - sigemptyset(&action.sa_mask); - - ASSERT_MSG(sigaction(native_signum, &action, nullptr) == 0, - "Failed to reinstate original signal handler for signal {}", native_signum); - } else { - struct sigaction act = {}; - act.sa_flags = SA_SIGINFO | SA_RESTART; - act.sa_sigaction = nullptr; - sigemptyset(&act.sa_mask); - sigaction(native_signum, &act, nullptr); + if (ret < 0) { + LOG_ERROR(Lib_Kernel, "sigaction failed: {}", strerror(errno)); + *__Error() = ErrnoToSceKernelError(errno); + return ORBIS_FAIL; } #endif return ORBIS_OK; } -int PS4_SYSV_ABI sceKernelRaiseException(PthreadT thread, int signum) { - if (signum != POSIX_SIGUSR1) { - return ORBIS_KERNEL_ERROR_EINVAL; +s32 PS4_SYSV_ABI posix_pthread_kill(PthreadT thread, s32 sig) { + if (sig < 1 || sig > 128) { // off-by-one error? + return POSIX_EINVAL; } - LOG_WARNING(Lib_Kernel, "Raising exception on thread '{}'", thread->name); - int const native_signum = OrbisToNativeSignal(signum); + LOG_WARNING(Lib_Kernel, "Raising signal {} on thread '{}'", sig, thread->name); + int const native_signum = OrbisToNativeSignal(sig); #ifndef _WIN64 const auto pthr = reinterpret_cast(thread->native_thr.GetHandle()); const auto ret = pthread_kill(pthr, native_signum); if (ret != 0) { LOG_ERROR(Kernel, "Failed to send exception signal to thread '{}': {}", thread->name, - strerror(ret)); + strerror(errno)); } #else USER_APC_OPTION option; @@ -326,6 +382,67 @@ int PS4_SYSV_ABI sceKernelRaiseException(PthreadT thread, int signum) { return ORBIS_OK; } +// libkernel has a check in sceKernelInstallExceptionHandler and sceKernelRemoveExceptionHandler for +// validating if the application requested a handler for an allowed signal or not. However, that is +// just a wrapper for sigaction, which itself does not have any such restrictions, and therefore +// this check is ridiculously trivial to go around. This, however, means that we need to support all +// 127 - 3 possible signals, even if realistically, only homebrew will use most of them. +static std::unordered_set orbis_allowed_signals{ + POSIX_SIGHUP, POSIX_SIGILL, POSIX_SIGFPE, POSIX_SIGBUS, POSIX_SIGSEGV, POSIX_SIGUSR1, +}; + +int PS4_SYSV_ABI sceKernelInstallExceptionHandler(s32 signum, OrbisKernelExceptionHandler handler) { + if (!orbis_allowed_signals.contains(signum)) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + if (Handlers[signum] != nullptr) { + return ORBIS_KERNEL_ERROR_EAGAIN; + } + LOG_INFO(Lib_Kernel, "Installing signal handler for {}", signum); + Sigaction act = {}; + act.sa_flags = POSIX_SA_SIGINFO | POSIX_SA_RESTART; + act.__sigaction_handler.sigaction = + reinterpret_cast(handler); + posix_sigemptyset(&act.sa_mask); + s32 ret = posix_sigaction(signum, &act, nullptr); + if (ret < 0) { + LOG_ERROR(Lib_Kernel, "Failed to add handler for signal {}: {}", signum, + strerror(*__Error())); + return ErrnoToSceKernelError(*__Error()); + } + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceKernelRemoveExceptionHandler(s32 signum) { + if (!orbis_allowed_signals.contains(signum)) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + int const native_signum = OrbisToNativeSignal(signum); + Handlers[signum] = nullptr; + Sigaction act = {}; + act.sa_flags = POSIX_SA_SIGINFO; + act.__sigaction_handler.sigaction = nullptr; + posix_sigemptyset(&act.sa_mask); + s32 ret = posix_sigaction(signum, &act, nullptr); + if (ret < 0) { + LOG_ERROR(Lib_Kernel, "Failed to remove handler for signal {}: {}", signum, + strerror(*__Error())); + return ErrnoToSceKernelError(*__Error()); + } + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceKernelRaiseException(PthreadT thread, int signum) { + if (signum != POSIX_SIGUSR1) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + s32 ret = posix_pthread_kill(thread, signum); + if (ret < 0) { + return ErrnoToSceKernelError(ret); + } + return ret; +} + s32 PS4_SYSV_ABI sceKernelDebugRaiseException(s32 error, s64 unk) { if (unk != 0) { return ORBIS_KERNEL_ERROR_EINVAL; @@ -352,6 +469,13 @@ void RegisterException(Core::Loader::SymbolsResolver* sym) { sceKernelDebugRaiseExceptionOnReleaseMode); LIB_FUNCTION("WkwEd3N7w0Y", "libkernel", 1, "libkernel", sceKernelInstallExceptionHandler); LIB_FUNCTION("Qhv5ARAoOEc", "libkernel", 1, "libkernel", sceKernelRemoveExceptionHandler); + + LIB_FUNCTION("KiJEPEWRyUY", "libkernel", 1, "libkernel", posix_sigaction); + LIB_FUNCTION("+F7C-hdk7+E", "libkernel", 1, "libkernel", posix_sigemptyset); + LIB_FUNCTION("yH-uQW3LbX0", "libkernel", 1, "libkernel", posix_pthread_kill); + LIB_FUNCTION("KiJEPEWRyUY", "libScePosix", 1, "libkernel", posix_sigaction); + LIB_FUNCTION("+F7C-hdk7+E", "libScePosix", 1, "libkernel", posix_sigemptyset); + LIB_FUNCTION("yH-uQW3LbX0", "libScePosix", 1, "libkernel", posix_pthread_kill); } } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/threads/exception.h b/src/core/libraries/kernel/threads/exception.h index 42c92ab2e..c07242c1d 100644 --- a/src/core/libraries/kernel/threads/exception.h +++ b/src/core/libraries/kernel/threads/exception.h @@ -11,7 +11,7 @@ class SymbolsResolver; namespace Libraries::Kernel { -using SceKernelExceptionHandler = PS4_SYSV_ABI void (*)(int, void*); +using OrbisKernelExceptionHandler = PS4_SYSV_ABI void (*)(int, void*); constexpr s32 POSIX_SIGHUP = 1; constexpr s32 POSIX_SIGINT = 2; @@ -47,6 +47,23 @@ constexpr s32 POSIX_SIGUSR2 = 31; constexpr s32 POSIX_SIGTHR = 32; constexpr s32 POSIX_SIGLIBRT = 33; +#ifdef __linux__ +constexpr s32 _SIGEMT = 128; +constexpr s32 _SIGINFO = 129; +#elif !defined(_WIN32) +constexpr s32 _SIGEMT = SIGEMT; +constexpr s32 _SIGINFO = SIGINFO; +#endif + +constexpr s32 POSIX_SA_NOCLDSTOP = 1; +constexpr s32 POSIX_SA_NOCLDWAIT = 2; +constexpr s32 POSIX_SA_SIGINFO = 4; +constexpr s32 POSIX_SA_ONSTACK = 0x08000000; +constexpr s32 POSIX_SA_RESTART = 0x10000000; +constexpr s32 POSIX_SA_NODEFER = 0x40000000; +constexpr s32 POSIX_SA_RESETHAND = 0x80000000; +constexpr s32 POSIX_SA_RESTORER = 0x04000000; + struct Mcontext { u64 mc_onstack; u64 mc_rdi; @@ -101,17 +118,74 @@ struct Sigset { u64 bits[2]; }; +union Sigval { + /* Members as suggested by Annex C of POSIX 1003.1b. */ + int sival_int; + void* sival_ptr; + /* 6.0 compatibility */ + int sigval_int; + void* sigval_ptr; +}; + +struct Siginfo { + int _si_signo; /* signal number */ + int _si_errno; /* errno association */ + /* + * Cause of signal, one of the SI_ macros or signal-specific + * values, i.e. one of the FPE_... values for SIGFPE. This + * value is equivalent to the second argument to an old-style + * FreeBSD signal handler. + */ + int _si_code; /* signal code */ + s32 _si_pid; /* sending process */ + u32 _si_uid; /* sender's ruid */ + int _si_status; /* exit value */ + void* _si_addr; /* faulting instruction */ + union Sigval _si_value; /* signal value */ + union { + struct { + int _trapno; /* machine specific trap code */ + } _fault; + struct { + int _timerid; + int _overrun; + } _timer; + struct { + int _mqd; + } _mesgq; + struct { + long _band; /* band event for SIGPOLL */ + } _poll; /* was this ever used ? */ + struct { + long __spare1__; + int __spare2__[7]; + } __spare__; + } _reason; +}; + +struct Sigaction { + union { + void (*handler)(int); + void (*sigaction)(int, struct Siginfo*, void*); + } __sigaction_handler; + int sa_flags; + Sigset sa_mask; +}; + struct Ucontext { struct Sigset uc_sigmask; int field1_0x10[12]; - struct Mcontext uc_mcontext; - struct Ucontext* uc_link; - struct ExStack uc_stack; + Mcontext uc_mcontext; + Ucontext* uc_link; + ExStack uc_stack; int uc_flags; int __spare[4]; int field7_0x4f4[3]; }; +s32 NativeToOrbisSignal(s32 s); +s32 OrbisToNativeSignal(s32 s); + void RegisterException(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index da9e1600f..f97451154 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -665,6 +665,7 @@ void RegisterThread(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("Z4QosVuAsA0", "libkernel", 1, "libkernel", posix_pthread_once); LIB_FUNCTION("EotR8a3ASf4", "libkernel", 1, "libkernel", posix_pthread_self); LIB_FUNCTION("OxhIB8LB-PQ", "libkernel", 1, "libkernel", posix_pthread_create); + LIB_FUNCTION("Jmi+9w9u0E4", "libkernel", 1, "libkernel", posix_pthread_create_name_np); LIB_FUNCTION("lZzFeSxPl08", "libkernel", 1, "libkernel", posix_pthread_setcancelstate); LIB_FUNCTION("CBNtXOoef-E", "libkernel", 1, "libkernel", posix_sched_get_priority_max); LIB_FUNCTION("m0iS6jNsXds", "libkernel", 1, "libkernel", posix_sched_get_priority_min); diff --git a/src/core/linker.cpp b/src/core/linker.cpp index d0d682043..6640c7204 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -22,6 +22,10 @@ #include "core/tls.h" #include "ipc/ipc.h" +#ifndef _WIN32 +#include +#endif + namespace Core { static PS4_SYSV_ABI void ProgramExitFunc() { @@ -107,6 +111,11 @@ void Linker::Execute(const std::vector& args) { main_thread.Run([this, module, &args](std::stop_token) { Common::SetCurrentThreadName("Game:Main"); +#ifndef _WIN32 // Clear any existing signal mask for game threads. + sigset_t emptyset; + sigemptyset(&emptyset); + pthread_sigmask(SIG_SETMASK, &emptyset, nullptr); +#endif if (auto& ipc = IPC::Instance()) { ipc.WaitForStart(); } diff --git a/src/core/signals.cpp b/src/core/signals.cpp index 70b431d39..f9b45bab7 100644 --- a/src/core/signals.cpp +++ b/src/core/signals.cpp @@ -21,7 +21,7 @@ #ifndef _WIN32 namespace Libraries::Kernel { void SigactionHandler(int native_signum, siginfo_t* inf, ucontext_t* raw_context); -extern std::array Handlers; +extern std::array Handlers; } // namespace Libraries::Kernel #endif @@ -86,7 +86,7 @@ void SignalHandler(int sig, siginfo_t* info, void* raw_context) { if (!signals->DispatchAccessViolation(raw_context, info->si_addr)) { // If the guest has installed a custom signal handler, and the access violation didn't // come from HLE memory tracking, pass the signal on - if (Libraries::Kernel::Handlers[sig]) { + if (Libraries::Kernel::Handlers[Libraries::Kernel::NativeToOrbisSignal(sig)]) { Libraries::Kernel::SigactionHandler(sig, info, reinterpret_cast(raw_context)); return; @@ -99,7 +99,7 @@ void SignalHandler(int sig, siginfo_t* info, void* raw_context) { } case SIGILL: if (!signals->DispatchIllegalInstruction(raw_context)) { - if (Libraries::Kernel::Handlers[sig]) { + if (Libraries::Kernel::Handlers[Libraries::Kernel::NativeToOrbisSignal(sig)]) { Libraries::Kernel::SigactionHandler(sig, info, reinterpret_cast(raw_context)); return; diff --git a/src/core/signals.h b/src/core/signals.h index 90801debb..011383693 100644 --- a/src/core/signals.h +++ b/src/core/signals.h @@ -10,10 +10,8 @@ #ifdef _WIN32 #define SIGSLEEP -1 -#elif defined(__APPLE__) -#define SIGSLEEP SIGVTALRM #else -#define SIGSLEEP SIGRTMAX +#define SIGSLEEP SIGVTALRM #endif namespace Core { From e16ba06ab0aa10492d719c3a11f46aeaf73ff5c1 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Mon, 9 Mar 2026 17:33:58 +0200 Subject: [PATCH 03/44] shader_recompiler: Support 32 thread sharing mode (#4110) --- .../frontend/translate/data_share.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/shader_recompiler/frontend/translate/data_share.cpp b/src/shader_recompiler/frontend/translate/data_share.cpp index 634486fc4..b1aca83d3 100644 --- a/src/shader_recompiler/frontend/translate/data_share.cpp +++ b/src/shader_recompiler/frontend/translate/data_share.cpp @@ -277,12 +277,21 @@ void Translator::DS_SWIZZLE_B32(const GcnInst& inst) { const u8 offset0 = inst.control.ds.offset0; const u8 offset1 = inst.control.ds.offset1; const IR::U32 src{GetSrc(inst.src[0])}; - // ASSERT(offset1 & 0x80); const IR::U32 lane_id = ir.LaneId(); - const IR::U32 id_in_group = ir.BitwiseAnd(lane_id, ir.Imm32(0b11)); - const IR::U32 base = ir.ShiftLeftLogical(id_in_group, ir.Imm32(1)); - const IR::U32 index = ir.BitFieldExtract(ir.Imm32(offset0), base, ir.Imm32(2)); - SetDst(inst.dst[0], ir.QuadShuffle(src, index)); + if (offset1 & 0x80) { + const IR::U32 id_in_group = ir.BitwiseAnd(lane_id, ir.Imm32(0b11)); + const IR::U32 base = ir.ShiftLeftLogical(id_in_group, ir.Imm32(1)); + const IR::U32 index = ir.BitFieldExtract(ir.Imm32(offset0), base, ir.Imm32(2)); + SetDst(inst.dst[0], ir.QuadShuffle(src, index)); + } else { + const u8 and_mask = (offset0 & 0x1f) | (~u8{0} << 5); + const u8 or_mask = (offset0 >> 5) | ((offset1 & 0x3) << 3); + const u8 xor_mask = offset1 >> 2; + const IR::U32 index = ir.BitwiseXor( + ir.BitwiseOr(ir.BitwiseAnd(lane_id, ir.Imm32(and_mask)), ir.Imm32(or_mask)), + ir.Imm32(xor_mask)); + SetDst(inst.dst[0], ir.ReadLane(src, index)); + } } void Translator::DS_APPEND(const GcnInst& inst) { From df6bb8562e79971beb966789e4cbbb2b35fcc9d9 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Mon, 9 Mar 2026 17:46:51 +0200 Subject: [PATCH 04/44] renderer_vulkan: Force subgroup size to 64 when possible (#4111) --- src/video_core/renderer_vulkan/vk_compute_pipeline.cpp | 4 ++++ src/video_core/renderer_vulkan/vk_instance.cpp | 7 +++++-- src/video_core/renderer_vulkan/vk_instance.h | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp index 35eda86da..eecd416d1 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp @@ -21,7 +21,11 @@ ComputePipeline::ComputePipeline(const Instance& instance, Scheduler& scheduler, info = &info_; const auto debug_str = GetDebugString(); + const vk::PipelineShaderStageRequiredSubgroupSizeCreateInfo subgroup_size_ci = { + .requiredSubgroupSize = 64, + }; const vk::PipelineShaderStageCreateInfo shader_ci = { + .pNext = instance.IsSubgroupSize64Supported() ? &subgroup_size_ci : nullptr, .stage = vk::ShaderStageFlagBits::eCompute, .module = module, .pName = "main", diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 44aa79d98..6898df97a 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -220,9 +220,11 @@ bool Instance::CreateDevice() { const vk::StructureChain properties_chain = physical_device.getProperties2< vk::PhysicalDeviceProperties2, vk::PhysicalDeviceVulkan11Properties, - vk::PhysicalDeviceVulkan12Properties, vk::PhysicalDevicePushDescriptorPropertiesKHR>(); + vk::PhysicalDeviceVulkan12Properties, vk::PhysicalDeviceVulkan13Properties, + vk::PhysicalDevicePushDescriptorPropertiesKHR>(); vk11_props = properties_chain.get(); vk12_props = properties_chain.get(); + vk13_props = properties_chain.get(); push_descriptor_props = properties_chain.get(); LOG_INFO(Render_Vulkan, "Physical device subgroup size {}", vk11_props.subgroupSize); @@ -367,7 +369,7 @@ bool Instance::CreateDevice() { feature_chain.get(); const auto vk11_features = feature_chain.get(); vk12_features = feature_chain.get(); - const auto vk13_features = feature_chain.get(); + vk13_features = feature_chain.get(); vk::StructureChain device_chain = { vk::DeviceCreateInfo{ .queueCreateInfoCount = 1u, @@ -429,6 +431,7 @@ bool Instance::CreateDevice() { vk::PhysicalDeviceVulkan13Features{ .robustImageAccess = vk13_features.robustImageAccess, .shaderDemoteToHelperInvocation = vk13_features.shaderDemoteToHelperInvocation, + .subgroupSizeControl = vk13_features.subgroupSizeControl, .synchronization2 = vk13_features.synchronization2, .dynamicRendering = vk13_features.dynamicRendering, .maintenance4 = vk13_features.maintenance4, diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 8975669bb..7a8a906d5 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -233,6 +233,11 @@ public: return vk12_features.shaderSharedInt64Atomics; } + /// Returns true if the subgroup size can be set to match guest subgroup size + bool IsSubgroupSize64Supported() const { + return vk13_features.subgroupSizeControl && vk13_props.maxSubgroupSize >= 64; + } + /// Returns true when VK_KHR_workgroup_memory_explicit_layout is supported. bool IsWorkgroupMemoryExplicitLayoutSupported() const { return workgroup_memory_explicit_layout && @@ -455,9 +460,11 @@ private: vk::PhysicalDeviceMemoryProperties memory_properties; vk::PhysicalDeviceVulkan11Properties vk11_props; vk::PhysicalDeviceVulkan12Properties vk12_props; + vk::PhysicalDeviceVulkan13Properties vk13_props; vk::PhysicalDevicePushDescriptorPropertiesKHR push_descriptor_props; vk::PhysicalDeviceFeatures features; vk::PhysicalDeviceVulkan12Features vk12_features; + vk::PhysicalDeviceVulkan13Features vk13_features; vk::PhysicalDevicePortabilitySubsetFeaturesKHR portability_features; vk::PhysicalDeviceExtendedDynamicState3FeaturesEXT dynamic_state_3_features; vk::PhysicalDeviceRobustness2FeaturesEXT robustness2_features; From 85476e55ea70201c470eb618193864844bead976 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:02:22 -0500 Subject: [PATCH 05/44] Recompiler: Implement IMAGE_ATOMIC_CMPSWAP (#4109) * To implement ImageAtomicCmpSwap ...but it doesn't work, so here it shall stay. * a fix * Clang * Add to MayHaveSideEffects I missed this while digging through IR code. --- .../backend/spirv/emit_spirv_atomic.cpp | 15 +++++++++++++++ .../backend/spirv/emit_spirv_instructions.h | 2 ++ src/shader_recompiler/frontend/format.cpp | 4 ++-- .../frontend/translate/vector_memory.cpp | 6 ++++++ src/shader_recompiler/ir/ir_emitter.cpp | 5 +++++ src/shader_recompiler/ir/ir_emitter.h | 3 +++ src/shader_recompiler/ir/microinstruction.cpp | 1 + src/shader_recompiler/ir/opcodes.inc | 1 + .../ir/passes/resource_tracking_pass.cpp | 1 + 9 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp index 6dd1637dd..549e27ae0 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp @@ -140,6 +140,15 @@ Id ImageAtomicF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id va const auto [scope, semantics]{AtomicArgs(ctx)}; return (ctx.*atomic_func)(ctx.F32[1], pointer, scope, semantics, value); } + +Id ImageAtomicU32CmpSwap(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value, + Id cmp_value, + Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id, Id, Id)) { + const auto& texture = ctx.images[handle & 0xFFFF]; + const Id pointer{ctx.OpImageTexelPointer(ctx.image_u32, texture.id, coords, ctx.ConstU32(0U))}; + const auto [scope, semantics]{AtomicArgs(ctx)}; + return (ctx.*atomic_func)(ctx.F32[1], pointer, scope, semantics, semantics, value, cmp_value); +} } // Anonymous namespace Id EmitSharedAtomicIAdd32(EmitContext& ctx, Id offset, Id value) { @@ -420,6 +429,12 @@ Id EmitImageAtomicExchange32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id co return ImageAtomicU32(ctx, inst, handle, coords, value, &Sirit::Module::OpAtomicExchange); } +Id EmitImageAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value, + Id cmp_value) { + return ImageAtomicU32CmpSwap(ctx, inst, handle, coords, value, cmp_value, + &Sirit::Module::OpAtomicCompareExchange); +} + Id EmitDataAppend(EmitContext& ctx, u32 gds_addr, u32 binding) { const auto& buffer = ctx.buffers[binding]; const auto [id, pointer_type] = buffer.Alias(PointerType::U32); diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 80968eaf0..69fa36eaa 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -456,6 +456,8 @@ Id EmitImageAtomicAnd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id EmitImageAtomicOr32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value); Id EmitImageAtomicXor32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value); Id EmitImageAtomicExchange32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value); +Id EmitImageAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id cmp_value); Id EmitCubeFaceIndex(EmitContext& ctx, IR::Inst* inst, Id cube_coords); Id EmitLaneId(EmitContext& ctx); Id EmitWarpId(EmitContext& ctx); diff --git a/src/shader_recompiler/frontend/format.cpp b/src/shader_recompiler/frontend/format.cpp index d26873396..4a90fe358 100644 --- a/src/shader_recompiler/frontend/format.cpp +++ b/src/shader_recompiler/frontend/format.cpp @@ -3430,8 +3430,8 @@ constexpr std::array InstructionFormatMIMG = {{ {InstClass::VectorMemImgNoSmp, InstCategory::VectorMemory, 4, 1, ScalarType::Uint32, ScalarType::Uint32}, // 16 = IMAGE_ATOMIC_CMPSWAP - {InstClass::VectorMemImgNoSmp, InstCategory::VectorMemory, 4, 1, ScalarType::Undefined, - ScalarType::Undefined}, + {InstClass::VectorMemImgNoSmp, InstCategory::VectorMemory, 4, 1, ScalarType::Uint32, + ScalarType::Uint32}, // 17 = IMAGE_ATOMIC_ADD {InstClass::VectorMemImgNoSmp, InstCategory::VectorMemory, 4, 1, ScalarType::Uint32, ScalarType::Uint32}, diff --git a/src/shader_recompiler/frontend/translate/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index 72286b29c..0d9e8f220 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -137,6 +137,8 @@ void Translator::EmitVectorMemory(const GcnInst& inst) { // Image atomic operations case Opcode::IMAGE_ATOMIC_SWAP: return IMAGE_ATOMIC(AtomicOp::Swap, inst); + case Opcode::IMAGE_ATOMIC_CMPSWAP: + return IMAGE_ATOMIC(AtomicOp::CmpSwap, inst); case Opcode::IMAGE_ATOMIC_ADD: return IMAGE_ATOMIC(AtomicOp::Add, inst); case Opcode::IMAGE_ATOMIC_SMIN: @@ -520,6 +522,10 @@ void Translator::IMAGE_ATOMIC(AtomicOp op, const GcnInst& inst) { switch (op) { case AtomicOp::Swap: return ir.ImageAtomicExchange(handle, body, value, {}); + case AtomicOp::CmpSwap: { + const IR::Value cmp_val = ir.GetVectorReg(val_reg + 1); + return ir.ImageAtomicCmpSwap(handle, body, value, cmp_val, info); + } case AtomicOp::Add: return ir.ImageAtomicIAdd(handle, body, value, info); case AtomicOp::Smin: diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index 1e77dc677..c681c3120 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -2055,6 +2055,11 @@ Value IREmitter::ImageAtomicExchange(const Value& handle, const Value& coords, c return Inst(Opcode::ImageAtomicExchange32, Flags{info}, handle, coords, value); } +Value IREmitter::ImageAtomicCmpSwap(const Value& handle, const Value& coords, const Value& value, + const Value& cmp_value, TextureInstInfo info) { + return Inst(Opcode::ImageAtomicCmpSwap32, Flags{info}, handle, coords, value, cmp_value); +} + Value IREmitter::ImageSampleRaw(const Value& image_handle, const Value& sampler_handle, const Value& address1, const Value& address2, const Value& address3, const Value& address4, TextureInstInfo info) { diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index 6f20d5780..adc8f5fb1 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -360,6 +360,9 @@ public: TextureInstInfo info); [[nodiscard]] Value ImageAtomicExchange(const Value& handle, const Value& coords, const Value& value, TextureInstInfo info); + [[nodiscard]] Value ImageAtomicCmpSwap(const Value& handle, const Value& coords, + const Value& value, const Value& cmp_value, + TextureInstInfo info); [[nodiscard]] Value ImageSampleRaw(const Value& image_handle, const Value& sampler_handle, const Value& address1, const Value& address2, diff --git a/src/shader_recompiler/ir/microinstruction.cpp b/src/shader_recompiler/ir/microinstruction.cpp index 40ce69df8..cd0131770 100644 --- a/src/shader_recompiler/ir/microinstruction.cpp +++ b/src/shader_recompiler/ir/microinstruction.cpp @@ -123,6 +123,7 @@ bool Inst::MayHaveSideEffects() const noexcept { case Opcode::ImageAtomicOr32: case Opcode::ImageAtomicXor32: case Opcode::ImageAtomicExchange32: + case Opcode::ImageAtomicCmpSwap32: case Opcode::DebugPrint: case Opcode::EmitVertex: case Opcode::EmitPrimitive: diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index 4875375bc..6304a96fa 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -436,6 +436,7 @@ OPCODE(ImageAtomicAnd32, U32, Opaq OPCODE(ImageAtomicOr32, U32, Opaque, Opaque, U32, ) OPCODE(ImageAtomicXor32, U32, Opaque, Opaque, U32, ) OPCODE(ImageAtomicExchange32, U32, Opaque, Opaque, U32, ) +OPCODE(ImageAtomicCmpSwap32, U32, Opaque, Opaque, U32, U32, ) // Cube operations - optional, usable if profile.supports_native_cube_calc OPCODE(CubeFaceIndex, F32, F32x3, ) diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 93129ac0e..4c41e94e9 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -214,6 +214,7 @@ bool IsImageAtomicInstruction(const IR::Inst& inst) { case IR::Opcode::ImageAtomicOr32: case IR::Opcode::ImageAtomicXor32: case IR::Opcode::ImageAtomicExchange32: + case IR::Opcode::ImageAtomicCmpSwap32: return true; default: return false; From 3ee06a91df6975851660ae497bff7a79d61a9f96 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:25:45 -0500 Subject: [PATCH 06/44] Relocate imports on HLE loads (#4113) Now that dynamic HLE loads happen after the eboot loads, HLEs for most "preload" modules wouldn't detect if you didn't have libSceRtc dumped. This was because, while we stored all the new symbols from the HLE lib, we weren't relocating loaded modules to use these symbols. --- src/core/libraries/sysmodule/sysmodule_internal.cpp | 4 ++++ src/core/linker.h | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/libraries/sysmodule/sysmodule_internal.cpp b/src/core/libraries/sysmodule/sysmodule_internal.cpp index 55acded94..def410e25 100644 --- a/src/core/libraries/sysmodule/sysmodule_internal.cpp +++ b/src/core/libraries/sysmodule/sysmodule_internal.cpp @@ -252,6 +252,10 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { if (init_func) { LOG_INFO(Loader, "Can't Load {} switching to HLE", mod_name); init_func(&linker->GetHLESymbols()); + + // When loading HLEs, we need to relocate imports + // This ensures later module loads can see our HLE functions. + linker->RelocateAllImports(); } else { LOG_INFO(Loader, "No HLE available for {} module", mod_name); } diff --git a/src/core/linker.h b/src/core/linker.h index 8ffcd9d45..3cb59d9ee 100644 --- a/src/core/linker.h +++ b/src/core/linker.h @@ -125,11 +125,10 @@ public: } } - void LoadSharedLibraries() { + void RelocateAllImports() { + std::scoped_lock lk{mutex}; for (auto& module : m_modules) { - if (module->IsSharedLib()) { - module->Start(0, nullptr, nullptr); - } + Relocate(module.get()); } } From ac3786f533024f04c77b5d295a14bca05f737751 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:25:22 -0500 Subject: [PATCH 07/44] Fix return type for ImageAtomicU32CmpSwap (#4115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Somedays I wonder how I miss these details. But hey, least there are two other people who also missed this 😅 --- src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp index 549e27ae0..1055bf081 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp @@ -147,7 +147,7 @@ Id ImageAtomicU32CmpSwap(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords const auto& texture = ctx.images[handle & 0xFFFF]; const Id pointer{ctx.OpImageTexelPointer(ctx.image_u32, texture.id, coords, ctx.ConstU32(0U))}; const auto [scope, semantics]{AtomicArgs(ctx)}; - return (ctx.*atomic_func)(ctx.F32[1], pointer, scope, semantics, semantics, value, cmp_value); + return (ctx.*atomic_func)(ctx.U32[1], pointer, scope, semantics, semantics, value, cmp_value); } } // Anonymous namespace From c8b3d63e7ebb48ac16a287b78032708cf3c9e222 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:53:12 -0500 Subject: [PATCH 08/44] Core: Fix game arguments (#4118) * Fix game arguments. Tested with Crash Team Racing Nitro Fueled * Fix the fix This callback runs unconditionally, so only perform erase if we actually place anything in gameArgs --- src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.cpp b/src/main.cpp index d3799e2ec..3370901a8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -99,6 +99,7 @@ int main(int argc, char* argv[]) { const auto& extras = app.remaining(); if (!extras.empty()) { gameArgs = extras; + gameArgs.erase(gameArgs.begin()); } }); From 67ffd0334b8428f395563c89fbdaa5f6233ea1e7 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:15:04 +0100 Subject: [PATCH 09/44] Properly fix game flag handling (#4119) * fix the fix for the fix * fine there's no debug info then because of Ubuntu things --- src/main.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 3370901a8..d2804ee62 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -99,7 +99,6 @@ int main(int argc, char* argv[]) { const auto& extras = app.remaining(); if (!extras.empty()) { gameArgs = extras; - gameArgs.erase(gameArgs.begin()); } }); @@ -143,6 +142,14 @@ int main(int argc, char* argv[]) { return 1; } } + if (!gameArgs.empty()) { + if (gameArgs.front() == "--") { + gameArgs.erase(gameArgs.begin()); + } else { + std::cerr << "Error: unhandled flags\n"; + return 1; + } + } // ---- Apply flags ---- if (patchFile) From f336096b12cf853b8eb8137cb67646fee7cfde17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=BD=D1=8C=D0=BA=D0=B0=20=D0=A7=D0=B5=D1=82?= =?UTF-8?q?=D0=B2=D1=91=D1=80=D1=82=D1=8B=D0=B9?= Date: Fri, 13 Mar 2026 17:38:22 +0700 Subject: [PATCH 10/44] PSF file format: close file after encode() (#4122) --- src/core/file_format/psf.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/file_format/psf.cpp b/src/core/file_format/psf.cpp index e647059f0..c5be7410a 100644 --- a/src/core/file_format/psf.cpp +++ b/src/core/file_format/psf.cpp @@ -113,6 +113,7 @@ bool PSF::Encode(const std::filesystem::path& filepath) const { LOG_ERROR(Core, "Failed to write PSF file. Written {} Expected {}", written, psf_buffer.size()); } + file.Close(); return written == psf_buffer.size(); } From 844cfe51850e97bd3b3aee5ad99a390394b356f3 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 14 Mar 2026 10:12:26 -0500 Subject: [PATCH 11/44] Lib.Ssl2: Stub data for sceSslGetCaCerts (#4127) * Test * More robust logic for storing and freeing dummy data Anything heap allocated is invalidated when the function returns. Use malloc to allocate the string instead, and make sure to free those allocations in sceSslFreeCaCerts. --- src/core/libraries/network/ssl2.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/network/ssl2.cpp b/src/core/libraries/network/ssl2.cpp index 0b408d094..3a7fd71e5 100644 --- a/src/core/libraries/network/ssl2.cpp +++ b/src/core/libraries/network/ssl2.cpp @@ -114,7 +114,13 @@ int PS4_SYSV_ABI sceSslFreeCaCerts(s32 ssl_ctx_id, OrbisSslCaCerts* certs) { if (certs == nullptr) { return ORBIS_SSL_ERROR_INVALID_ARGUMENT; } - delete (certs->certs); + if (certs->certs != nullptr) { + for (s32 data = 0; data < certs->num; data++) { + free(certs->certs[data].ptr); + } + delete (certs->certs); + } + // delete (certs->pool); return ORBIS_OK; } @@ -139,7 +145,12 @@ int PS4_SYSV_ABI sceSslGetCaCerts(s32 ssl_ctx_id, OrbisSslCaCerts* certs) { if (certs == nullptr) { return ORBIS_SSL_ERROR_INVALID_ARGUMENT; } - certs->certs = new OrbisSslData{nullptr, 0}; + // Allocate a buffer to store dummy data in. + const char* dummy_data = "dummy"; + u64 dummy_length = strlen(dummy_data) + 1; + char* data = static_cast(malloc(dummy_length)); + strncpy(data, dummy_data, dummy_length); + certs->certs = new OrbisSslData{data, dummy_length}; certs->num = 1; certs->pool = nullptr; return ORBIS_OK; From 30ff9cf05045124d47843512568fdd7cfbbbff3d Mon Sep 17 00:00:00 2001 From: shinra-electric <50119606+shinra-electric@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:15:29 +0000 Subject: [PATCH 12/44] CI: Update actions/cache due to Node 20 deprecation (#4128) * Upload-artifact v4 --> v6 * Download-artifact v5 --> v8 * Checkout v5 --> v6 * cache v4 --> v5 --- .github/workflows/build.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ffe7c22fb..b54698e3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,14 +26,14 @@ jobs: runs-on: ubuntu-24.04 continue-on-error: true steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: fsfe/reuse-action@v5 clang-format: runs-on: ubuntu-24.04 continue-on-error: true steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install @@ -54,7 +54,7 @@ jobs: shorthash: ${{ steps.vars.outputs.shorthash }} fullhash: ${{ steps.vars.outputs.fullhash }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Get date and git hash id: vars run: | @@ -69,12 +69,12 @@ jobs: runs-on: windows-2025 needs: get-info steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: recursive - name: Cache CMake Configuration - uses: actions/cache@v4 + uses: actions/cache@v5 env: cache-name: ${{ runner.os }}-sdl-ninja-cache-cmake-configuration with: @@ -99,7 +99,7 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $env:NUMBER_OF_PROCESSORS - name: Upload Windows SDL artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: shadps4-win64-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: ${{github.workspace}}/build/shadPS4.exe @@ -108,7 +108,7 @@ jobs: runs-on: macos-15 needs: get-info steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: recursive @@ -118,7 +118,7 @@ jobs: xcode-version: latest - name: Cache CMake Configuration - uses: actions/cache@v4 + uses: actions/cache@v5 env: cache-name: ${{ runner.os }}-sdl-cache-cmake-configuration with: @@ -150,7 +150,7 @@ jobs: mv ${{github.workspace}}/build/shadps4 upload mv ${{github.workspace}}/build/MoltenVK_icd.json upload mv ${{github.workspace}}/build/libMoltenVK.dylib upload - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: shadps4-macos-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: upload/ @@ -159,7 +159,7 @@ jobs: runs-on: ubuntu-24.04 needs: get-info steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: recursive @@ -172,7 +172,7 @@ jobs: run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 clang-19 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev - name: Cache CMake Configuration - uses: actions/cache@v4 + uses: actions/cache@v5 env: cache-name: ${{ runner.os }}-sdl-cache-cmake-configuration with: @@ -200,7 +200,7 @@ jobs: run: | ls -la ${{ github.workspace }}/build/shadps4 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: shadps4-ubuntu64-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: ${{ github.workspace }}/build/shadps4 @@ -211,7 +211,7 @@ jobs: - name: Package and Upload Linux SDL artifact run: | tar cf shadps4-linux-sdl.tar.gz -C ${{github.workspace}}/build shadps4 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: shadps4-linux-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: Shadps4-sdl.AppImage @@ -220,7 +220,7 @@ jobs: runs-on: ubuntu-24.04 needs: get-info steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: recursive @@ -228,7 +228,7 @@ jobs: run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 gcc-14 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev - name: Cache CMake Configuration - uses: actions/cache@v4 + uses: actions/cache@v5 env: cache-name: ${{ runner.os }}-sdl-gcc-cache-cmake-configuration with: @@ -258,7 +258,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download all artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: path: ./artifacts From 4d62930075925f5ff7c1d0d9db015e2b0b588397 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Tue, 17 Mar 2026 09:22:22 +0200 Subject: [PATCH 13/44] tagged 0.15.0 release --- CMakeLists.txt | 8 ++++---- dist/net.shadps4.shadPS4.metainfo.xml | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dd02a6378..b75592a4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,13 +202,13 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") -set(EMULATOR_VERSION_MINOR "14") -set(EMULATOR_VERSION_PATCH "1") +set(EMULATOR_VERSION_MINOR "15") +set(EMULATOR_VERSION_PATCH "0") set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}") -set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH} WIP") -set(APP_IS_RELEASE false) +set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") +set(APP_IS_RELEASE true) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/common/scm_rev.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp" @ONLY) message("-- end git things, remote: ${GIT_REMOTE_NAME}, branch: ${GIT_BRANCH}, link: ${GIT_REMOTE_URL}") diff --git a/dist/net.shadps4.shadPS4.metainfo.xml b/dist/net.shadps4.shadPS4.metainfo.xml index 210ca1c5e..8a7fa852b 100644 --- a/dist/net.shadps4.shadPS4.metainfo.xml +++ b/dist/net.shadps4.shadPS4.metainfo.xml @@ -38,7 +38,10 @@ Game - + + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.15.0 + + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.14.0 From 3a3ef5b05f3dc5276857e3e0bcadb03f3f8c89f5 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Tue, 17 Mar 2026 10:26:34 +0200 Subject: [PATCH 14/44] started 0.15.1 WIP --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b75592a4c..ee6f37802 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,12 +203,12 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") set(EMULATOR_VERSION_MINOR "15") -set(EMULATOR_VERSION_PATCH "0") +set(EMULATOR_VERSION_PATCH "1") set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}") -set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") -set(APP_IS_RELEASE true) +set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH} WIP") +set(APP_IS_RELEASE false) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/common/scm_rev.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp" @ONLY) message("-- end git things, remote: ${GIT_REMOTE_NAME}, branch: ${GIT_BRANCH}, link: ${GIT_REMOTE_URL}") From e6b743032dc7f48ba3e839cc4a726b5c2cf3d705 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:58:31 +0100 Subject: [PATCH 15/44] Don't print unresolved libc and libSceFios2 stubs (#4137) --- src/core/linker.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/linker.cpp b/src/core/linker.cpp index 6640c7204..3f410e926 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -361,8 +361,10 @@ bool Linker::Resolve(const std::string& name, Loader::SymbolType sym_type, Modul return_info->virtual_address = AeroLib::GetStub(sr.name.c_str()); return_info->name = "Unknown !!!"; } - LOG_WARNING(Core_Linker, "Linker: Stub resolved {} as {} (lib: {}, mod: {})", sr.name, - return_info->name, library->name, module->name); + if (library->name != "libc" && library->name != "libSceFios2") { + LOG_WARNING(Core_Linker, "Linker: Stub resolved {} as {} (lib: {}, mod: {})", sr.name, + return_info->name, library->name, module->name); + } return false; } From 88c34372402ed32f5102c2504604800df8d0139f Mon Sep 17 00:00:00 2001 From: shinra-electric <50119606+shinra-electric@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:36:59 +0000 Subject: [PATCH 16/44] Bump ccache-action (#4138) --- .github/workflows/build.yml | 92 ++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b54698e3a..3d77c5800 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: env: COMMIT_RANGE: ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} run: ./.ci/clang-format.sh - + get-info: runs-on: ubuntu-24.04 outputs: @@ -78,14 +78,14 @@ jobs: env: cache-name: ${{ runner.os }}-sdl-ninja-cache-cmake-configuration with: - path: | + path: | ${{github.workspace}}/build key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} restore-keys: | ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.21 env: cache-name: ${{ runner.os }}-sdl-cache-cmake-build with: @@ -119,17 +119,17 @@ jobs: - name: Cache CMake Configuration uses: actions/cache@v5 - env: + env: cache-name: ${{ runner.os }}-sdl-cache-cmake-configuration - with: - path: | - ${{github.workspace}}/build - key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - restore-keys: | - ${{ env.cache-name }}- + with: + path: | + ${{github.workspace}}/build + key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} + restore-keys: | + ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.21 env: cache-name: ${{runner.os}}-sdl-cache-cmake-build with: @@ -173,17 +173,17 @@ jobs: - name: Cache CMake Configuration uses: actions/cache@v5 - env: + env: cache-name: ${{ runner.os }}-sdl-cache-cmake-configuration - with: - path: | - ${{github.workspace}}/build - key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - restore-keys: | - ${{ env.cache-name }}- + with: + path: | + ${{github.workspace}}/build + key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} + restore-keys: | + ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.21 env: cache-name: ${{ runner.os }}-sdl-cache-cmake-build with: @@ -195,11 +195,11 @@ jobs: - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc) - - - name: Package and Upload Linux(ubuntu64) SDL artifact + + - name: Package and Upload Linux(ubuntu64) SDL artifact run: | ls -la ${{ github.workspace }}/build/shadps4 - + - uses: actions/upload-artifact@v6 with: name: shadps4-ubuntu64-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} @@ -207,7 +207,7 @@ jobs: - name: Run AppImage packaging script run: ./.github/linux-appimage-sdl.sh - + - name: Package and Upload Linux SDL artifact run: | tar cf shadps4-linux-sdl.tar.gz -C ${{github.workspace}}/build shadps4 @@ -229,17 +229,17 @@ jobs: - name: Cache CMake Configuration uses: actions/cache@v5 - env: + env: cache-name: ${{ runner.os }}-sdl-gcc-cache-cmake-configuration - with: - path: | - ${{github.workspace}}/build - key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - restore-keys: | - ${{ env.cache-name }}- + with: + path: | + ${{github.workspace}}/build + key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} + restore-keys: | + ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.21 env: cache-name: ${{ runner.os }}-sdl-gcc-cache-cmake-build with: @@ -266,7 +266,7 @@ jobs: run: | chmod -R a+x ./artifacts/shadps4-linux-sdl-* chmod -R a+x ./artifacts/shadps4-macos-sdl-* - + - name: Compress individual directories (without parent directory) run: | cd ./artifacts @@ -277,7 +277,7 @@ jobs: (cd "$dir_name" && zip -r "../${dir_name}.zip" .) fi done - + - name: Get latest release information id: get_latest_release env: @@ -351,52 +351,52 @@ jobs: upload_url="https://uploads.github.com/repos/$REPO/releases/$release_id/assets?name=$filename" curl -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/octet-stream" --data-binary @"$file" "$upload_url" done - + - name: Get current pre-release information env: GITHUB_TOKEN: ${{ secrets.SHADPS4_TOKEN_REPO }} run: | api_url="https://api.github.com/repos/${{ github.repository }}/releases" - + # Get all releases (sorted by date) releases=$(curl -H "Authorization: token $GITHUB_TOKEN" "$api_url") - + # Capture the most recent pre-release (assuming the first one is the latest) current_release=$(echo "$releases" | jq -c '.[] | select(.prerelease == true) | .published_at' | sort -r | head -n 1) - + # Remove extra quotes from captured date current_release=$(echo $current_release | tr -d '"') - + # Export the current published_at to be available for the next step echo "CURRENT_PUBLISHED_AT=$current_release" >> $GITHUB_ENV - + - name: Delete old pre-releases and tags env: GITHUB_TOKEN: ${{ secrets.SHADPS4_TOKEN_REPO }} run: | api_url="https://api.github.com/repos/${{ github.repository }}/releases" - + # Get current pre-releases releases=$(curl -H "Authorization: token $GITHUB_TOKEN" "$api_url") - + # Remove extra quotes from captured date CURRENT_PUBLISHED_AT=$(echo $CURRENT_PUBLISHED_AT | tr -d '"') - + # Convert CURRENT_PUBLISHED_AT para timestamp Unix current_published_ts=$(date -d "$CURRENT_PUBLISHED_AT" +%s) - + # Identify pre-releases echo "$releases" | jq -c '.[] | select(.prerelease == true)' | while read -r release; do release_date=$(echo "$release" | jq -r '.published_at') release_id=$(echo "$release" | jq -r '.id') release_tag=$(echo "$release" | jq -r '.tag_name') - + # Remove extra quotes from captured date release_date=$(echo $release_date | tr -d '"') - + # Convert release_date para timestamp Unix release_date_ts=$(date -d "$release_date" +%s) - + # Compare timestamps and delete old pre-releases if [[ "$release_date_ts" -lt "$current_published_ts" ]]; then echo "Deleting old pre-release: $release_id from $release_date with tag: $release_tag" From 1bb152d9769a454652e2658b2e35c429c96127ee Mon Sep 17 00:00:00 2001 From: baggins183 Date: Tue, 17 Mar 2026 12:47:19 -0700 Subject: [PATCH 17/44] IMAGE_STORE_MIP fallback (#4075) * fallback for IMAGE_STORE_MIP when not natively supported * Lod should be treated as absolute, independent of sharp's base_level (judging by other implemented instructions) * fix descriptor set layouts * dumb error * force fallback for testing * treat Lod as relative to base_level * optimization when lod index is constant --- .../backend/spirv/emit_spirv_image.cpp | 26 +++++-- .../backend/spirv/spirv_emit_context.cpp | 16 ++++- .../backend/spirv/spirv_emit_context.h | 2 +- src/shader_recompiler/ir/passes/ir_passes.h | 2 +- .../ir/passes/resource_tracking_pass.cpp | 59 +++++++++++++-- src/shader_recompiler/recompiler.cpp | 2 +- src/shader_recompiler/resource.h | 11 +++ src/shader_recompiler/specialization.h | 5 +- .../renderer_vulkan/vk_compute_pipeline.cpp | 6 +- .../renderer_vulkan/vk_graphics_pipeline.cpp | 6 +- .../renderer_vulkan/vk_pipeline_cache.cpp | 2 +- .../renderer_vulkan/vk_rasterizer.cpp | 71 ++++++++++++++----- 12 files changed, 168 insertions(+), 40 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp index e2a969b61..0b05dcef4 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp @@ -220,20 +220,33 @@ Id EmitImageGradient(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id Id EmitImageRead(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id lod, Id ms) { const auto& texture = ctx.images[handle & 0xFFFF]; - const Id image = ctx.OpLoad(texture.image_type, texture.id); const Id color_type = texture.data_types->Get(4); ImageOperands operands; operands.Add(spv::ImageOperandsMask::Sample, ms); Id texel; if (!texture.is_storage) { + const Id image = ctx.OpLoad(texture.image_type, texture.id); operands.Add(spv::ImageOperandsMask::Lod, lod); texel = ctx.OpImageFetch(color_type, image, coords, operands.mask, operands.operands); } else { + Id image_ptr = texture.id; if (ctx.profile.supports_image_load_store_lod) { operands.Add(spv::ImageOperandsMask::Lod, lod); } else if (Sirit::ValidId(lod)) { - LOG_WARNING(Render, "Image read with LOD not supported by driver"); +#if 1 + // It's confusing what interactions will cause this code path so leave it as + // unreachable until a case is found. + // Normally IMAGE_LOAD_MIP should translate -> OpImageFetch + UNREACHABLE_MSG("Unsupported ImageRead with Lod"); +#else + LOG_WARNING(Render, "Fallback for ImageRead with LOD"); + ASSERT(texture.mip_fallback_mode == MipStorageFallbackMode::DynamicIndex); + const Id single_image_ptr_type = + ctx.TypePointer(spv::StorageClass::UniformConstant, texture.image_type); + image_ptr = ctx.OpAccessChain(single_image_ptr_type, image_ptr, std::array{lod}); +#endif } + const Id image = ctx.OpLoad(texture.image_type, image_ptr); texel = ctx.OpImageRead(color_type, image, coords, operands.mask, operands.operands); } return texture.is_integer ? ctx.OpBitcast(ctx.F32[4], texel) : texel; @@ -242,15 +255,20 @@ Id EmitImageRead(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id lod void EmitImageWrite(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id lod, Id ms, Id color) { const auto& texture = ctx.images[handle & 0xFFFF]; - const Id image = ctx.OpLoad(texture.image_type, texture.id); + Id image_ptr = texture.id; const Id color_type = texture.data_types->Get(4); ImageOperands operands; operands.Add(spv::ImageOperandsMask::Sample, ms); if (ctx.profile.supports_image_load_store_lod) { operands.Add(spv::ImageOperandsMask::Lod, lod); } else if (Sirit::ValidId(lod)) { - LOG_WARNING(Render, "Image write with LOD not supported by driver"); + LOG_WARNING(Render, "Fallback for ImageWrite with LOD"); + ASSERT(texture.mip_fallback_mode == MipStorageFallbackMode::DynamicIndex); + const Id single_image_ptr_type = + ctx.TypePointer(spv::StorageClass::UniformConstant, texture.image_type); + image_ptr = ctx.OpAccessChain(single_image_ptr_type, image_ptr, std::array{lod}); } + const Id image = ctx.OpLoad(texture.image_type, image_ptr); const Id texel = texture.is_integer ? ctx.OpBitcast(color_type, color) : color; ctx.OpImageWrite(image, coords, texel, operands.mask, operands.operands); } diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 261155ab5..c0e469964 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -961,23 +961,33 @@ void EmitContext::DefineImagesAndSamplers() { const auto nfmt = sharp.GetNumberFmt(); const bool is_integer = AmdGpu::IsInteger(nfmt); const bool is_storage = image_desc.is_written; + const MipStorageFallbackMode mip_fallback_mode = image_desc.mip_fallback_mode; const VectorIds& data_types = GetAttributeType(*this, nfmt); const Id sampled_type = data_types[1]; const Id image_type{ImageType(*this, image_desc, sampled_type)}; - const Id pointer_type{TypePointer(spv::StorageClass::UniformConstant, image_type)}; + + const u32 num_bindings = image_desc.NumBindings(info); + Id pointee_type = image_type; + if (mip_fallback_mode == MipStorageFallbackMode::DynamicIndex) { + pointee_type = TypeArray(pointee_type, ConstU32(num_bindings)); + } + + const Id pointer_type{TypePointer(spv::StorageClass::UniformConstant, pointee_type)}; const Id id{AddGlobalVariable(pointer_type, spv::StorageClass::UniformConstant)}; - Decorate(id, spv::Decoration::Binding, binding.unified++); + Decorate(id, spv::Decoration::Binding, binding.unified); + binding.unified += num_bindings; Decorate(id, spv::Decoration::DescriptorSet, 0U); + // TODO better naming for resources (flattened sharp_idx is not informative) Name(id, fmt::format("{}_{}{}", stage, "img", image_desc.sharp_idx)); images.push_back({ .data_types = &data_types, .id = id, .sampled_type = is_storage ? sampled_type : TypeSampledImage(image_type), - .pointer_type = pointer_type, .image_type = image_type, .view_type = sharp.GetViewType(image_desc.is_array), .is_integer = is_integer, .is_storage = is_storage, + .mip_fallback_mode = mip_fallback_mode, }); interfaces.push_back(id); } diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index 9bb2b7d7a..a9c6f0968 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -293,11 +293,11 @@ public: const VectorIds* data_types; Id id; Id sampled_type; - Id pointer_type; Id image_type; AmdGpu::ImageType view_type; bool is_integer = false; bool is_storage = false; + MipStorageFallbackMode mip_fallback_mode{}; }; enum class PointerType : u32 { diff --git a/src/shader_recompiler/ir/passes/ir_passes.h b/src/shader_recompiler/ir/passes/ir_passes.h index f103b6736..1b14a1c6b 100644 --- a/src/shader_recompiler/ir/passes/ir_passes.h +++ b/src/shader_recompiler/ir/passes/ir_passes.h @@ -19,7 +19,7 @@ void DeadCodeEliminationPass(IR::Program& program); void ConstantPropagationPass(IR::BlockList& program); void FlattenExtendedUserdataPass(IR::Program& program); void ReadLaneEliminationPass(IR::Program& program); -void ResourceTrackingPass(IR::Program& program); +void ResourceTrackingPass(IR::Program& program, const Profile& profile); void CollectShaderInfoPass(IR::Program& program, const Profile& profile); void LowerBufferFormatToRaw(IR::Program& program); void LowerFp64ToFp32(IR::Program& program); diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 4c41e94e9..3b7888ab3 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -9,6 +9,7 @@ #include "shader_recompiler/ir/operand_helper.h" #include "shader_recompiler/ir/program.h" #include "shader_recompiler/ir/reinterpret.h" +#include "shader_recompiler/profile.h" #include "video_core/amdgpu/resource.h" namespace Shader::Optimization { @@ -255,7 +256,9 @@ public: u32 Add(const ImageResource& desc) { const u32 index{Add(image_resources, desc, [&desc](const auto& existing) { - return desc.sharp_idx == existing.sharp_idx && desc.is_array == existing.is_array; + return desc.sharp_idx == existing.sharp_idx && desc.is_array == existing.is_array && + desc.mip_fallback_mode == existing.mip_fallback_mode && + desc.constant_mip_index == existing.constant_mip_index; })}; auto& image = image_resources[index]; image.is_atomic |= desc.is_atomic; @@ -529,14 +532,21 @@ void PatchBufferSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& inst.SetArg(0, ir.Imm32(buffer_binding)); } -void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& descriptors) { +void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& descriptors, + const Profile& profile) { // Read image sharp. const auto inst_info = inst.Flags(); const IR::Inst* image_handle = inst.Arg(0).InstRecursive(); const auto tsharp = TrackSharp(image_handle, block, inst_info.pc); const bool is_atomic = IsImageAtomicInstruction(inst); const bool is_written = inst.GetOpcode() == IR::Opcode::ImageWrite || is_atomic; - const ImageResource image_res = { + const bool is_storage = + inst.GetOpcode() == IR::Opcode::ImageRead || inst.GetOpcode() == IR::Opcode::ImageWrite; + // ImageRead with !is_written gets emitted as OpImageFetch with LOD operand, doesn't + // need fallback (TODO is this 100% true?) + const bool needs_mip_storage_fallback = + inst_info.has_lod && is_written && !profile.supports_image_load_store_lod; + ImageResource image_res = { .sharp_idx = tsharp, .is_depth = bool(inst_info.is_depth), .is_atomic = is_atomic, @@ -544,9 +554,42 @@ void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& .is_written = is_written, .is_r128 = bool(inst_info.is_r128), }; + auto image = image_res.GetSharp(info); ASSERT(image.GetType() != AmdGpu::ImageType::Invalid); + if (needs_mip_storage_fallback) { + // If the mip level to IMAGE_(LOAD/STORE)_MIP is a constant, set up ImageResource + // so that we will only bind a single level. + // If index is dynamic, we will bind levels as an array + const auto view_type = image.GetViewType(image_res.is_array); + + IR::Inst* body = inst.Arg(1).InstRecursive(); + const auto lod_arg = [&] -> IR::Value { + switch (view_type) { + case AmdGpu::ImageType::Color1D: // x, [lod] + return body->Arg(1); + case AmdGpu::ImageType::Color1DArray: // x, slice, [lod] + case AmdGpu::ImageType::Color2D: // x, y, [lod] + return body->Arg(2); + case AmdGpu::ImageType::Color2DArray: // x, y, slice, [lod] + case AmdGpu::ImageType::Color3D: // x, y, z, [lod] + return body->Arg(3); + case AmdGpu::ImageType::Color2DMsaa: + case AmdGpu::ImageType::Color2DMsaaArray: + default: + UNREACHABLE_MSG("Invalid image type {}", view_type); + } + }(); + + if (lod_arg.IsImmediate()) { + image_res.mip_fallback_mode = MipStorageFallbackMode::ConstantIndex; + image_res.constant_mip_index = lod_arg.U32(); + } else { + image_res.mip_fallback_mode = MipStorageFallbackMode::DynamicIndex; + } + } + // Patch image instruction if image is FMask. if (AmdGpu::IsFmask(image.GetDataFmt())) { ASSERT_MSG(!is_written, "FMask storage instructions are not supported"); @@ -1080,7 +1123,11 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) { const auto has_ms = view_type == AmdGpu::ImageType::Color2DMsaa || view_type == AmdGpu::ImageType::Color2DMsaaArray; ASSERT(!inst_info.has_lod || !has_ms); - const auto lod = inst_info.has_lod ? IR::U32{arg} : IR::U32{}; + // If we are binding a single mip level as fallback, drop the argument + const auto lod = + (inst_info.has_lod && image_res.mip_fallback_mode != MipStorageFallbackMode::ConstantIndex) + ? IR::U32{arg} + : IR::U32{}; const auto ms = has_ms ? IR::U32{arg} : IR::U32{}; const auto is_storage = image_res.is_written; @@ -1111,7 +1158,7 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) { } } -void ResourceTrackingPass(IR::Program& program) { +void ResourceTrackingPass(IR::Program& program, const Profile& profile) { // Iterate resource instructions and patch them after finding the sharp. auto& info = program.info; @@ -1122,7 +1169,7 @@ void ResourceTrackingPass(IR::Program& program) { if (IsBufferInstruction(inst)) { PatchBufferSharp(*block, inst, info, descriptors); } else if (IsImageInstruction(inst)) { - PatchImageSharp(*block, inst, info, descriptors); + PatchImageSharp(*block, inst, info, descriptors, profile); } } } diff --git a/src/shader_recompiler/recompiler.cpp b/src/shader_recompiler/recompiler.cpp index f4fa45afc..d6efa2890 100644 --- a/src/shader_recompiler/recompiler.cpp +++ b/src/shader_recompiler/recompiler.cpp @@ -80,7 +80,7 @@ IR::Program TranslateProgram(const std::span& code, Pools& pools, Inf Shader::Optimization::RingAccessElimination(program, runtime_info); Shader::Optimization::ReadLaneEliminationPass(program); Shader::Optimization::FlattenExtendedUserdataPass(program); - Shader::Optimization::ResourceTrackingPass(program); + Shader::Optimization::ResourceTrackingPass(program, profile); Shader::Optimization::LowerBufferFormatToRaw(program); Shader::Optimization::SharedMemorySimplifyPass(program, profile); Shader::Optimization::SharedMemoryToStoragePass(program, runtime_info, profile); diff --git a/src/shader_recompiler/resource.h b/src/shader_recompiler/resource.h index 5ae3179f6..82a861e2a 100644 --- a/src/shader_recompiler/resource.h +++ b/src/shader_recompiler/resource.h @@ -71,6 +71,8 @@ struct BufferResource { }; using BufferResourceList = boost::container::static_vector; +enum class MipStorageFallbackMode : u32 { None, DynamicIndex, ConstantIndex }; + struct ImageResource { u32 sharp_idx; bool is_depth{}; @@ -78,6 +80,8 @@ struct ImageResource { bool is_array{}; bool is_written{}; bool is_r128{}; + MipStorageFallbackMode mip_fallback_mode{}; + u32 constant_mip_index{}; constexpr AmdGpu::Image GetSharp(const auto& info) const noexcept { AmdGpu::Image image{}; @@ -102,6 +106,13 @@ struct ImageResource { } return image; } + + u32 NumBindings(const auto& info) const { + const AmdGpu::Image tsharp = GetSharp(info); + return (mip_fallback_mode == MipStorageFallbackMode::DynamicIndex) + ? (tsharp.last_level - tsharp.base_level + 1) + : 1; + } }; using ImageResourceList = boost::container::static_vector; diff --git a/src/shader_recompiler/specialization.h b/src/shader_recompiler/specialization.h index 4f6bb44bf..fa14583af 100644 --- a/src/shader_recompiler/specialization.h +++ b/src/shader_recompiler/specialization.h @@ -52,6 +52,8 @@ struct ImageSpecialization { bool is_srgb = false; AmdGpu::CompMapping dst_select{}; AmdGpu::NumberConversion num_conversion{}; + // FIXME any pipeline cache changes needed? + u32 num_bindings = 0; bool operator==(const ImageSpecialization&) const = default; }; @@ -133,7 +135,7 @@ struct StageSpecialization { } }); ForEachSharp(binding, images, info->images, - [](auto& spec, const auto& desc, AmdGpu::Image sharp) { + [&](auto& spec, const auto& desc, AmdGpu::Image sharp) { spec.type = sharp.GetViewType(desc.is_array); spec.is_integer = AmdGpu::IsInteger(sharp.GetNumberFmt()); spec.is_storage = desc.is_written; @@ -144,6 +146,7 @@ struct StageSpecialization { spec.is_srgb = sharp.GetNumberFmt() == AmdGpu::NumberFormat::Srgb; } spec.num_conversion = sharp.GetNumberConversion(); + spec.num_bindings = desc.NumBindings(*info); }); ForEachSharp(binding, fmasks, info->fmasks, [](auto& spec, const auto& desc, AmdGpu::Image sharp) { diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp index eecd416d1..ba0a3afa2 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp @@ -48,13 +48,15 @@ ComputePipeline::ComputePipeline(const Instance& instance, Scheduler& scheduler, }); } for (const auto& image : info->images) { + const u32 num_bindings = image.NumBindings(*info); bindings.push_back({ - .binding = binding++, + .binding = binding, .descriptorType = image.is_written ? vk::DescriptorType::eStorageImage : vk::DescriptorType::eSampledImage, - .descriptorCount = 1, + .descriptorCount = num_bindings, .stageFlags = vk::ShaderStageFlagBits::eCompute, }); + binding += num_bindings; } for (const auto& sampler : info->samplers) { bindings.push_back({ diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 242c9b6f2..bc9ef571b 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -457,13 +457,15 @@ void GraphicsPipeline::BuildDescSetLayout(bool preloading) { }); } for (const auto& image : stage->images) { + const u32 num_bindings = image.NumBindings(*stage); bindings.push_back({ - .binding = binding++, + .binding = binding, .descriptorType = image.is_written ? vk::DescriptorType::eStorageImage : vk::DescriptorType::eSampledImage, - .descriptorCount = 1, + .descriptorCount = num_bindings, .stageFlags = stage_bit, }); + binding += num_bindings; } for (const auto& sampler : stage->samplers) { bindings.push_back({ diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 1b0af1d17..fdf6b3f2d 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -246,7 +246,7 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, .support_fp32_denorm_flush = bool(vk12_props.shaderDenormFlushToZeroFloat32), .support_fp32_round_to_zero = bool(vk12_props.shaderRoundingModeRTZFloat32), .support_legacy_vertex_attributes = instance_.IsLegacyVertexAttributesSupported(), - .supports_image_load_store_lod = instance_.IsImageLoadStoreLodSupported(), + .supports_image_load_store_lod = /*instance_.IsImageLoadStoreLodSupported()*/ false, // TEST .supports_native_cube_calc = instance_.IsAmdGcnShaderSupported(), .supports_trinary_minmax = instance_.IsAmdShaderTrinaryMinMaxSupported(), // TODO: Emitted bounds checks cause problems with phi control flow; needs to be fixed. diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 737c9feed..80af19372 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -662,6 +662,13 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Binding void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindings& binding) { image_bindings.clear(); + const u32 first_image_idx = image_infos.size(); + // For loading/storing to explicit mip levels, when no native instruction support, bind an array + // of descriptors consecutively, 1 for each mip level. The shader can index this with LOD + // operand. + // This array holds the size of each consecutive array with the number of bindings consumed. + // This is currently always 1 for anything other than mip fallback arrays. + boost::container::small_vector image_descriptor_array_sizes; for (const auto& image_desc : stage.images) { const auto tsharp = image_desc.GetSharp(stage); @@ -671,25 +678,43 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin if (tsharp.GetDataFmt() == AmdGpu::DataFormat::FormatInvalid) { image_bindings.emplace_back(std::piecewise_construct, std::tuple{}, std::tuple{}); + image_descriptor_array_sizes.push_back(1); continue; } - auto& [image_id, desc] = image_bindings.emplace_back(std::piecewise_construct, std::tuple{}, - std::tuple{tsharp, image_desc}); - image_id = texture_cache.FindImage(desc); - auto* image = &texture_cache.GetImage(image_id); - if (image->depth_id) { - // If this image has an associated depth image, it's a stencil attachment. - // Redirect the access to the actual depth-stencil buffer. - image_id = image->depth_id; - image = &texture_cache.GetImage(image_id); + const Shader::MipStorageFallbackMode mip_fallback_mode = image_desc.mip_fallback_mode; + const u32 num_bindings = image_desc.NumBindings(stage); + + for (auto i = 0; i < num_bindings; i++) { + auto& [image_id, desc] = image_bindings.emplace_back( + std::piecewise_construct, std::tuple{}, std::tuple{tsharp, image_desc}); + + if (mip_fallback_mode == Shader::MipStorageFallbackMode::ConstantIndex) { + ASSERT(num_bindings == 1); + desc.view_info.range.base.level += image_desc.constant_mip_index; + desc.view_info.range.extent.levels = 1; + } else if (mip_fallback_mode == Shader::MipStorageFallbackMode::DynamicIndex) { + desc.view_info.range.base.level += i; + desc.view_info.range.extent.levels = 1; + } + + image_id = texture_cache.FindImage(desc); + auto* image = &texture_cache.GetImage(image_id); + if (image->depth_id) { + // If this image has an associated depth image, it's a stencil attachment. + // Redirect the access to the actual depth-stencil buffer. + image_id = image->depth_id; + image = &texture_cache.GetImage(image_id); + } + if (image->binding.is_bound) { + // The image is already bound. In case if it is about to be used as storage we + // need to force general layout on it. + image->binding.force_general |= image_desc.is_written; + } + image->binding.is_bound = 1u; } - if (image->binding.is_bound) { - // The image is already bound. In case if it is about to be used as storage we need - // to force general layout on it. - image->binding.force_general |= image_desc.is_written; - } - image->binding.is_bound = 1u; + + image_descriptor_array_sizes.push_back(num_bindings); } // Second pass to re-bind images that were updated after binding @@ -749,16 +774,26 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin image_infos.emplace_back(VK_NULL_HANDLE, *image_view.image_view, image.backing->state.layout); } + } + u32 image_info_idx = first_image_idx; + u32 image_binding_idx = 0; + for (u32 array_size : image_descriptor_array_sizes) { + const auto& [_, desc] = image_bindings[image_binding_idx]; + const bool is_storage = desc.type == VideoCore::TextureCache::BindingType::Storage; set_writes.push_back({ .dstSet = VK_NULL_HANDLE, - .dstBinding = binding.unified++, + .dstBinding = binding.unified, .dstArrayElement = 0, - .descriptorCount = 1, + .descriptorCount = array_size, .descriptorType = is_storage ? vk::DescriptorType::eStorageImage : vk::DescriptorType::eSampledImage, - .pImageInfo = &image_infos.back(), + .pImageInfo = &image_infos[image_info_idx], }); + + image_info_idx += array_size; + image_binding_idx += array_size; + binding.unified += array_size; } for (const auto& sampler : stage.samplers) { From 6e843d0c4ba977d584d04698d20cf2570131af00 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Tue, 17 Mar 2026 22:18:26 +0200 Subject: [PATCH 18/44] feeling dangerous , let's re-enable lod where supported --- src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index fdf6b3f2d..1b0af1d17 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -246,7 +246,7 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, .support_fp32_denorm_flush = bool(vk12_props.shaderDenormFlushToZeroFloat32), .support_fp32_round_to_zero = bool(vk12_props.shaderRoundingModeRTZFloat32), .support_legacy_vertex_attributes = instance_.IsLegacyVertexAttributesSupported(), - .supports_image_load_store_lod = /*instance_.IsImageLoadStoreLodSupported()*/ false, // TEST + .supports_image_load_store_lod = instance_.IsImageLoadStoreLodSupported(), .supports_native_cube_calc = instance_.IsAmdGcnShaderSupported(), .supports_trinary_minmax = instance_.IsAmdShaderTrinaryMinMaxSupported(), // TODO: Emitted bounds checks cause problems with phi control flow; needs to be fixed. From 9a3e7b097c9d0153143a5f4ce3fbb245275a7f16 Mon Sep 17 00:00:00 2001 From: rosenkolev1 <50500415+rosenkolev1@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:40:37 +0200 Subject: [PATCH 19/44] Make thread TidCounter atomic (#4133) --- src/core/libraries/kernel/threads/pthread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index f97451154..3742db5cf 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -242,7 +242,7 @@ int PS4_SYSV_ABI posix_pthread_create_name_np(PthreadT* thread, const PthreadAtt new_thread->attr.sched_policy = curthread->attr.sched_policy; } - static int TidCounter = 1; + static std::atomic TidCounter = 1; new_thread->tid = ++TidCounter; if (new_thread->attr.stackaddr_attr == nullptr) { From 2ca342970a8c5f0a03ae026cb9004d6a49794b75 Mon Sep 17 00:00:00 2001 From: Kravickas Date: Wed, 18 Mar 2026 09:05:20 +0100 Subject: [PATCH 20/44] MIP fixes (#4141) * int32-modifiers GCN VOP3 abs/neg modifier bits always operate on the sign bit (bit 31) regardless of instruction type. For integer operands this means: abs = clear bit 31 (x & 0x7FFFFFFF) neg = toggle bit 31 (x ^ 0x80000000) * int64-modifiers Previously GetSrc64 completely ignored input modifiers for integer operands. Now unpacks to two U32s, modifies the high dword's bit 31 (= bit 63 of the 64-bit value), and repacks. * V_MUL_LEGACY_F32 GCN V_MUL_LEGACY_F32: if either source is zero, result is +0.0 regardless of the other operand (even NaN or Inf). Standard IEEE multiply produces NaN for 0*Inf. The fix adds a zero-check select before the multiply. --- .../frontend/translate/translate.cpp | 21 +++++++++++++++++-- .../frontend/translate/translate.h | 1 + .../frontend/translate/vector_alu.cpp | 15 ++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/shader_recompiler/frontend/translate/translate.cpp b/src/shader_recompiler/frontend/translate/translate.cpp index 3aa70e2ec..611070a86 100644 --- a/src/shader_recompiler/frontend/translate/translate.cpp +++ b/src/shader_recompiler/frontend/translate/translate.cpp @@ -352,10 +352,10 @@ T Translator::GetSrc(const InstOperand& operand) { } } else { if (operand.input_modifier.abs) { - value = ir.IAbs(value); + value = ir.BitwiseAnd(value, ir.Imm32(0x7FFFFFFFu)); } if (operand.input_modifier.neg) { - value = ir.INeg(value); + value = ir.BitwiseXor(value, ir.Imm32(0x80000000u)); } } return value; @@ -453,6 +453,23 @@ T Translator::GetSrc64(const InstOperand& operand) { if (operand.input_modifier.neg) { value = ir.FPNeg(value); } + } else { + // GCN VOP3 abs/neg modifier bits operate on the sign bit (bit 63 for + // 64-bit values). Unpack, modify the high dword's bit 31, repack. + if (operand.input_modifier.abs) { + const auto unpacked = ir.UnpackUint2x32(value); + const auto lo = IR::U32{ir.CompositeExtract(unpacked, 0)}; + const auto hi = IR::U32{ir.CompositeExtract(unpacked, 1)}; + const auto hi_abs = ir.BitwiseAnd(hi, ir.Imm32(0x7FFFFFFFu)); + value = ir.PackUint2x32(ir.CompositeConstruct(lo, hi_abs)); + } + if (operand.input_modifier.neg) { + const auto unpacked = ir.UnpackUint2x32(value); + const auto lo = IR::U32{ir.CompositeExtract(unpacked, 0)}; + const auto hi = IR::U32{ir.CompositeExtract(unpacked, 1)}; + const auto hi_neg = ir.BitwiseXor(hi, ir.Imm32(0x80000000u)); + value = ir.PackUint2x32(ir.CompositeConstruct(lo, hi_neg)); + } } return value; } diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 08b0192f5..5ee75e336 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -153,6 +153,7 @@ public: void V_SUB_F32(const GcnInst& inst); void V_SUBREV_F32(const GcnInst& inst); void V_MUL_F32(const GcnInst& inst); + void V_MUL_LEGACY_F32(const GcnInst& inst); void V_MUL_I32_I24(const GcnInst& inst, bool is_signed); void V_MIN_F32(const GcnInst& inst, bool is_legacy = false); void V_MAX_F32(const GcnInst& inst, bool is_legacy = false); diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index 08a0f6527..23236b702 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -25,7 +25,7 @@ void Translator::EmitVectorAlu(const GcnInst& inst) { case Opcode::V_MAC_LEGACY_F32: return V_MAC_F32(inst); case Opcode::V_MUL_LEGACY_F32: - return V_MUL_F32(inst); + return V_MUL_LEGACY_F32(inst); case Opcode::V_MUL_F32: return V_MUL_F32(inst); case Opcode::V_MUL_I32_I24: @@ -493,6 +493,19 @@ void Translator::V_MUL_F32(const GcnInst& inst) { SetDst(inst.dst[0], ir.FPMul(GetSrc(inst.src[0]), GetSrc(inst.src[1]))); } +void Translator::V_MUL_LEGACY_F32(const GcnInst& inst) { + // GCN V_MUL_LEGACY_F32: if either source is zero, the result is +0.0 + // regardless of the other operand (even if NaN or Inf). + // Standard IEEE multiply would produce NaN for 0 * Inf. + const IR::F32 src0{GetSrc(inst.src[0])}; + const IR::F32 src1{GetSrc(inst.src[1])}; + const IR::F32 zero{ir.Imm32(0.0f)}; + const IR::U1 src0_zero{ir.FPEqual(src0, zero)}; + const IR::U1 src1_zero{ir.FPEqual(src1, zero)}; + const IR::U1 either_zero{ir.LogicalOr(src0_zero, src1_zero)}; + SetDst(inst.dst[0], IR::F32{ir.Select(either_zero, zero, ir.FPMul(src0, src1))}); +} + void Translator::V_MUL_I32_I24(const GcnInst& inst, bool is_signed) { const IR::U32 src0{ ir.BitFieldExtract(GetSrc(inst.src[0]), ir.Imm32(0), ir.Imm32(24), is_signed)}; From ec1719e4d35947931538771942a5264e7db5e34d Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:58:00 +0100 Subject: [PATCH 21/44] update current firmware version (#4144) --- src/core/libraries/kernel/kernel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/kernel.h b/src/core/libraries/kernel/kernel.h index ce6446129..315af6b06 100644 --- a/src/core/libraries/kernel/kernel.h +++ b/src/core/libraries/kernel/kernel.h @@ -37,7 +37,7 @@ struct OrbisWrapperImpl { #define ORBIS(func) (Libraries::Kernel::OrbisWrapperImpl::wrap) -#define CURRENT_FIRMWARE_VERSION 0x13020011 +#define CURRENT_FIRMWARE_VERSION 0x13500011 s32* PS4_SYSV_ABI __Error(); From 78411c4b8a21e55078f8a55ff2fb69636cf63c1d Mon Sep 17 00:00:00 2001 From: Kravickas Date: Wed, 18 Mar 2026 22:09:19 +0100 Subject: [PATCH 22/44] Write after write sync hazard (#4142) * WAW barrier * clang --- src/video_core/texture_cache/image.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index 972f028d4..fa28d0c65 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -239,7 +239,13 @@ Image::Barriers Image::GetBarriers(vk::ImageLayout dst_layout, vk::AccessFlags2 ASSERT(subres_idx < subresource_states.size()); auto& state = subresource_states[subres_idx]; - if (state.layout != dst_layout || state.access_mask != dst_mask) { + if (state.layout != dst_layout || state.access_mask != dst_mask || + static_cast(dst_mask & + (vk::AccessFlagBits2::eTransferWrite | + vk::AccessFlagBits2::eShaderWrite | + vk::AccessFlagBits2::eColorAttachmentWrite | + vk::AccessFlagBits2::eDepthStencilAttachmentWrite | + vk::AccessFlagBits2::eMemoryWrite))) { barriers.emplace_back(vk::ImageMemoryBarrier2{ .srcStageMask = state.pl_stage, .srcAccessMask = state.access_mask, @@ -269,7 +275,12 @@ Image::Barriers Image::GetBarriers(vk::ImageLayout dst_layout, vk::AccessFlags2 subresource_states.clear(); } } else { // Full resource transition - if (last_state.layout == dst_layout && last_state.access_mask == dst_mask) { + constexpr auto write_flags = + vk::AccessFlagBits2::eTransferWrite | vk::AccessFlagBits2::eShaderWrite | + vk::AccessFlagBits2::eColorAttachmentWrite | + vk::AccessFlagBits2::eDepthStencilAttachmentWrite | vk::AccessFlagBits2::eMemoryWrite; + const bool is_write = static_cast(dst_mask & write_flags); + if (last_state.layout == dst_layout && last_state.access_mask == dst_mask && !is_write) { return {}; } From f245cf76a7663a5dc434c0a45a0cf3fd4ce3c5f8 Mon Sep 17 00:00:00 2001 From: Kravickas Date: Wed, 18 Mar 2026 22:52:15 +0100 Subject: [PATCH 23/44] waw hotfix (#4146) --- src/video_core/texture_cache/image.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index fa28d0c65..ffa5168c5 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -647,6 +647,10 @@ void Image::CopyImageWithBuffer(Image& src_image, vk::Buffer buffer, u64 offset) cmdbuf.copyBufferToImage(buffer, GetImage(), vk::ImageLayout::eTransferDstOptimal, buffer_copies); + + // Match CopyImage: transition to general so shaders can sample the result. + Transit(vk::ImageLayout::eGeneral, + vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); } void Image::CopyMip(Image& src_image, u32 mip, u32 slice) { From 64e10e6f981c101e1cf85381260db8e5c3bc7fb8 Mon Sep 17 00:00:00 2001 From: Semahegn <83661382+Seme30@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:02:03 +0300 Subject: [PATCH 24/44] sdl_window: Call SDL_SyncWindow after setting fullscreen mode (#4131) SDL_SetWindowFullscreen is asynchronous on Windows. Without SDL_SyncWindow, the window may not have finished transitioning to borderless fullscreen before the Vulkan surface and swapchain are created, causing borderless mode to silently fail at startup. This is most visible with Immediate (No VSync) present mode where the swapchain is created quickly, but the fix is correct for all present modes. SDL3 docs state: "On asynchronous windowing systems, this acts as a synchronization barrier for pending window state." and SDL_SyncWindow is listed as the recommended follow-up to SDL_SetWindowFullscreen. https://wiki.libsdl.org/SDL3/SDL_SyncWindow Fixes #3945 --- src/sdl_window.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index ae4646de6..838fc4db4 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -323,6 +323,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ window, Config::getFullscreenMode() == "Fullscreen" ? displayMode : NULL); } SDL_SetWindowFullscreen(window, Config::getIsFullscreen()); + SDL_SyncWindow(window); SDL_InitSubSystem(SDL_INIT_GAMEPAD); controller->SetEngine(std::make_unique()); From 16066f4834863bc1a3bcfe6a9bc7860c592c774e Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Thu, 19 Mar 2026 09:51:32 +0000 Subject: [PATCH 25/44] Upgrade GitHub Actions to latest versions (#4148) Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d77c5800..53411369c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@v6 - - uses: fsfe/reuse-action@v5 + - uses: fsfe/reuse-action@v6 clang-format: runs-on: ubuntu-24.04 From 0c3fac6ce0a3edf16f6b5d78add8eb06969a04d1 Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Thu, 19 Mar 2026 09:51:45 +0000 Subject: [PATCH 26/44] Upgrade GitHub Actions for Node 24 compatibility (#4147) Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com> --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53411369c..26c8e20fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,7 +99,7 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $env:NUMBER_OF_PROCESSORS - name: Upload Windows SDL artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: shadps4-win64-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: ${{github.workspace}}/build/shadPS4.exe @@ -150,7 +150,7 @@ jobs: mv ${{github.workspace}}/build/shadps4 upload mv ${{github.workspace}}/build/MoltenVK_icd.json upload mv ${{github.workspace}}/build/libMoltenVK.dylib upload - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: shadps4-macos-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: upload/ @@ -200,7 +200,7 @@ jobs: run: | ls -la ${{ github.workspace }}/build/shadps4 - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: shadps4-ubuntu64-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: ${{ github.workspace }}/build/shadps4 @@ -211,7 +211,7 @@ jobs: - name: Package and Upload Linux SDL artifact run: | tar cf shadps4-linux-sdl.tar.gz -C ${{github.workspace}}/build shadps4 - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: shadps4-linux-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: Shadps4-sdl.AppImage From 980919b07bb12cfd34ed5914a9fcf38bab7cc5d9 Mon Sep 17 00:00:00 2001 From: Kravickas Date: Fri, 20 Mar 2026 07:46:22 +0100 Subject: [PATCH 27/44] Fix FSR crash on content area resize (#4153) * fsr * fsr * fsr --- src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp | 7 +++++++ src/video_core/texture_cache/image.cpp | 8 ++++++++ src/video_core/texture_cache/image.h | 2 ++ 3 files changed, 17 insertions(+) diff --git a/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp b/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp index 8f58f3499..373ec1711 100644 --- a/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp +++ b/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp @@ -407,6 +407,13 @@ void FsrPass::ResizeAndInvalidate(u32 width, u32 height) { void FsrPass::CreateImages(Img& img) const { img.dirty = false; + // Destroy previous resources before re-creating at new size. + // Views first, then images (views reference the images). + img.intermediary_image_view.reset(); + img.output_image_view.reset(); + img.intermediary_image.Destroy(); + img.output_image.Destroy(); + vk::ImageCreateInfo image_create_info{ .imageType = vk::ImageType::e2D, .format = vk::Format::eR16G16B16A16Sfloat, diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index ffa5168c5..e458f5cb3 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -82,6 +82,14 @@ UniqueImage::~UniqueImage() { } } +void UniqueImage::Destroy() { + if (image) { + vmaDestroyImage(allocator, image, allocation); + image = vk::Image{}; + allocation = {}; + } +} + void UniqueImage::Create(const vk::ImageCreateInfo& image_ci) { this->image_ci = image_ci; ASSERT(!image); diff --git a/src/video_core/texture_cache/image.h b/src/video_core/texture_cache/image.h index 00b9296b7..0bf471dce 100644 --- a/src/video_core/texture_cache/image.h +++ b/src/video_core/texture_cache/image.h @@ -59,6 +59,8 @@ struct UniqueImage { void Create(const vk::ImageCreateInfo& image_ci); + void Destroy(); + operator vk::Image() const { return image; } From a858f9c96cc74c45ac9c49b954a504a8eecb269b Mon Sep 17 00:00:00 2001 From: AlpinDale <52078762+AlpinDale@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:17:46 +0430 Subject: [PATCH 28/44] chore: add an RFC issue template (#4150) --- .github/ISSUE_TEMPLATE/rfc.yaml | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/rfc.yaml diff --git a/.github/ISSUE_TEMPLATE/rfc.yaml b/.github/ISSUE_TEMPLATE/rfc.yaml new file mode 100644 index 000000000..7ad453101 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/rfc.yaml @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later +# Docs - https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema +name: Request For Comments (RFC) +description: Ask for feedback on major architectural changes or design choices +title: "[RFC]: " +labels: ["RFC"] + +body: + - type: markdown + attributes: + value: | + ## Important: Read First + + RFCs are for major architectural changes, design direction, or changes that benefit from structured discussion before merge. + + Please make an effort to search for an existing RFC or issue before opening a new one. + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have searched for a similar RFC or issue in this repository and did not find one. + required: true + + - type: textarea + id: motivation + attributes: + label: Motivation + description: | + Explain the problem this RFC is trying to solve. + + Describe why the current design is insufficient and why this change is worth discussing now. + validations: + required: true + + - type: textarea + id: proposed_change + attributes: + label: Proposed Change + description: | + Describe the proposed change in enough detail for maintainers and contributors to evaluate it. + + Include the high-level design, affected areas, and any important constraints. + validations: + required: true + + - type: textarea + id: feedback_period + attributes: + label: Feedback Period + description: | + State the intended review window for this RFC. + + Example: one week, two weeks, or until specific maintainers have reviewed it. + placeholder: "Example: 1 week" + validations: + required: false + + - type: textarea + id: cc_list + attributes: + label: CC List + description: | + List any maintainers or contributors you want to explicitly notify for feedback. + validations: + required: false + + - type: textarea + id: additional_context + attributes: + label: Any Other Things + description: | + Add any other relevant context, tradeoffs, diagrams, migration notes, or links to related work. + validations: + required: false From 2bb20e46509210b517b50694a4ff65e80c858f6a Mon Sep 17 00:00:00 2001 From: Kravickas Date: Fri, 20 Mar 2026 12:43:41 +0100 Subject: [PATCH 29/44] waw fix (#4154) --- src/video_core/texture_cache/image.cpp | 27 +++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index e458f5cb3..44ddd55c6 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -247,13 +247,11 @@ Image::Barriers Image::GetBarriers(vk::ImageLayout dst_layout, vk::AccessFlags2 ASSERT(subres_idx < subresource_states.size()); auto& state = subresource_states[subres_idx]; - if (state.layout != dst_layout || state.access_mask != dst_mask || - static_cast(dst_mask & - (vk::AccessFlagBits2::eTransferWrite | - vk::AccessFlagBits2::eShaderWrite | - vk::AccessFlagBits2::eColorAttachmentWrite | - vk::AccessFlagBits2::eDepthStencilAttachmentWrite | - vk::AccessFlagBits2::eMemoryWrite))) { + constexpr auto write_flags = vk::AccessFlagBits2::eTransferWrite | + vk::AccessFlagBits2::eShaderWrite | + vk::AccessFlagBits2::eMemoryWrite; + const bool is_write = static_cast(state.access_mask & write_flags); + if (state.layout != dst_layout || state.access_mask != dst_mask || is_write) { barriers.emplace_back(vk::ImageMemoryBarrier2{ .srcStageMask = state.pl_stage, .srcAccessMask = state.access_mask, @@ -283,11 +281,10 @@ Image::Barriers Image::GetBarriers(vk::ImageLayout dst_layout, vk::AccessFlags2 subresource_states.clear(); } } else { // Full resource transition - constexpr auto write_flags = - vk::AccessFlagBits2::eTransferWrite | vk::AccessFlagBits2::eShaderWrite | - vk::AccessFlagBits2::eColorAttachmentWrite | - vk::AccessFlagBits2::eDepthStencilAttachmentWrite | vk::AccessFlagBits2::eMemoryWrite; - const bool is_write = static_cast(dst_mask & write_flags); + constexpr auto write_flags = vk::AccessFlagBits2::eTransferWrite | + vk::AccessFlagBits2::eShaderWrite | + vk::AccessFlagBits2::eMemoryWrite; + const bool is_write = static_cast(last_state.access_mask & write_flags); if (last_state.layout == dst_layout && last_state.access_mask == dst_mask && !is_write) { return {}; } @@ -385,6 +382,8 @@ void Image::Upload(std::span upload_copies, vk::Buffe .bufferMemoryBarrierCount = 1, .pBufferMemoryBarriers = &post_barrier, }); + Transit(vk::ImageLayout::eGeneral, + vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); flags &= ~ImageFlagBits::Dirty; } @@ -655,8 +654,6 @@ void Image::CopyImageWithBuffer(Image& src_image, vk::Buffer buffer, u64 offset) cmdbuf.copyBufferToImage(buffer, GetImage(), vk::ImageLayout::eTransferDstOptimal, buffer_copies); - - // Match CopyImage: transition to general so shaders can sample the result. Transit(vk::ImageLayout::eGeneral, vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); } @@ -698,6 +695,8 @@ void Image::CopyMip(Image& src_image, u32 mip, u32 slice) { const auto cmdbuf = scheduler->CommandBuffer(); cmdbuf.copyImage(src_image.GetImage(), src_image.backing->state.layout, GetImage(), backing->state.layout, image_copy); + Transit(vk::ImageLayout::eGeneral, + vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); } void Image::Resolve(Image& src_image, const VideoCore::SubresourceRange& mrt0_range, From 0a722d69e63f49e122a42e3c9654195781bfe333 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 21 Mar 2026 05:02:17 -0500 Subject: [PATCH 30/44] Only iterate to next VMA if we're past the current VMA. (#4158) It's possible we're merging with a later memory area. If that occurred here, we would end up iterating past where we need to be, which then messes up logic. --- src/core/memory.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 9d26142ce..768fb3c90 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -1223,13 +1223,16 @@ s32 MemoryManager::SetDirectMemoryType(VAddr addr, u64 size, s32 memory_type) { // Increment phys_handle phys_handle++; } - - // Check if VMA can be merged with adjacent areas after physical area modifications. - vma_handle = MergeAdjacent(vma_map, vma_handle); } current_addr += size_in_vma; remaining_size -= size_in_vma; - vma_handle++; + + // Check if VMA can be merged with adjacent areas after modifications. + vma_handle = MergeAdjacent(vma_map, vma_handle); + if (vma_handle->second.base + vma_handle->second.size <= current_addr) { + // If we're now in the next VMA, then go to the next handle. + vma_handle++; + } } return ORBIS_OK; @@ -1262,10 +1265,15 @@ void MemoryManager::NameVirtualRange(VAddr virtual_addr, u64 size, std::string_v vma.name = name; } } - it = MergeAdjacent(vma_map, it); remaining_size -= size_in_vma; current_addr += size_in_vma; - it++; + + // Check if VMA can be merged with adjacent areas after modifications. + it = MergeAdjacent(vma_map, it); + if (it->second.base + it->second.size <= current_addr) { + // If we're now in the next VMA, then go to the next handle. + it++; + } } } From 08168dc38656d326e055e15d5737d76a2c201cb7 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 21 Mar 2026 22:26:36 +0200 Subject: [PATCH 31/44] New config mode (part1 of 0.15.1 branch series) (#4145) * using new emulator_settings * the default user is now just player one * transfer install, addon dirs * fix load custom config issue --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> --- CMakeLists.txt | 6 + src/common/logging/backend.cpp | 16 +- src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + src/common/path_util.cpp | 1 + src/common/path_util.h | 2 + src/common/serdes.h | 3 +- src/core/address_space.cpp | 8 +- src/core/devtools/layer.cpp | 16 +- src/core/devtools/widget/frame_graph.cpp | 6 +- src/core/devtools/widget/module_list.h | 6 +- src/core/devtools/widget/shader_list.cpp | 8 +- src/core/emulator_settings.cpp | 647 ++++++++++++++++++ src/core/emulator_settings.h | 636 +++++++++++++++++ src/core/ipc/ipc.cpp | 4 +- .../libraries/app_content/app_content.cpp | 6 +- src/core/libraries/audio/sdl_audio_in.cpp | 4 +- src/core/libraries/audio/sdl_audio_out.cpp | 14 +- src/core/libraries/gnmdriver/gnmdriver.cpp | 6 +- src/core/libraries/kernel/process.cpp | 8 +- src/core/libraries/network/net.cpp | 2 +- src/core/libraries/network/net_ctl_obj.cpp | 14 +- src/core/libraries/network/netctl.cpp | 12 +- src/core/libraries/np/np_auth.cpp | 6 +- src/core/libraries/np/np_matching2.cpp | 4 +- src/core/libraries/np/np_web_api2.cpp | 12 +- src/core/libraries/np/np_web_api_internal.cpp | 8 +- src/core/libraries/np/trophy_ui.cpp | 12 +- src/core/libraries/system/systemservice.cpp | 8 +- src/core/libraries/usbd/usbd.cpp | 12 +- src/core/libraries/videoout/driver.cpp | 7 +- src/core/libraries/videoout/video_out.cpp | 8 +- src/core/linker.cpp | 4 +- src/core/memory.cpp | 8 +- src/core/module.h | 6 +- src/core/user_manager.cpp | 177 +++++ src/core/user_manager.h | 58 ++ src/core/user_settings.cpp | 110 +++ src/core/user_settings.h | 46 ++ src/emulator.cpp | 69 +- src/imgui/renderer/imgui_core.cpp | 8 +- src/imgui/renderer/imgui_impl_sdl3.cpp | 14 +- src/imgui/renderer/texture_manager.cpp | 6 +- src/main.cpp | 29 +- .../spirv/emit_spirv_context_get_set.cpp | 6 +- .../frontend/translate/translate.cpp | 6 +- .../passes/flatten_extended_userdata_pass.cpp | 7 +- .../ir/passes/shader_info_collection_pass.cpp | 6 +- src/shader_recompiler/ir/program.cpp | 5 +- src/video_core/amdgpu/liverpool.cpp | 12 +- src/video_core/buffer_cache/memory_tracker.h | 5 +- src/video_core/buffer_cache/region_manager.h | 8 +- src/video_core/cache_storage.cpp | 28 +- src/video_core/renderdoc.cpp | 8 +- .../renderer_vulkan/host_passes/fsr_pass.cpp | 8 +- .../renderer_vulkan/host_passes/pp_pass.cpp | 8 +- .../renderer_vulkan/vk_pipeline_cache.cpp | 14 +- .../vk_pipeline_serialization.cpp | 4 +- .../renderer_vulkan/vk_platform.cpp | 12 +- .../renderer_vulkan/vk_presenter.cpp | 22 +- .../renderer_vulkan/vk_rasterizer.cpp | 22 +- .../renderer_vulkan/vk_swapchain.cpp | 10 +- 62 files changed, 1967 insertions(+), 263 deletions(-) create mode 100644 src/core/emulator_settings.cpp create mode 100644 src/core/emulator_settings.h create mode 100644 src/core/user_manager.cpp create mode 100644 src/core/user_manager.h create mode 100644 src/core/user_settings.cpp create mode 100644 src/core/user_settings.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ee6f37802..ab204b7a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -871,6 +871,12 @@ set(CORE src/core/aerolib/stubs.cpp src/core/tls.h src/core/emulator_state.cpp src/core/emulator_state.h + src/core/emulator_settings.cpp + src/core/emulator_settings.h + src/core/user_manager.cpp + src/core/user_manager.h + src/core/user_settings.cpp + src/core/user_settings.h ) if (ARCHITECTURE STREQUAL "x86_64") diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 930b1ac30..5de4f64a0 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -14,7 +14,6 @@ #endif #include "common/bounded_threadsafe_queue.h" -#include "common/config.h" #include "common/debug.h" #include "common/io_file.h" #include "common/logging/backend.h" @@ -24,6 +23,7 @@ #include "common/path_util.h" #include "common/string_util.h" #include "common/thread.h" +#include "core/emulator_settings.h" namespace Common::Log { @@ -141,7 +141,7 @@ public: const auto& log_dir = GetUserPath(PathType::LogDir); std::filesystem::create_directory(log_dir); Filter filter; - filter.ParseFilterString(Config::getLogFilter()); + filter.ParseFilterString(EmulatorSettings.GetLogFilter()); const auto& log_file_path = log_file.empty() ? LOG_FILE : log_file; instance = std::unique_ptr( new Impl(log_dir / log_file_path, filter), Deleter); @@ -185,7 +185,7 @@ public: void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num, const char* function, const char* format, const fmt::format_args& args) { - if (!filter.CheckMessage(log_class, log_level) || !Config::getLoggingEnabled()) { + if (!filter.CheckMessage(log_class, log_level) || !EmulatorSettings.IsLogEnabled()) { return; } @@ -213,7 +213,7 @@ public: using std::chrono::microseconds; using std::chrono::steady_clock; - if (Config::groupIdenticalLogs()) { + if (EmulatorSettings.IsIdenticalLogGrouped()) { std::unique_lock entry_loc(_mutex); if (_last_entry.message == message) { @@ -226,7 +226,7 @@ public: } if (_last_entry.counter >= 1) { - if (Config::getLogType() == "async") { + if (EmulatorSettings.GetLogType() == "async") { message_queue.EmplaceWait(_last_entry); } else { ForEachBackend([this](auto& backend) { backend.Write(this->_last_entry); }); @@ -258,7 +258,7 @@ public: .counter = 1, }; - if (Config::getLogType() == "async") { + if (EmulatorSettings.GetLogType() == "async") { message_queue.EmplaceWait(entry); } else { ForEachBackend([&entry](auto& backend) { backend.Write(entry); }); @@ -296,14 +296,14 @@ private: } void StopBackendThread() { - if (Config::groupIdenticalLogs()) { + if (EmulatorSettings.IsIdenticalLogGrouped()) { // log last message if (_last_entry.counter >= 2) { _last_entry.message += " x" + std::to_string(_last_entry.counter); } if (_last_entry.counter >= 1) { - if (Config::getLogType() == "async") { + if (EmulatorSettings.GetLogType() == "async") { message_queue.EmplaceWait(_last_entry); } else { ForEachBackend([this](auto& backend) { backend.Write(this->_last_entry); }); diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 9a3fe0aa1..f2597603e 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -164,6 +164,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(Input) \ CLS(Tty) \ CLS(KeyManager) \ + CLS(EmuSettings) \ CLS(Loader) // GetClassName is a macro defined by Windows.h, grrr... diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 9e176c698..4c6e53453 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -132,6 +132,7 @@ enum class Class : u8 { Input, ///< Input emulation Tty, ///< Debug output from emu KeyManager, ///< Key management system + EmuSettings, /// Emulator settings system Count ///< Total number of logging classes }; diff --git a/src/common/path_util.cpp b/src/common/path_util.cpp index 5d37990ff..103f17d29 100644 --- a/src/common/path_util.cpp +++ b/src/common/path_util.cpp @@ -129,6 +129,7 @@ static auto UserPaths = [] { create_path(PathType::CustomConfigs, user_dir / CUSTOM_CONFIGS); create_path(PathType::CacheDir, user_dir / CACHE_DIR); create_path(PathType::FontsDir, user_dir / FONTS_DIR); + create_path(PathType::HomeDir, user_dir / HOME_DIR); std::ofstream notice_file(user_dir / CUSTOM_TROPHY / "Notice.txt"); if (notice_file.is_open()) { diff --git a/src/common/path_util.h b/src/common/path_util.h index 434f77b0d..485c72270 100644 --- a/src/common/path_util.h +++ b/src/common/path_util.h @@ -26,6 +26,7 @@ enum class PathType { CustomConfigs, // Where custom files for different games are stored. CacheDir, // Where pipeline and shader cache is stored. FontsDir, // Where dumped system fonts are stored. + HomeDir, // PS4 home directory }; constexpr auto PORTABLE_DIR = "user"; @@ -46,6 +47,7 @@ constexpr auto CUSTOM_TROPHY = "custom_trophy"; constexpr auto CUSTOM_CONFIGS = "custom_configs"; constexpr auto CACHE_DIR = "cache"; constexpr auto FONTS_DIR = "fonts"; +constexpr auto HOME_DIR = "home"; // Filenames constexpr auto LOG_FILE = "shad_log.txt"; diff --git a/src/common/serdes.h b/src/common/serdes.h index a36fed4d3..f91a0ace8 100644 --- a/src/common/serdes.h +++ b/src/common/serdes.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -7,6 +7,7 @@ #include "common/types.h" #include +#include namespace Serialization { diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index 758c7240c..ca3d52042 100644 --- a/src/core/address_space.cpp +++ b/src/core/address_space.cpp @@ -1,14 +1,14 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/alignment.h" #include "common/arch.h" #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/error.h" #include "core/address_space.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/memory.h" #include "core/memory.h" #include "libraries/error_codes.h" @@ -187,7 +187,7 @@ struct AddressSpace::Impl { user_size = supported_user_max - USER_MIN - 1; // Increase BackingSize to account for config options. - BackingSize += Config::getExtraDmemInMbytes() * 1_MB; + BackingSize += EmulatorSettings.GetExtraDmemInMBytes() * 1_MB; // Allocate backing file that represents the total physical memory. backing_handle = CreateFileMapping2(INVALID_HANDLE_VALUE, nullptr, FILE_MAP_ALL_ACCESS, @@ -606,7 +606,7 @@ enum PosixPageProtection { struct AddressSpace::Impl { Impl() { - BackingSize += Config::getExtraDmemInMbytes() * 1_MB; + BackingSize += EmulatorSettings.GetExtraDmemInMBytes() * 1_MB; // Allocate virtual address placeholder for our address space. system_managed_size = SystemManagedSize; system_reserved_size = SystemReservedSize; diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index 4be107713..10e5f911c 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -7,10 +7,10 @@ #include #include "SDL3/SDL_log.h" -#include "common/config.h" #include "common/singleton.h" #include "common/types.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/emulator_state.h" #include "imgui/imgui_std.h" #include "imgui_internal.h" @@ -110,11 +110,11 @@ void L::DrawMenuBar() { EndDisabled(); if (Button("Save")) { - Config::setFsrEnabled(fsr.enable); - Config::setRcasEnabled(fsr.use_rcas); - Config::setRcasAttenuation(static_cast(fsr.rcas_attenuation * 1000)); - Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / - "config.toml"); + EmulatorSettings.SetFsrEnabled(fsr.enable); + EmulatorSettings.SetRcasEnabled(fsr.use_rcas); + EmulatorSettings.SetRcasAttenuation( + static_cast(fsr.rcas_attenuation * 1000)); + EmulatorSettings.Save(); CloseCurrentPopup(); } @@ -311,7 +311,7 @@ static void LoadSettings(const char* line) { void L::SetupSettings() { frame_graph.is_open = true; - show_simple_fps = Config::getShowFpsCounter(); + show_simple_fps = EmulatorSettings.IsShowFpsCounter(); using SettingLoader = void (*)(const char*); @@ -472,7 +472,7 @@ void L::Draw() { if (ImGui::Begin("Volume Window", &show_volume, ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) { - Text("Volume: %d", Config::getVolumeSlider()); + Text("Volume: %d", EmulatorSettings.GetVolumeSlider()); } End(); } diff --git a/src/core/devtools/widget/frame_graph.cpp b/src/core/devtools/widget/frame_graph.cpp index 6b63d4978..6d4452074 100644 --- a/src/core/devtools/widget/frame_graph.cpp +++ b/src/core/devtools/widget/frame_graph.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "frame_graph.h" -#include "common/config.h" #include "common/singleton.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "imgui.h" #include "imgui_internal.h" @@ -29,7 +29,7 @@ void FrameGraph::DrawFrameGraph() { return; } - float target_dt = 1.0f / (float)Config::vblankFreq(); + float target_dt = 1.0f / (float)EmulatorSettings.GetVblankFrequency(); float cur_pos_x = pos.x + full_width; pos.y += FRAME_GRAPH_PADDING_Y; const float final_pos_y = pos.y + FRAME_GRAPH_HEIGHT; diff --git a/src/core/devtools/widget/module_list.h b/src/core/devtools/widget/module_list.h index 0702ac4db..4eed5444d 100644 --- a/src/core/devtools/widget/module_list.h +++ b/src/core/devtools/widget/module_list.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -8,9 +8,9 @@ #include #include #include -#include "common/config.h" #include "common/elf_info.h" #include "common/path_util.h" +#include "core/emulator_settings.h" namespace Core::Devtools::Widget { @@ -23,7 +23,7 @@ public: bool open = false; static bool IsSystemModule(const std::filesystem::path& path) { - const auto sys_modules_path = Config::getSysModulesPath(); + const auto sys_modules_path = EmulatorSettings.GetSysModulesDir(); const auto abs_path = std::filesystem::absolute(path).lexically_normal(); const auto abs_sys_path = std::filesystem::absolute(sys_modules_path).lexically_normal(); diff --git a/src/core/devtools/widget/shader_list.cpp b/src/core/devtools/widget/shader_list.cpp index 0285db5a5..243e2355f 100644 --- a/src/core/devtools/widget/shader_list.cpp +++ b/src/core/devtools/widget/shader_list.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -8,11 +8,11 @@ #include #include "common.h" -#include "common/config.h" #include "common/path_util.h" #include "common/string_util.h" #include "core/debug_state.h" #include "core/devtools/options.h" +#include "core/emulator_settings.h" #include "imgui/imgui_std.h" #include "sdl_window.h" #include "video_core/renderer_vulkan/vk_presenter.h" @@ -244,8 +244,8 @@ void ShaderList::Draw() { return; } - if (!Config::collectShadersForDebug()) { - DrawCenteredText("Enable 'CollectShader' in config to see shaders"); + if (!EmulatorSettings.IsShaderCollect()) { + DrawCenteredText("Enable 'shader_collect' in config to see shaders"); End(); return; } diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp new file mode 100644 index 000000000..c1c0342ea --- /dev/null +++ b/src/core/emulator_settings.cpp @@ -0,0 +1,647 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "emulator_settings.h" +#include "emulator_state.h" + +#include + +using json = nlohmann::json; + +// ── Singleton storage ───────────────────────────────────────────────── +std::shared_ptr EmulatorSettingsImpl::s_instance = nullptr; +std::mutex EmulatorSettingsImpl::s_mutex; + +// ── nlohmann helpers for std::filesystem::path ─────────────────────── +namespace nlohmann { +template <> +struct adl_serializer { + static void to_json(json& j, const std::filesystem::path& p) { + j = p.string(); + } + static void from_json(const json& j, std::filesystem::path& p) { + p = j.get(); + } +}; +} // namespace nlohmann + +namespace toml { +// why is it so hard to avoid exceptions with this library +template +std::optional get_optional(const toml::value& v, const std::string& key) { + if (!v.is_table()) + return std::nullopt; + const auto& tbl = v.as_table(); + auto it = tbl.find(key); + if (it == tbl.end()) + return std::nullopt; + + if constexpr (std::is_same_v) { + if (it->second.is_integer()) { + return static_cast(toml::get(it->second)); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_integer()) { + return static_cast(toml::get(it->second)); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_floating()) { + return toml::get(it->second); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_string()) { + return toml::get(it->second); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_string()) { + return toml::get(it->second); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_boolean()) { + return toml::get(it->second); + } + } else { + static_assert([] { return false; }(), "Unsupported type in get_optional"); + } + + return std::nullopt; +} + +} // namespace toml + +// ── Helpers ─────────────────────────────────────────────────────────── + +void EmulatorSettingsImpl::PrintChangedSummary(const std::vector& changed) { + if (changed.empty()) { + LOG_DEBUG(EmuSettings, "No game-specific overrides applied"); + return; + } + LOG_DEBUG(EmuSettings, "Game-specific overrides applied:"); + for (const auto& k : changed) + LOG_DEBUG(EmuSettings, " * {}", k); +} + +// ── Singleton ──────────────────────────────────────────────────────── +EmulatorSettingsImpl::EmulatorSettingsImpl() = default; + +EmulatorSettingsImpl::~EmulatorSettingsImpl() { + Save(); +} + +std::shared_ptr EmulatorSettingsImpl::GetInstance() { + std::lock_guard lock(s_mutex); + if (!s_instance) + s_instance = std::make_shared(); + return s_instance; +} + +void EmulatorSettingsImpl::SetInstance(std::shared_ptr instance) { + std::lock_guard lock(s_mutex); + s_instance = std::move(instance); +} + +// -------------------- +// General helpers +// -------------------- +bool EmulatorSettingsImpl::AddGameInstallDir(const std::filesystem::path& dir, bool enabled) { + for (const auto& d : m_general.install_dirs.value) + if (d.path == dir) + return false; + m_general.install_dirs.value.push_back({dir, enabled}); + return true; +} + +std::vector EmulatorSettingsImpl::GetGameInstallDirs() const { + std::vector out; + for (const auto& d : m_general.install_dirs.value) + if (d.enabled) + out.push_back(d.path); + return out; +} + +const std::vector& EmulatorSettingsImpl::GetAllGameInstallDirs() const { + return m_general.install_dirs.value; +} + +void EmulatorSettingsImpl::SetAllGameInstallDirs(const std::vector& dirs) { + m_general.install_dirs.value = dirs; +} + +void EmulatorSettingsImpl::RemoveGameInstallDir(const std::filesystem::path& dir) { + auto iterator = + std::find_if(m_general.install_dirs.value.begin(), m_general.install_dirs.value.end(), + [&dir](const GameInstallDir& install_dir) { return install_dir.path == dir; }); + if (iterator != m_general.install_dirs.value.end()) { + m_general.install_dirs.value.erase(iterator); + } +} + +void EmulatorSettingsImpl::SetGameInstallDirEnabled(const std::filesystem::path& dir, + bool enabled) { + auto iterator = + std::find_if(m_general.install_dirs.value.begin(), m_general.install_dirs.value.end(), + [&dir](const GameInstallDir& install_dir) { return install_dir.path == dir; }); + if (iterator != m_general.install_dirs.value.end()) { + iterator->enabled = enabled; + } +} + +void EmulatorSettingsImpl::SetGameInstallDirs( + const std::vector& dirs_config) { + m_general.install_dirs.value.clear(); + for (const auto& dir : dirs_config) { + m_general.install_dirs.value.push_back({dir, true}); + } +} + +const std::vector EmulatorSettingsImpl::GetGameInstallDirsEnabled() { + std::vector enabled_dirs; + for (const auto& dir : m_general.install_dirs.value) { + enabled_dirs.push_back(dir.enabled); + } + return enabled_dirs; +} + +std::filesystem::path EmulatorSettingsImpl::GetHomeDir() { + if (m_general.home_dir.value.empty()) { + return Common::FS::GetUserPath(Common::FS::PathType::HomeDir); + } + return m_general.home_dir.value; +} + +void EmulatorSettingsImpl::SetHomeDir(const std::filesystem::path& dir) { + m_general.home_dir.value = dir; +} + +std::filesystem::path EmulatorSettingsImpl::GetSysModulesDir() { + if (m_general.sys_modules_dir.value.empty()) { + return Common::FS::GetUserPath(Common::FS::PathType::SysModuleDir); + } + return m_general.sys_modules_dir.value; +} + +void EmulatorSettingsImpl::SetSysModulesDir(const std::filesystem::path& dir) { + m_general.sys_modules_dir.value = dir; +} + +std::filesystem::path EmulatorSettingsImpl::GetFontsDir() { + if (m_general.font_dir.value.empty()) { + return Common::FS::GetUserPath(Common::FS::PathType::FontsDir); + } + return m_general.font_dir.value; +} + +void EmulatorSettingsImpl::SetFontsDir(const std::filesystem::path& dir) { + m_general.font_dir.value = dir; +} + +// ── Game-specific override management ──────────────────────────────── +void EmulatorSettingsImpl::ClearGameSpecificOverrides() { + ClearGroupOverrides(m_general); + ClearGroupOverrides(m_debug); + ClearGroupOverrides(m_input); + ClearGroupOverrides(m_audio); + ClearGroupOverrides(m_gpu); + ClearGroupOverrides(m_vulkan); + LOG_DEBUG(EmuSettings, "All game-specific overrides cleared"); +} + +void EmulatorSettingsImpl::ResetGameSpecificValue(const std::string& key) { + // Walk every overrideable group until we find the matching key. + auto tryGroup = [&key](auto& group) { + for (auto& item : group.GetOverrideableFields()) { + if (key == item.key) { + item.reset_game_specific(&group); + return true; + } + } + return false; + }; + if (tryGroup(m_general)) + return; + if (tryGroup(m_debug)) + return; + if (tryGroup(m_input)) + return; + if (tryGroup(m_audio)) + return; + if (tryGroup(m_gpu)) + return; + if (tryGroup(m_vulkan)) + return; + LOG_WARNING(EmuSettings, "ResetGameSpecificValue: key '{}' not found", key); +} + +bool EmulatorSettingsImpl::Save(const std::string& serial) { + try { + if (!serial.empty()) { + const auto cfgDir = Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs); + std::filesystem::create_directories(cfgDir); + const auto path = cfgDir / (serial + ".json"); + + json j = json::object(); + + json generalObj = json::object(); + SaveGroupGameSpecific(m_general, generalObj); + j["General"] = generalObj; + + json debugObj = json::object(); + SaveGroupGameSpecific(m_debug, debugObj); + j["Debug"] = debugObj; + + json inputObj = json::object(); + SaveGroupGameSpecific(m_input, inputObj); + j["Input"] = inputObj; + + json audioObj = json::object(); + SaveGroupGameSpecific(m_audio, audioObj); + j["Audio"] = audioObj; + + json gpuObj = json::object(); + SaveGroupGameSpecific(m_gpu, gpuObj); + j["GPU"] = gpuObj; + + json vulkanObj = json::object(); + SaveGroupGameSpecific(m_vulkan, vulkanObj); + j["Vulkan"] = vulkanObj; + + std::ofstream out(path); + if (!out) { + LOG_ERROR(EmuSettings, "Failed to open game config for writing: {}", path.string()); + return false; + } + out << std::setw(2) << j; + return !out.fail(); + + } else { + // ── Global config.json ───────────────────────────────────── + const auto path = + Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.json"; + + SetConfigVersion(Common::g_scm_rev); + + json j; + j["General"] = m_general; + j["Debug"] = m_debug; + j["Input"] = m_input; + j["Audio"] = m_audio; + j["GPU"] = m_gpu; + j["Vulkan"] = m_vulkan; + + // Read the existing file so we can preserve keys unknown to this build + json existing = json::object(); + if (std::ifstream existingIn{path}; existingIn.good()) { + try { + existingIn >> existing; + } catch (...) { + existing = json::object(); + } + } + + // Merge: update each section's known keys, but leave unknown keys intact + for (auto& [section, val] : j.items()) { + if (existing.contains(section) && existing[section].is_object() && val.is_object()) + existing[section].update(val); // overwrites known keys, keeps unknown ones + else + existing[section] = val; + } + + std::ofstream out(path); + if (!out) { + LOG_ERROR(EmuSettings, "Failed to open config for writing: {}", path.string()); + return false; + } + out << std::setw(2) << existing; + return !out.fail(); + } + } catch (const std::exception& e) { + LOG_ERROR(EmuSettings, "Error saving settings: {}", e.what()); + return false; + } +} + +// ── Load ────────────────────────────────────────────────────────────── + +bool EmulatorSettingsImpl::Load(const std::string& serial) { + try { + if (serial.empty()) { + // ── Global config ────────────────────────────────────────── + const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + const auto configPath = userDir / "config.json"; + LOG_DEBUG(EmuSettings, "Loading global config from: {}", configPath.string()); + + if (std::ifstream in{configPath}; in.good()) { + json gj; + in >> gj; + + auto mergeGroup = [&gj](auto& group, const char* section) { + if (!gj.contains(section)) + return; + json current = group; + current.update(gj.at(section)); + group = current.get>(); + }; + + mergeGroup(m_general, "General"); + mergeGroup(m_debug, "Debug"); + mergeGroup(m_input, "Input"); + mergeGroup(m_audio, "Audio"); + mergeGroup(m_gpu, "GPU"); + mergeGroup(m_vulkan, "Vulkan"); + + LOG_DEBUG(EmuSettings, "Global config loaded successfully"); + } else { + if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / + "config.toml")) { + SDL_MessageBoxButtonData btns[2]{ + {0, 0, "No"}, + {0, 1, "Yes"}, + }; + SDL_MessageBoxData msg_box{ + 0, + nullptr, + "Config Migration", + "The shadPS4 config backend has been updated, and you only have " + "the old version of the config. Do you wish to update it " + "automatically, or continue with the default config?", + 2, + btns, + nullptr, + }; + int result = 1; + SDL_ShowMessageBox(&msg_box, &result); + if (result == 1) { + if (TransferSettings()) { + Save(); + return true; + } else { + SDL_ShowSimpleMessageBox(0, "Config Migration", + "Error transferring settings, exiting.", + nullptr); + std::quick_exit(1); + } + } + } + LOG_DEBUG(EmuSettings, "Global config not found - using defaults"); + SetDefaultValues(); + Save(); + } + if (GetConfigVersion() != Common::g_scm_rev) { + Save(); + } + return true; + } else { + // ── Per-game override file ───────────────────────────────── + // Never reloads global settings. Only applies + // game_specific_value overrides on top of the already-loaded + // base configuration. + const auto gamePath = + Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (serial + ".json"); + LOG_DEBUG(EmuSettings, "Applying game config: {}", gamePath.string()); + + if (!std::filesystem::exists(gamePath)) { + LOG_DEBUG(EmuSettings, "No game-specific config found for {}", serial); + return false; + } + + std::ifstream in(gamePath); + if (!in) { + LOG_ERROR(EmuSettings, "Failed to open game config: {}", gamePath.string()); + return false; + } + + json gj; + in >> gj; + + std::vector changed; + + // ApplyGroupOverrides now correctly stores values as + // game_specific_value (see make_override in the header). + // ConfigMode::Default will then resolve them at getter call + // time without ever touching the base values. + if (gj.contains("General")) + ApplyGroupOverrides(m_general, gj.at("General"), changed); + if (gj.contains("Debug")) + ApplyGroupOverrides(m_debug, gj.at("Debug"), changed); + if (gj.contains("Input")) + ApplyGroupOverrides(m_input, gj.at("Input"), changed); + if (gj.contains("Audio")) + ApplyGroupOverrides(m_audio, gj.at("Audio"), changed); + if (gj.contains("GPU")) + ApplyGroupOverrides(m_gpu, gj.at("GPU"), changed); + if (gj.contains("Vulkan")) + ApplyGroupOverrides(m_vulkan, gj.at("Vulkan"), changed); + + PrintChangedSummary(changed); + EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(true); + return true; + } + } catch (const std::exception& e) { + LOG_ERROR(EmuSettings, "Error loading settings: {}", e.what()); + return false; + } +} + +void EmulatorSettingsImpl::SetDefaultValues() { + m_general = GeneralSettings{}; + m_debug = DebugSettings{}; + m_input = InputSettings{}; + m_audio = AudioSettings{}; + m_gpu = GPUSettings{}; + m_vulkan = VulkanSettings{}; +} + +bool EmulatorSettingsImpl::TransferSettings() { + toml::value og_data; + json new_data = json::object(); + try { + auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml"; + std::ifstream ifs; + ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit); + ifs.open(path, std::ios_base::binary); + og_data = toml::parse(ifs, std::string{fmt::UTF(path.filename().u8string()).data}); + } catch (std::exception& ex) { + fmt::print("Got exception trying to load config file. Exception: {}\n", ex.what()); + return false; + } + auto setFromToml = [&](Setting& n, toml::value const& t, std::string k) { + n = toml::get_optional(t, k).value_or(n.default_value); + }; + if (og_data.contains("General")) { + const toml::value& general = og_data.at("General"); + auto& s = m_general; + + setFromToml(s.volume_slider, general, "volumeSlider"); + setFromToml(s.neo_mode, general, "isPS4Pro"); + setFromToml(s.dev_kit_mode, general, "isDevKit"); + setFromToml(s.psn_signed_in, general, "isPSNSignedIn"); + setFromToml(s.trophy_popup_disabled, general, "isTrophyPopupDisabled"); + setFromToml(s.trophy_notification_duration, general, "trophyNotificationDuration"); + setFromToml(s.discord_rpc_enabled, general, "enableDiscordRPC"); + setFromToml(s.log_filter, general, "logFilter"); + setFromToml(s.log_type, general, "logType"); + setFromToml(s.identical_log_grouped, general, "isIdenticalLogGrouped"); + setFromToml(s.show_splash, general, "showSplash"); + setFromToml(s.trophy_notification_side, general, "sideTrophy"); + setFromToml(s.connected_to_network, general, "isConnectedToNetwork"); + setFromToml(s.sys_modules_dir, general, "sysModulesPath"); + setFromToml(s.font_dir, general, "fontsPath"); + // setFromToml(, general, "userName"); + // setFromToml(s.defaultControllerID, general, "defaultControllerID"); + } + + if (og_data.contains("Input")) { + const toml::value& input = og_data.at("Input"); + auto& s = m_input; + + setFromToml(s.cursor_state, input, "cursorState"); + setFromToml(s.cursor_hide_timeout, input, "cursorHideTimeout"); + setFromToml(s.use_special_pad, input, "useSpecialPad"); + setFromToml(s.special_pad_class, input, "specialPadClass"); + setFromToml(s.motion_controls_enabled, input, "isMotionControlsEnabled"); + setFromToml(s.use_unified_input_config, input, "useUnifiedInputConfig"); + setFromToml(s.background_controller_input, input, "backgroundControllerInput"); + setFromToml(s.usb_device_backend, input, "usbDeviceBackend"); + } + + if (og_data.contains("Audio")) { + const toml::value& audio = og_data.at("Audio"); + auto& s = m_audio; + + setFromToml(s.sdl_mic_device, audio, "micDevice"); + setFromToml(s.sdl_main_output_device, audio, "mainOutputDevice"); + setFromToml(s.sdl_padSpk_output_device, audio, "padSpkOutputDevice"); + } + + if (og_data.contains("GPU")) { + const toml::value& gpu = og_data.at("GPU"); + auto& s = m_gpu; + + setFromToml(s.window_width, gpu, "screenWidth"); + setFromToml(s.window_height, gpu, "screenHeight"); + setFromToml(s.internal_screen_width, gpu, "internalScreenWidth"); + setFromToml(s.internal_screen_height, gpu, "internalScreenHeight"); + setFromToml(s.null_gpu, gpu, "nullGpu"); + setFromToml(s.copy_gpu_buffers, gpu, "copyGPUBuffers"); + setFromToml(s.readbacks_mode, gpu, "readbacksMode"); + setFromToml(s.readback_linear_images_enabled, gpu, "readbackLinearImages"); + setFromToml(s.direct_memory_access_enabled, gpu, "directMemoryAccess"); + setFromToml(s.dump_shaders, gpu, "dumpShaders"); + setFromToml(s.patch_shaders, gpu, "patchShaders"); + setFromToml(s.vblank_frequency, gpu, "vblankFrequency"); + setFromToml(s.full_screen, gpu, "Fullscreen"); + setFromToml(s.full_screen_mode, gpu, "FullscreenMode"); + setFromToml(s.present_mode, gpu, "presentMode"); + setFromToml(s.hdr_allowed, gpu, "allowHDR"); + setFromToml(s.fsr_enabled, gpu, "fsrEnabled"); + setFromToml(s.rcas_enabled, gpu, "rcasEnabled"); + setFromToml(s.rcas_attenuation, gpu, "rcasAttenuation"); + } + + if (og_data.contains("Vulkan")) { + const toml::value& vk = og_data.at("Vulkan"); + auto& s = m_vulkan; + + setFromToml(s.gpu_id, vk, "gpuId"); + setFromToml(s.vkvalidation_enabled, vk, "validation"); + setFromToml(s.vkvalidation_core_enabled, vk, "validation_core"); + setFromToml(s.vkvalidation_sync_enabled, vk, "validation_sync"); + setFromToml(s.vkvalidation_gpu_enabled, vk, "validation_gpu"); + setFromToml(s.vkcrash_diagnostic_enabled, vk, "crashDiagnostic"); + setFromToml(s.vkhost_markers, vk, "hostMarkers"); + setFromToml(s.vkguest_markers, vk, "guestMarkers"); + setFromToml(s.renderdoc_enabled, vk, "rdocEnable"); + setFromToml(s.pipeline_cache_enabled, vk, "pipelineCacheEnable"); + setFromToml(s.pipeline_cache_archived, vk, "pipelineCacheArchive"); + } + + if (og_data.contains("Debug")) { + const toml::value& debug = og_data.at("Debug"); + auto& s = m_debug; + + setFromToml(s.debug_dump, debug, "DebugDump"); + setFromToml(s.separate_logging_enabled, debug, "isSeparateLogFilesEnabled"); + setFromToml(s.shader_collect, debug, "CollectShader"); + setFromToml(s.log_enabled, debug, "logEnabled"); + setFromToml(m_general.show_fps_counter, debug, "showFpsCounter"); + } + + if (og_data.contains("Settings")) { + const toml::value& settings = og_data.at("Settings"); + auto& s = m_general; + setFromToml(s.console_language, settings, "consoleLanguage"); + } + + if (og_data.contains("GUI")) { + const toml::value& gui = og_data.at("GUI"); + auto& s = m_general; + + // Transfer install directories + try { + const auto install_dir_array = + toml::find_or>(gui, "installDirs", {}); + std::vector install_dirs_enabled; + + try { + install_dirs_enabled = toml::find>(gui, "installDirsEnabled"); + } catch (...) { + // If it does not exist, assume that all are enabled. + install_dirs_enabled.resize(install_dir_array.size(), true); + } + + if (install_dirs_enabled.size() < install_dir_array.size()) { + install_dirs_enabled.resize(install_dir_array.size(), true); + } + + std::vector settings_install_dirs; + for (size_t i = 0; i < install_dir_array.size(); i++) { + settings_install_dirs.push_back( + {std::filesystem::path{install_dir_array[i]}, install_dirs_enabled[i]}); + } + s.install_dirs.value = settings_install_dirs; + } catch (const std::exception& e) { + LOG_WARNING(EmuSettings, "Failed to transfer install directories: {}", e.what()); + } + + // Transfer addon install directory + try { + std::string addon_install_dir_str; + if (gui.contains("addonInstallDir")) { + const auto& addon_value = gui.at("addonInstallDir"); + if (addon_value.is_string()) { + addon_install_dir_str = toml::get(addon_value); + if (!addon_install_dir_str.empty()) { + s.addon_install_dir.value = std::filesystem::path{addon_install_dir_str}; + } + } + } + } catch (const std::exception& e) { + LOG_WARNING(EmuSettings, "Failed to transfer addon install directory: {}", e.what()); + } + } + + return true; +} + +std::vector EmulatorSettingsImpl::GetAllOverrideableKeys() const { + std::vector keys; + auto addGroup = [&keys](const auto& fields) { + for (const auto& item : fields) + keys.push_back(item.key); + }; + addGroup(m_general.GetOverrideableFields()); + addGroup(m_debug.GetOverrideableFields()); + addGroup(m_input.GetOverrideableFields()); + addGroup(m_audio.GetOverrideableFields()); + addGroup(m_gpu.GetOverrideableFields()); + addGroup(m_vulkan.GetOverrideableFields()); + return keys; +} \ No newline at end of file diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h new file mode 100644 index 000000000..0490aba77 --- /dev/null +++ b/src/core/emulator_settings.h @@ -0,0 +1,636 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "common/types.h" + +#define EmulatorSettings (*EmulatorSettingsImpl::GetInstance()) + +enum HideCursorState : int { + Never, + Idle, + Always, +}; + +enum UsbBackendType : int { + Real, + SkylandersPortal, + InfinityBase, + DimensionsToypad, +}; + +enum GpuReadbacksMode : int { + Disabled, + Relaxed, + Precise, +}; + +enum class ConfigMode { + Default, + Global, + Clean, +}; + +enum AudioBackend : int { + SDL, + OpenAL, + // Add more backends as needed +}; + +template +struct Setting { + T default_value{}; + T value{}; + std::optional game_specific_value{}; + + Setting() = default; + // Single-argument ctor: initialises both default_value and value so + // that CleanMode can always recover the intended factory default. + /*implicit*/ Setting(T init) : default_value(std::move(init)), value(default_value) {} + + /// Return the active value under the given mode. + T get(ConfigMode mode = ConfigMode::Default) const { + switch (mode) { + case ConfigMode::Default: + return game_specific_value.value_or(value); + case ConfigMode::Global: + return value; + case ConfigMode::Clean: + return default_value; + } + return value; + } + + /// Write v to the base layer. + /// Game-specific overrides are applied exclusively via Load(serial) + void set(const T& v) { + value = v; + } + + /// Discard the game-specific override; subsequent get(Default) will + /// fall back to the base value. + void reset_game_specific() { + game_specific_value = std::nullopt; + } +}; + +template +void to_json(nlohmann::json& j, const Setting& s) { + j = s.value; +} + +template +void from_json(const nlohmann::json& j, Setting& s) { + s.value = j.get(); +} + +struct OverrideItem { + const char* key; + std::function& changed)> + apply; + /// Return the value that should be written to the per-game config file. + /// Falls back to base value if no game-specific override is set. + std::function get_for_save; + + /// Clear game_specific_value for this field. + std::function reset_game_specific; +}; + +template +inline OverrideItem make_override(const char* key, Setting Struct::* member) { + return OverrideItem{ + key, + [member, key](void* base, const nlohmann::json& entry, std::vector& changed) { + LOG_DEBUG(EmuSettings, "[make_override] Processing key: {}", key); + LOG_DEBUG(EmuSettings, "[make_override] Entry JSON: {}", entry.dump()); + Struct* obj = reinterpret_cast(base); + Setting& dst = obj->*member; + try { + T newValue = entry.get(); + LOG_DEBUG(EmuSettings, "[make_override] Parsed value: {}", newValue); + LOG_DEBUG(EmuSettings, "[make_override] Current value: {}", dst.value); + if (dst.value != newValue) { + std::ostringstream oss; + oss << key << " ( " << dst.value << " → " << newValue << " )"; + changed.push_back(oss.str()); + LOG_DEBUG(EmuSettings, "[make_override] Recorded change: {}", oss.str()); + } + dst.game_specific_value = newValue; + LOG_DEBUG(EmuSettings, "[make_override] Successfully updated {}", key); + } catch (const std::exception& e) { + LOG_ERROR(EmuSettings, "[make_override] ERROR parsing {}: {}", key, e.what()); + LOG_ERROR(EmuSettings, "[make_override] Entry was: {}", entry.dump()); + LOG_ERROR(EmuSettings, "[make_override] Type name: {}", entry.type_name()); + } + }, + + // --- get_for_save ------------------------------------------- + // Returns game_specific_value when present, otherwise base value. + // This means a freshly-opened game-specific dialog still shows + // useful (current-global) values rather than empty entries. + [member](const void* base) -> nlohmann::json { + const Struct* obj = reinterpret_cast(base); + const Setting& src = obj->*member; + return nlohmann::json(src.game_specific_value.value_or(src.value)); + }, + + // --- reset_game_specific ------------------------------------ + [member](void* base) { + Struct* obj = reinterpret_cast(base); + (obj->*member).reset_game_specific(); + }}; +} + +// ------------------------------- +// Support types +// ------------------------------- +struct GameInstallDir { + std::filesystem::path path; + bool enabled; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GameInstallDir, path, enabled) + +// ------------------------------- +// General settings +// ------------------------------- +struct GeneralSettings { + Setting> install_dirs; + Setting addon_install_dir; + Setting home_dir; + Setting sys_modules_dir; + Setting font_dir; + + Setting volume_slider{100}; + Setting neo_mode{false}; + Setting dev_kit_mode{false}; + Setting extra_dmem_in_mbytes{0}; + Setting psn_signed_in{false}; + Setting trophy_popup_disabled{false}; + Setting trophy_notification_duration{6.0}; + Setting trophy_notification_side{"right"}; + Setting log_filter{""}; + Setting log_type{"sync"}; + Setting show_splash{false}; + Setting identical_log_grouped{true}; + Setting connected_to_network{false}; + Setting discord_rpc_enabled{false}; + Setting show_fps_counter{false}; + Setting console_language{1}; + + // return a vector of override descriptors (runtime, but tiny) + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("volume_slider", &GeneralSettings::volume_slider), + make_override("neo_mode", &GeneralSettings::neo_mode), + make_override("dev_kit_mode", &GeneralSettings::dev_kit_mode), + make_override("extra_dmem_in_mbytes", + &GeneralSettings::extra_dmem_in_mbytes), + make_override("psn_signed_in", &GeneralSettings::psn_signed_in), + make_override("trophy_popup_disabled", + &GeneralSettings::trophy_popup_disabled), + make_override("trophy_notification_duration", + &GeneralSettings::trophy_notification_duration), + make_override("log_filter", &GeneralSettings::log_filter), + make_override("log_type", &GeneralSettings::log_type), + make_override("identical_log_grouped", + &GeneralSettings::identical_log_grouped), + make_override("show_splash", &GeneralSettings::show_splash), + make_override("trophy_notification_side", + &GeneralSettings::trophy_notification_side), + make_override("connected_to_network", + &GeneralSettings::connected_to_network)}; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, install_dirs, addon_install_dir, home_dir, + sys_modules_dir, font_dir, volume_slider, neo_mode, dev_kit_mode, + extra_dmem_in_mbytes, psn_signed_in, trophy_popup_disabled, + trophy_notification_duration, log_filter, log_type, show_splash, + identical_log_grouped, trophy_notification_side, + connected_to_network, discord_rpc_enabled, show_fps_counter, + console_language) + +// ------------------------------- +// Debug settings +// ------------------------------- +struct DebugSettings { + Setting separate_logging_enabled{false}; // specific + Setting debug_dump{false}; // specific + Setting shader_collect{false}; // specific + Setting log_enabled{true}; // specific + Setting config_version{""}; // specific + + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("debug_dump", &DebugSettings::debug_dump), + make_override("shader_collect", &DebugSettings::shader_collect), + make_override("separate_logging_enabled", + &DebugSettings::separate_logging_enabled), + make_override("log_enabled", &DebugSettings::log_enabled)}; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DebugSettings, separate_logging_enabled, debug_dump, + shader_collect, log_enabled, config_version) + +// ------------------------------- +// Input settings +// ------------------------------- + +struct InputSettings { + Setting cursor_state{HideCursorState::Idle}; // specific + Setting cursor_hide_timeout{5}; // specific + Setting usb_device_backend{UsbBackendType::Real}; // specific + Setting use_special_pad{false}; + Setting special_pad_class{1}; + Setting motion_controls_enabled{true}; // specific + Setting use_unified_input_config{true}; + Setting default_controller_id{""}; + Setting background_controller_input{false}; // specific + Setting camera_id{-1}; + + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("cursor_state", &InputSettings::cursor_state), + make_override("cursor_hide_timeout", + &InputSettings::cursor_hide_timeout), + make_override("usb_device_backend", &InputSettings::usb_device_backend), + make_override("motion_controls_enabled", + &InputSettings::motion_controls_enabled), + make_override("background_controller_input", + &InputSettings::background_controller_input), + make_override("camera_id", &InputSettings::camera_id)}; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(InputSettings, cursor_state, cursor_hide_timeout, + usb_device_backend, use_special_pad, special_pad_class, + motion_controls_enabled, use_unified_input_config, + default_controller_id, background_controller_input, camera_id) +// ------------------------------- +// Audio settings +// ------------------------------- +struct AudioSettings { + Setting audio_backend{AudioBackend::SDL}; + Setting sdl_mic_device{"Default Device"}; + Setting sdl_main_output_device{"Default Device"}; + Setting sdl_padSpk_output_device{"Default Device"}; + Setting openal_mic_device{"Default Device"}; + Setting openal_main_output_device{"Default Device"}; + Setting openal_padSpk_output_device{"Default Device"}; + + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("audio_backend", &AudioSettings::audio_backend), + make_override("sdl_mic_device", &AudioSettings::sdl_mic_device), + make_override("sdl_main_output_device", + &AudioSettings::sdl_main_output_device), + make_override("sdl_padSpk_output_device", + &AudioSettings::sdl_padSpk_output_device), + make_override("openal_mic_device", &AudioSettings::openal_mic_device), + make_override("openal_main_output_device", + &AudioSettings::openal_main_output_device), + make_override("openal_padSpk_output_device", + &AudioSettings::openal_padSpk_output_device)}; + } +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AudioSettings, audio_backend, sdl_mic_device, + sdl_main_output_device, sdl_padSpk_output_device, + openal_mic_device, openal_main_output_device, + openal_padSpk_output_device) + +// ------------------------------- +// GPU settings +// ------------------------------- +struct GPUSettings { + Setting window_width{1280}; + Setting window_height{720}; + Setting internal_screen_width{1280}; + Setting internal_screen_height{720}; + Setting null_gpu{false}; + Setting copy_gpu_buffers{false}; + Setting readbacks_mode{GpuReadbacksMode::Disabled}; + Setting readback_linear_images_enabled{false}; + Setting direct_memory_access_enabled{false}; + Setting dump_shaders{false}; + Setting patch_shaders{false}; + Setting vblank_frequency{60}; + Setting full_screen{false}; + Setting full_screen_mode{"Windowed"}; + Setting present_mode{"Mailbox"}; + Setting hdr_allowed{false}; + Setting fsr_enabled{false}; + Setting rcas_enabled{true}; + Setting rcas_attenuation{250}; + // TODO add overrides + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("null_gpu", &GPUSettings::null_gpu), + make_override("copy_gpu_buffers", &GPUSettings::copy_gpu_buffers), + make_override("full_screen", &GPUSettings::full_screen), + make_override("full_screen_mode", &GPUSettings::full_screen_mode), + make_override("present_mode", &GPUSettings::present_mode), + make_override("window_height", &GPUSettings::window_height), + make_override("window_width", &GPUSettings::window_width), + make_override("hdr_allowed", &GPUSettings::hdr_allowed), + make_override("fsr_enabled", &GPUSettings::fsr_enabled), + make_override("rcas_enabled", &GPUSettings::rcas_enabled), + make_override("rcas_attenuation", &GPUSettings::rcas_attenuation), + make_override("dump_shaders", &GPUSettings::dump_shaders), + make_override("patch_shaders", &GPUSettings::patch_shaders), + make_override("readbacks_mode", &GPUSettings::readbacks_mode), + make_override("readback_linear_images_enabled", + &GPUSettings::readback_linear_images_enabled), + make_override("direct_memory_access_enabled", + &GPUSettings::direct_memory_access_enabled), + make_override("vblank_frequency", &GPUSettings::vblank_frequency), + }; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GPUSettings, window_width, window_height, internal_screen_width, + internal_screen_height, null_gpu, copy_gpu_buffers, + readbacks_mode, readback_linear_images_enabled, + direct_memory_access_enabled, dump_shaders, patch_shaders, + vblank_frequency, full_screen, full_screen_mode, present_mode, + hdr_allowed, fsr_enabled, rcas_enabled, rcas_attenuation) +// ------------------------------- +// Vulkan settings +// ------------------------------- +struct VulkanSettings { + Setting gpu_id{-1}; + Setting renderdoc_enabled{false}; + Setting vkvalidation_enabled{false}; + Setting vkvalidation_core_enabled{true}; + Setting vkvalidation_sync_enabled{false}; + Setting vkvalidation_gpu_enabled{false}; + Setting vkcrash_diagnostic_enabled{false}; + Setting vkhost_markers{false}; + Setting vkguest_markers{false}; + Setting pipeline_cache_enabled{false}; + Setting pipeline_cache_archived{false}; + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("gpu_id", &VulkanSettings::gpu_id), + make_override("renderdoc_enabled", &VulkanSettings::renderdoc_enabled), + make_override("vkvalidation_enabled", + &VulkanSettings::vkvalidation_enabled), + make_override("vkvalidation_core_enabled", + &VulkanSettings::vkvalidation_core_enabled), + make_override("vkvalidation_sync_enabled", + &VulkanSettings::vkvalidation_sync_enabled), + make_override("vkvalidation_gpu_enabled", + &VulkanSettings::vkvalidation_gpu_enabled), + make_override("vkcrash_diagnostic_enabled", + &VulkanSettings::vkcrash_diagnostic_enabled), + make_override("vkhost_markers", &VulkanSettings::vkhost_markers), + make_override("vkguest_markers", &VulkanSettings::vkguest_markers), + make_override("pipeline_cache_enabled", + &VulkanSettings::pipeline_cache_enabled), + make_override("pipeline_cache_archived", + &VulkanSettings::pipeline_cache_archived), + }; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(VulkanSettings, gpu_id, renderdoc_enabled, vkvalidation_enabled, + vkvalidation_core_enabled, vkvalidation_sync_enabled, + vkvalidation_gpu_enabled, vkcrash_diagnostic_enabled, + vkhost_markers, vkguest_markers, pipeline_cache_enabled, + pipeline_cache_archived) + +// ------------------------------- +// Main manager +// ------------------------------- +class EmulatorSettingsImpl { +public: + EmulatorSettingsImpl(); + ~EmulatorSettingsImpl(); + + static std::shared_ptr GetInstance(); + static void SetInstance(std::shared_ptr instance); + + bool Save(const std::string& serial = ""); + bool Load(const std::string& serial = ""); + void SetDefaultValues(); + bool TransferSettings(); + + // Config mode + ConfigMode GetConfigMode() const { + return m_configMode; + } + void SetConfigMode(ConfigMode mode) { + m_configMode = mode; + } + + // + // Game-specific override management + /// Clears all per-game overrides. Call this when a game exits so + /// the emulator reverts to global settings. + void ClearGameSpecificOverrides(); + + /// Reset a single field's game-specific override by its JSON ke + void ResetGameSpecificValue(const std::string& key); + + // general accessors + bool AddGameInstallDir(const std::filesystem::path& dir, bool enabled = true); + std::vector GetGameInstallDirs() const; + void SetAllGameInstallDirs(const std::vector& dirs); + void RemoveGameInstallDir(const std::filesystem::path& dir); + void SetGameInstallDirEnabled(const std::filesystem::path& dir, bool enabled); + void SetGameInstallDirs(const std::vector& dirs_config); + const std::vector GetGameInstallDirsEnabled(); + const std::vector& GetAllGameInstallDirs() const; + + std::filesystem::path GetHomeDir(); + void SetHomeDir(const std::filesystem::path& dir); + std::filesystem::path GetSysModulesDir(); + void SetSysModulesDir(const std::filesystem::path& dir); + std::filesystem::path GetFontsDir(); + void SetFontsDir(const std::filesystem::path& dir); + +private: + GeneralSettings m_general{}; + DebugSettings m_debug{}; + InputSettings m_input{}; + AudioSettings m_audio{}; + GPUSettings m_gpu{}; + VulkanSettings m_vulkan{}; + ConfigMode m_configMode{ConfigMode::Default}; + + static std::shared_ptr s_instance; + static std::mutex s_mutex; + + /// Apply overrideable fields from groupJson into group.game_specific_value. + template + void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson, + std::vector& changed) { + for (auto& item : group.GetOverrideableFields()) { + if (!groupJson.contains(item.key)) + continue; + item.apply(&group, groupJson.at(item.key), changed); + } + } + + // Write all overrideable fields from group into out (for game-specific save). + template + static void SaveGroupGameSpecific(const Group& group, nlohmann::json& out) { + for (auto& item : group.GetOverrideableFields()) + out[item.key] = item.get_for_save(&group); + } + + // Discard every game-specific override in group. + template + static void ClearGroupOverrides(Group& group) { + for (auto& item : group.GetOverrideableFields()) + item.reset_game_specific(&group); + } + + static void PrintChangedSummary(const std::vector& changed); + +public: + // Add these getters to access overrideable fields + std::vector GetGeneralOverrideableFields() const { + return m_general.GetOverrideableFields(); + } + std::vector GetDebugOverrideableFields() const { + return m_debug.GetOverrideableFields(); + } + std::vector GetInputOverrideableFields() const { + return m_input.GetOverrideableFields(); + } + std::vector GetAudioOverrideableFields() const { + return m_audio.GetOverrideableFields(); + } + std::vector GetGPUOverrideableFields() const { + return m_gpu.GetOverrideableFields(); + } + std::vector GetVulkanOverrideableFields() const { + return m_vulkan.GetOverrideableFields(); + } + std::vector GetAllOverrideableKeys() const; + +#define SETTING_FORWARD(group, Name, field) \ + auto Get##Name() const { \ + return (group).field.get(m_configMode); \ + } \ + void Set##Name(const decltype((group).field.value)& v) { \ + (group).field.value = v; \ + } +#define SETTING_FORWARD_BOOL(group, Name, field) \ + bool Is##Name() const { \ + return (group).field.get(m_configMode); \ + } \ + void Set##Name(bool v) { \ + (group).field.value = v; \ + } +#define SETTING_FORWARD_BOOL_READONLY(group, Name, field) \ + bool Is##Name() const { \ + return (group).field.get(m_configMode); \ + } + + // General settings + SETTING_FORWARD(m_general, VolumeSlider, volume_slider) + SETTING_FORWARD_BOOL(m_general, Neo, neo_mode) + SETTING_FORWARD_BOOL(m_general, DevKit, dev_kit_mode) + SETTING_FORWARD(m_general, ExtraDmemInMBytes, extra_dmem_in_mbytes) + SETTING_FORWARD_BOOL(m_general, PSNSignedIn, psn_signed_in) + SETTING_FORWARD_BOOL(m_general, TrophyPopupDisabled, trophy_popup_disabled) + SETTING_FORWARD(m_general, TrophyNotificationDuration, trophy_notification_duration) + SETTING_FORWARD(m_general, TrophyNotificationSide, trophy_notification_side) + SETTING_FORWARD_BOOL(m_general, ShowSplash, show_splash) + SETTING_FORWARD_BOOL(m_general, IdenticalLogGrouped, identical_log_grouped) + SETTING_FORWARD(m_general, AddonInstallDir, addon_install_dir) + SETTING_FORWARD(m_general, LogFilter, log_filter) + SETTING_FORWARD(m_general, LogType, log_type) + SETTING_FORWARD_BOOL(m_general, ConnectedToNetwork, connected_to_network) + SETTING_FORWARD_BOOL(m_general, DiscordRPCEnabled, discord_rpc_enabled) + SETTING_FORWARD_BOOL(m_general, ShowFpsCounter, show_fps_counter) + SETTING_FORWARD(m_general, ConsoleLanguage, console_language) + + // Audio settings + SETTING_FORWARD(m_audio, AudioBackend, audio_backend) + SETTING_FORWARD(m_audio, SDLMicDevice, sdl_mic_device) + SETTING_FORWARD(m_audio, SDLMainOutputDevice, sdl_main_output_device) + SETTING_FORWARD(m_audio, SDLPadSpkOutputDevice, sdl_padSpk_output_device) + SETTING_FORWARD(m_audio, OpenALMicDevice, openal_mic_device) + SETTING_FORWARD(m_audio, OpenALMainOutputDevice, openal_main_output_device) + SETTING_FORWARD(m_audio, OpenALPadSpkOutputDevice, openal_padSpk_output_device) + + // Debug settings + SETTING_FORWARD_BOOL(m_debug, SeparateLoggingEnabled, separate_logging_enabled) + SETTING_FORWARD_BOOL(m_debug, DebugDump, debug_dump) + SETTING_FORWARD_BOOL(m_debug, ShaderCollect, shader_collect) + SETTING_FORWARD_BOOL(m_debug, LogEnabled, log_enabled) + SETTING_FORWARD(m_debug, ConfigVersion, config_version) + + // GPU Settings + SETTING_FORWARD_BOOL(m_gpu, NullGPU, null_gpu) + SETTING_FORWARD_BOOL(m_gpu, DumpShaders, dump_shaders) + SETTING_FORWARD_BOOL(m_gpu, CopyGpuBuffers, copy_gpu_buffers) + SETTING_FORWARD_BOOL(m_gpu, FullScreen, full_screen) + SETTING_FORWARD(m_gpu, FullScreenMode, full_screen_mode) + SETTING_FORWARD(m_gpu, PresentMode, present_mode) + SETTING_FORWARD(m_gpu, WindowHeight, window_height) + SETTING_FORWARD(m_gpu, WindowWidth, window_width) + SETTING_FORWARD(m_gpu, InternalScreenHeight, internal_screen_height) + SETTING_FORWARD(m_gpu, InternalScreenWidth, internal_screen_width) + SETTING_FORWARD_BOOL(m_gpu, HdrAllowed, hdr_allowed) + SETTING_FORWARD_BOOL(m_gpu, FsrEnabled, fsr_enabled) + SETTING_FORWARD_BOOL(m_gpu, RcasEnabled, rcas_enabled) + SETTING_FORWARD(m_gpu, RcasAttenuation, rcas_attenuation) + SETTING_FORWARD(m_gpu, ReadbacksMode, readbacks_mode) + SETTING_FORWARD_BOOL(m_gpu, ReadbackLinearImagesEnabled, readback_linear_images_enabled) + SETTING_FORWARD_BOOL(m_gpu, DirectMemoryAccessEnabled, direct_memory_access_enabled) + SETTING_FORWARD_BOOL_READONLY(m_gpu, PatchShaders, patch_shaders) + + u32 GetVblankFrequency() { + if (m_gpu.vblank_frequency.value < 60) { + m_gpu.vblank_frequency.value = 60; + } + return m_gpu.vblank_frequency.value; + } + void SetVblankFrequency(const u32& v) { + if (v < 60) { + m_gpu.vblank_frequency.value = 60; + } else { + m_gpu.vblank_frequency.value = v; + } + } + + // Input Settings + SETTING_FORWARD(m_input, CursorState, cursor_state) + SETTING_FORWARD(m_input, CursorHideTimeout, cursor_hide_timeout) + SETTING_FORWARD(m_input, UsbDeviceBackend, usb_device_backend) + SETTING_FORWARD_BOOL(m_input, MotionControlsEnabled, motion_controls_enabled) + SETTING_FORWARD_BOOL(m_input, BackgroundControllerInput, background_controller_input) + SETTING_FORWARD(m_input, DefaultControllerId, default_controller_id) + SETTING_FORWARD_BOOL(m_input, UsingSpecialPad, use_special_pad) + SETTING_FORWARD(m_input, SpecialPadClass, special_pad_class) + SETTING_FORWARD_BOOL(m_input, UseUnifiedInputConfig, use_unified_input_config) + SETTING_FORWARD(m_input, CameraId, camera_id) + + // Vulkan settings + SETTING_FORWARD(m_vulkan, GpuId, gpu_id) + SETTING_FORWARD_BOOL(m_vulkan, RenderdocEnabled, renderdoc_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkValidationEnabled, vkvalidation_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkValidationCoreEnabled, vkvalidation_core_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkValidationSyncEnabled, vkvalidation_sync_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkValidationGpuEnabled, vkvalidation_gpu_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkCrashDiagnosticEnabled, vkcrash_diagnostic_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkHostMarkersEnabled, vkhost_markers) + SETTING_FORWARD_BOOL(m_vulkan, VkGuestMarkersEnabled, vkguest_markers) + SETTING_FORWARD_BOOL(m_vulkan, PipelineCacheEnabled, pipeline_cache_enabled) + SETTING_FORWARD_BOOL(m_vulkan, PipelineCacheArchived, pipeline_cache_archived) + +#undef SETTING_FORWARD +#undef SETTING_FORWARD_BOOL +#undef SETTING_FORWARD_BOOL_READONLY +}; diff --git a/src/core/ipc/ipc.cpp b/src/core/ipc/ipc.cpp index aab3e7de5..489c34646 100644 --- a/src/core/ipc/ipc.cpp +++ b/src/core/ipc/ipc.cpp @@ -8,12 +8,12 @@ #include -#include "common/config.h" #include "common/memory_patcher.h" #include "common/thread.h" #include "common/types.h" #include "core/debug_state.h" #include "core/debugger.h" +#include "core/emulator_settings.h" #include "core/emulator_state.h" #include "core/libraries/audio/audioout.h" #include "input/input_handler.h" @@ -153,7 +153,7 @@ void IPC::InputLoop() { } else if (cmd == "ADJUST_VOLUME") { int value = static_cast(next_u64()); bool is_game_specific = next_u64() != 0; - Config::setVolumeSlider(value, is_game_specific); + EmulatorSettings.SetVolumeSlider(value); Libraries::AudioOut::AdjustVol(); } else if (cmd == "SET_FSR") { bool use_fsr = next_u64() != 0; diff --git a/src/core/libraries/app_content/app_content.cpp b/src/core/libraries/app_content/app_content.cpp index a5952c7ea..bf2b72b07 100644 --- a/src/core/libraries/app_content/app_content.cpp +++ b/src/core/libraries/app_content/app_content.cpp @@ -5,9 +5,9 @@ #include "app_content.h" #include "common/assert.h" -#include "common/config.h" #include "common/logging/log.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/file_format/psf.h" #include "core/file_sys/fs.h" #include "core/libraries/app_content/app_content_error.h" @@ -57,7 +57,7 @@ int PS4_SYSV_ABI sceAppContentAddcontMount(u32 service_label, OrbisAppContentMountPoint* mount_point) { LOG_INFO(Lib_AppContent, "called"); - const auto& addon_path = Config::getAddonInstallDir() / title_id; + const auto& addon_path = EmulatorSettings.GetAddonInstallDir() / title_id; auto* mnt = Common::Singleton::Instance(); // Determine which loaded additional content this entitlement label is for. @@ -282,7 +282,7 @@ int PS4_SYSV_ABI sceAppContentInitialize(const OrbisAppContentInitParam* initPar LOG_ERROR(Lib_AppContent, "(DUMMY) called"); auto* param_sfo = Common::Singleton::Instance(); - const auto addons_dir = Config::getAddonInstallDir(); + const auto addons_dir = EmulatorSettings.GetAddonInstallDir(); if (const auto value = param_sfo->GetString("TITLE_ID"); value.has_value()) { title_id = *value; } else { diff --git a/src/core/libraries/audio/sdl_audio_in.cpp b/src/core/libraries/audio/sdl_audio_in.cpp index d36811175..6e7a7bdbd 100644 --- a/src/core/libraries/audio/sdl_audio_in.cpp +++ b/src/core/libraries/audio/sdl_audio_in.cpp @@ -3,8 +3,8 @@ #include #include -#include #include +#include #include "audioin.h" #include "audioin_backend.h" @@ -21,7 +21,7 @@ public: fmt.channels = static_cast(port.channels_num); fmt.freq = static_cast(port.freq); - std::string micDevStr = Config::getMicDevice(); + std::string micDevStr = EmulatorSettings.GetSDLMicDevice(); uint32_t devId = 0; if (micDevStr == "None") { nullDevice = true; diff --git a/src/core/libraries/audio/sdl_audio_out.cpp b/src/core/libraries/audio/sdl_audio_out.cpp index ce2598759..b6706eff7 100644 --- a/src/core/libraries/audio/sdl_audio_out.cpp +++ b/src/core/libraries/audio/sdl_audio_out.cpp @@ -9,8 +9,8 @@ #include #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/audio/audioout.h" #include "core/libraries/audio/audioout_backend.h" #include "core/libraries/kernel/threads.h" @@ -110,7 +110,7 @@ public: max_channel_gain = std::max(max_channel_gain, channel_gain); } - const float slider_gain = Config::getVolumeSlider() * 0.01f; // Faster than /100.0f + const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; // Faster than /100.0f const float total_gain = max_channel_gain * slider_gain; const float current = current_gain.load(std::memory_order_acquire); @@ -156,7 +156,7 @@ private: } // Initialize current gain - current_gain.store(Config::getVolumeSlider() * 0.01f, std::memory_order_relaxed); + current_gain.store(EmulatorSettings.GetVolumeSlider() * 0.01f, std::memory_order_relaxed); if (!SelectConverter()) { FreeAlignedBuffer(); @@ -201,7 +201,7 @@ private: last_volume_check_time = current_time; - const float config_volume = Config::getVolumeSlider() * 0.01f; + const float config_volume = EmulatorSettings.GetVolumeSlider() * 0.01f; const float stored_gain = current_gain.load(std::memory_order_acquire); // Only update if the difference is significant @@ -368,11 +368,11 @@ private: switch (type) { case OrbisAudioOutPort::Main: case OrbisAudioOutPort::Bgm: - return Config::getMainOutputDevice(); + return EmulatorSettings.GetSDLMainOutputDevice(); case OrbisAudioOutPort::PadSpk: - return Config::getPadSpkOutputDevice(); + return EmulatorSettings.GetSDLPadSpkOutputDevice(); default: - return Config::getMainOutputDevice(); + return EmulatorSettings.GetSDLMainOutputDevice(); } } diff --git a/src/core/libraries/gnmdriver/gnmdriver.cpp b/src/core/libraries/gnmdriver/gnmdriver.cpp index 1993d8cd7..1bb034dd0 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.cpp +++ b/src/core/libraries/gnmdriver/gnmdriver.cpp @@ -1,17 +1,17 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "gnm_error.h" #include "gnmdriver.h" #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/elf_info.h" #include "common/logging/log.h" #include "common/slot_vector.h" #include "core/address_space.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/libraries/gnmdriver/gnm_error.h" #include "core/libraries/gnmdriver/gnmdriver_init.h" #include "core/libraries/kernel/orbis_error.h" @@ -2874,7 +2874,7 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { sdk_version = 0; } - if (Config::copyGPUCmdBuffers()) { + if (EmulatorSettings.IsCopyGpuBuffers()) { liverpool->ReserveCopyBufferSpace(); } diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index a79da62ee..2af5aa1bf 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/elf_info.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/file_sys/fs.h" #include "core/libraries/kernel/orbis_error.h" #include "core/libraries/kernel/process.h" @@ -17,19 +17,19 @@ s32 PS4_SYSV_ABI sceKernelIsInSandbox() { } s32 PS4_SYSV_ABI sceKernelIsNeoMode() { - return Config::isNeoModeConsole() && + return EmulatorSettings.IsNeo() && Common::ElfInfo::Instance().GetPSFAttributes().support_neo_mode; } s32 PS4_SYSV_ABI sceKernelHasNeoMode() { - return Config::isNeoModeConsole(); + return EmulatorSettings.IsNeo(); } s32 PS4_SYSV_ABI sceKernelGetMainSocId() { // These hardcoded values are based on hardware observations. // Different models of PS4/PS4 Pro likely return slightly different values. LOG_DEBUG(Lib_Kernel, "called"); - if (Config::isNeoModeConsole()) { + if (EmulatorSettings.IsNeo()) { return 0x740f30; } return 0x710f10; diff --git a/src/core/libraries/network/net.cpp b/src/core/libraries/network/net.cpp index ca75ad394..6bf4764c4 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -1447,7 +1447,7 @@ int PS4_SYSV_ABI sceNetResolverStartNtoa(OrbisNetId resolverid, const char* host return ORBIS_NET_ERROR_EBADF; } - if (!Config::getIsConnectedToNetwork()) { + if (!EmulatorSettings.IsConnectedToNetwork()) { *sceNetErrnoLoc() = ORBIS_NET_RESOLVER_ENODNS; file->resolver->resolution_error = ORBIS_NET_ERROR_RESOLVER_ENODNS; return ORBIS_NET_ERROR_RESOLVER_ENODNS; diff --git a/src/core/libraries/network/net_ctl_obj.cpp b/src/core/libraries/network/net_ctl_obj.cpp index a4081cd11..5eb6403c2 100644 --- a/src/core/libraries/network/net_ctl_obj.cpp +++ b/src/core/libraries/network/net_ctl_obj.cpp @@ -1,9 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/network/net_ctl_codes.h" #include "core/libraries/network/net_ctl_obj.h" #include "core/tls.h" @@ -46,8 +46,9 @@ s32 NetCtlInternal::RegisterNpToolkitCallback(OrbisNetCtlCallbackForNpToolkit fu void NetCtlInternal::CheckCallback() { std::scoped_lock lock{m_mutex}; - const auto event = Config::getIsConnectedToNetwork() ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED - : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; + const auto event = EmulatorSettings.IsConnectedToNetwork() + ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED + : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; for (const auto [func, arg] : callbacks) { if (func != nullptr) { func(event, arg); @@ -57,8 +58,9 @@ void NetCtlInternal::CheckCallback() { void NetCtlInternal::CheckNpToolkitCallback() { std::scoped_lock lock{m_mutex}; - const auto event = Config::getIsConnectedToNetwork() ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED - : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; + const auto event = EmulatorSettings.IsConnectedToNetwork() + ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED + : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; for (const auto [func, arg] : nptool_callbacks) { if (func != nullptr) { func(event, arg); diff --git a/src/core/libraries/network/netctl.cpp b/src/core/libraries/network/netctl.cpp index 8d60d3627..136d63810 100644 --- a/src/core/libraries/network/netctl.cpp +++ b/src/core/libraries/network/netctl.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #ifdef WIN32 @@ -13,8 +13,8 @@ #endif #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/network/net_ctl_codes.h" @@ -162,7 +162,7 @@ int PS4_SYSV_ABI sceNetCtlGetIfStat() { int PS4_SYSV_ABI sceNetCtlGetInfo(int code, OrbisNetCtlInfo* info) { LOG_DEBUG(Lib_NetCtl, "code = {}", code); - if (!Config::getIsConnectedToNetwork()) { + if (!EmulatorSettings.IsConnectedToNetwork()) { return ORBIS_NET_CTL_ERROR_NOT_CONNECTED; } @@ -180,8 +180,8 @@ int PS4_SYSV_ABI sceNetCtlGetInfo(int code, OrbisNetCtlInfo* info) { info->mtu = 1500; // default value break; case ORBIS_NET_CTL_INFO_LINK: - info->link = Config::getIsConnectedToNetwork() ? ORBIS_NET_CTL_LINK_CONNECTED - : ORBIS_NET_CTL_LINK_DISCONNECTED; + info->link = EmulatorSettings.IsConnectedToNetwork() ? ORBIS_NET_CTL_LINK_CONNECTED + : ORBIS_NET_CTL_LINK_DISCONNECTED; break; case ORBIS_NET_CTL_INFO_IP_ADDRESS: { strcpy(info->ip_address, @@ -318,7 +318,7 @@ int PS4_SYSV_ABI sceNetCtlGetScanInfoForSsidScanIpcInt() { } int PS4_SYSV_ABI sceNetCtlGetState(int* state) { - const auto connected = Config::getIsConnectedToNetwork(); + const auto connected = EmulatorSettings.IsConnectedToNetwork(); LOG_DEBUG(Lib_NetCtl, "connected = {}", connected); const auto current_state = connected ? ORBIS_NET_CTL_STATE_IPOBTAINED : ORBIS_NET_CTL_STATE_DISCONNECTED; diff --git a/src/core/libraries/np/np_auth.cpp b/src/core/libraries/np/np_auth.cpp index b6091723c..a9c2181b9 100644 --- a/src/core/libraries/np/np_auth.cpp +++ b/src/core/libraries/np/np_auth.cpp @@ -1,9 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_auth.h" @@ -363,7 +363,7 @@ s32 PS4_SYSV_ABI sceNpAuthDeleteRequest(s32 req_id) { } void RegisterLib(Core::Loader::SymbolsResolver* sym) { - g_signed_in = Config::getPSNSignedIn(); + g_signed_in = EmulatorSettings.IsPSNSignedIn(); LIB_FUNCTION("6bwFkosYRQg", "libSceNpAuth", 1, "libSceNpAuth", sceNpAuthCreateRequest); LIB_FUNCTION("N+mr7GjTvr8", "libSceNpAuth", 1, "libSceNpAuth", sceNpAuthCreateAsyncRequest); diff --git a/src/core/libraries/np/np_matching2.cpp b/src/core/libraries/np/np_matching2.cpp index 423b84257..bf9b7b7d0 100644 --- a/src/core/libraries/np/np_matching2.cpp +++ b/src/core/libraries/np/np_matching2.cpp @@ -5,7 +5,7 @@ #include #include "common/config.h" -#include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_manager.h" @@ -376,7 +376,7 @@ int PS4_SYSV_ABI sceNpMatching2ContextStart(OrbisNpMatching2ContextId ctxId, u64 } std::scoped_lock lk{g_events_mutex}; - if (Config::getIsConnectedToNetwork() && Config::getPSNSignedIn()) { + if (EmulatorSettings.IsConnectedToNetwork() && EmulatorSettings.IsPSNSignedIn()) { g_ctx_events.emplace_back(ctxId, ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED, ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ACTION, 0); } else { diff --git a/src/core/libraries/np/np_web_api2.cpp b/src/core/libraries/np/np_web_api2.cpp index c03636e73..a7c7ee3f3 100644 --- a/src/core/libraries/np/np_web_api2.cpp +++ b/src/core/libraries/np/np_web_api2.cpp @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_web_api2.h" @@ -115,10 +115,10 @@ s32 PS4_SYSV_ABI sceNpWebApi2IntInitialize2(const OrbisNpWebApi2IntInitialize2Ar if (args == nullptr || args->struct_size != sizeof(OrbisNpWebApi2IntInitialize2Args)) { return ORBIS_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; } - LOG_ERROR( - Lib_NpWebApi2, - "(STUBBED) called, lib_http_ctx_id = {:#x}, pool_size = {:#x}, name = '{}', group = {:#x}", - args->lib_http_ctx_id, args->pool_size, args->name, args->push_config_group); + LOG_ERROR(Lib_NpWebApi2, + "(STUBBED) called, lib_http_ctx_id = {:#x}, pool_size = {:#x}, name = '{}', " + "group = {:#x}", + args->lib_http_ctx_id, args->pool_size, args->name, args->push_config_group); return ORBIS_OK; } @@ -207,7 +207,7 @@ s32 PS4_SYSV_ABI sceNpWebApi2SendMultipartRequest() { } s32 PS4_SYSV_ABI sceNpWebApi2SendRequest() { - if (!Config::getPSNSignedIn()) { + if (!EmulatorSettings.IsPSNSignedIn()) { LOG_INFO(Lib_NpWebApi2, "called, returning PSN signed out."); return ORBIS_NP_WEBAPI2_ERROR_NOT_SIGNED_IN; } diff --git a/src/core/libraries/np/np_web_api_internal.cpp b/src/core/libraries/np/np_web_api_internal.cpp index f598344c7..66a09b493 100644 --- a/src/core/libraries/np/np_web_api_internal.cpp +++ b/src/core/libraries/np/np_web_api_internal.cpp @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/elf_info.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/process.h" #include "core/libraries/kernel/time.h" #include "core/libraries/network/http.h" @@ -606,7 +606,7 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s unlockContext(context); // Stubbing sceNpManagerIntGetSigninState call with a config check. - if (!Config::getPSNSignedIn()) { + if (!EmulatorSettings.IsPSNSignedIn()) { releaseRequest(request); releaseUserContext(user_context); releaseContext(context); @@ -1025,7 +1025,7 @@ s32 createServicePushEventFilterInternal( auto& handle = context->handles[handleId]; handle->userCount++; - if (pNpServiceName != nullptr && !Config::getPSNSignedIn()) { + if (pNpServiceName != nullptr && !EmulatorSettings.IsPSNSignedIn()) { // Seems sceNpManagerIntGetUserList fails? LOG_DEBUG(Lib_NpWebApi, "Cannot create service push event while PSN is disabled"); handle->userCount--; @@ -1202,7 +1202,7 @@ s32 createExtendedPushEventFilterInternal( auto& handle = context->handles[handleId]; handle->userCount++; - if (pNpServiceName != nullptr && !Config::getPSNSignedIn()) { + if (pNpServiceName != nullptr && !EmulatorSettings.IsPSNSignedIn()) { // Seems sceNpManagerIntGetUserList fails? LOG_DEBUG(Lib_NpWebApi, "Cannot create extended push event while PSN is disabled"); handle->userCount--; diff --git a/src/core/libraries/np/trophy_ui.cpp b/src/core/libraries/np/trophy_ui.cpp index b803403c4..6ca24b6d5 100644 --- a/src/core/libraries/np/trophy_ui.cpp +++ b/src/core/libraries/np/trophy_ui.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -13,9 +13,9 @@ #endif #include "common/assert.h" -#include "common/config.h" #include "common/path_util.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/libraries/np/trophy_ui.h" #include "imgui/imgui_std.h" @@ -36,9 +36,9 @@ TrophyUI::TrophyUI(const std::filesystem::path& trophyIconPath, const std::strin const std::string_view& rarity) : trophy_name(trophyName), trophy_type(rarity) { - side = Config::sideTrophy(); + side = EmulatorSettings.GetTrophyNotificationSide(); - trophy_timer = Config::getTrophyNotificationDuration(); + trophy_timer = EmulatorSettings.GetTrophyNotificationDuration(); if (std::filesystem::exists(trophyIconPath)) { trophy_icon = RefCountedTexture::DecodePngFile(trophyIconPath); @@ -98,7 +98,7 @@ TrophyUI::TrophyUI(const std::filesystem::path& trophyIconPath, const std::strin return; } - MIX_SetMasterGain(mixer, static_cast(Config::getVolumeSlider() / 100.f)); + MIX_SetMasterGain(mixer, static_cast(EmulatorSettings.GetVolumeSlider() / 100.f)); auto musicPathMp3 = CustomTrophy_Dir / "trophy.mp3"; auto musicPathWav = CustomTrophy_Dir / "trophy.wav"; @@ -284,7 +284,7 @@ void AddTrophyToQueue(const std::filesystem::path& trophyIconPath, const std::st const std::string_view& rarity) { std::lock_guard lock(queueMtx); - if (Config::getisTrophyPopupDisabled()) { + if (EmulatorSettings.IsTrophyPopupDisabled()) { return; } else if (current_trophy_ui.has_value()) { current_trophy_ui.reset(); diff --git a/src/core/libraries/system/systemservice.cpp b/src/core/libraries/system/systemservice.cpp index ce5542fc8..f03c7e7cb 100644 --- a/src/core/libraries/system/systemservice.cpp +++ b/src/core/libraries/system/systemservice.cpp @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/config.h" -#include "common/logging/log.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/file_sys/fs.h" #include "core/libraries/libs.h" #include "core/libraries/system/systemservice.h" @@ -18,7 +18,7 @@ std::queue g_event_queue; std::mutex g_event_queue_mutex; bool IsSplashVisible() { - return Config::showSplash() && g_splash_status; + return EmulatorSettings.IsShowSplash() && g_splash_status; } int PS4_SYSV_ABI sceAppMessagingClearEventFlag() { @@ -1918,7 +1918,7 @@ s32 PS4_SYSV_ABI sceSystemServiceParamGetInt(OrbisSystemServiceParamId param_id, } switch (param_id) { case OrbisSystemServiceParamId::Lang: - *value = Config::GetLanguage(); + *value = EmulatorSettings.GetConsoleLanguage(); break; case OrbisSystemServiceParamId::DateFormat: *value = u32(OrbisSystemParamDateFormat::FmtDDMMYYYY); diff --git a/src/core/libraries/usbd/usbd.cpp b/src/core/libraries/usbd/usbd.cpp index 0708c3dd7..52d6aec66 100644 --- a/src/core/libraries/usbd/usbd.cpp +++ b/src/core/libraries/usbd/usbd.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/logging/log.h" @@ -9,7 +9,7 @@ #include #include -#include "common/config.h" +#include "core/emulator_settings.h" namespace Libraries::Usbd { @@ -457,14 +457,14 @@ int PS4_SYSV_ABI Func_D56B43060720B1E0() { } void RegisterLib(Core::Loader::SymbolsResolver* sym) { - switch (Config::getUsbDeviceBackend()) { - case Config::SkylandersPortal: + switch (EmulatorSettings.GetUsbDeviceBackend()) { + case UsbBackendType::SkylandersPortal: usb_backend = std::make_shared(); break; - case Config::InfinityBase: + case UsbBackendType::InfinityBase: usb_backend = std::make_shared(); break; - case Config::DimensionsToypad: + case UsbBackendType::DimensionsToypad: usb_backend = std::make_shared(); break; default: diff --git a/src/core/libraries/videoout/driver.cpp b/src/core/libraries/videoout/driver.cpp index bebbf9602..9db70569b 100644 --- a/src/core/libraries/videoout/driver.cpp +++ b/src/core/libraries/videoout/driver.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/thread.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/time.h" #include "core/libraries/videoout/driver.h" #include "core/libraries/videoout/videoout_error.h" @@ -268,7 +268,8 @@ void VideoOutDriver::SubmitFlipInternal(VideoOutPort* port, s32 index, s64 flip_ } void VideoOutDriver::PresentThread(std::stop_token token) { - const std::chrono::nanoseconds vblank_period(1000000000 / Config::vblankFreq()); + const std::chrono::nanoseconds vblank_period(1000000000 / + EmulatorSettings.GetVblankFrequency()); Common::SetCurrentThreadName("shadPS4:PresentThread"); Common::SetCurrentThreadRealtime(vblank_period); diff --git a/src/core/libraries/videoout/video_out.cpp b/src/core/libraries/videoout/video_out.cpp index 1b8a6b59d..7714eb2b5 100644 --- a/src/core/libraries/videoout/video_out.cpp +++ b/src/core/libraries/videoout/video_out.cpp @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/libs.h" #include "core/libraries/system/userservice.h" #include "core/libraries/videoout/driver.h" @@ -455,8 +455,8 @@ s32 PS4_SYSV_ABI sceVideoOutSetWindowModeMargins(s32 handle, s32 top, s32 bottom } void RegisterLib(Core::Loader::SymbolsResolver* sym) { - driver = std::make_unique(Config::getInternalScreenWidth(), - Config::getInternalScreenHeight()); + driver = std::make_unique(EmulatorSettings.GetInternalScreenWidth(), + EmulatorSettings.GetInternalScreenHeight()); LIB_FUNCTION("SbU3dwp80lQ", "libSceVideoOut", 1, "libSceVideoOut", sceVideoOutGetFlipStatus); LIB_FUNCTION("U46NwOiJpys", "libSceVideoOut", 1, "libSceVideoOut", sceVideoOutSubmitFlip); diff --git a/src/core/linker.cpp b/src/core/linker.cpp index 3f410e926..889f3a298 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -4,7 +4,6 @@ #include "common/alignment.h" #include "common/arch.h" #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/logging/log.h" #include "common/path_util.h" @@ -13,6 +12,7 @@ #include "core/aerolib/aerolib.h" #include "core/aerolib/stubs.h" #include "core/devtools/widget/module_list.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/threads.h" @@ -61,7 +61,7 @@ Linker::Linker() : memory{Memory::Instance()} {} Linker::~Linker() = default; void Linker::Execute(const std::vector& args) { - if (Config::debugDump()) { + if (EmulatorSettings.IsDebugDump()) { DebugDump(); } diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 768fb3c90..a340c3643 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -3,9 +3,9 @@ #include "common/alignment.h" #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/elf_info.h" +#include "core/emulator_settings.h" #include "core/file_sys/fs.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/orbis_error.h" @@ -37,11 +37,11 @@ void MemoryManager::SetupMemoryRegions(u64 flexible_size, bool use_extended_mem1 bool use_extended_mem2) { const bool is_neo = ::Libraries::Kernel::sceKernelIsNeoMode(); auto total_size = is_neo ? ORBIS_KERNEL_TOTAL_MEM_PRO : ORBIS_KERNEL_TOTAL_MEM; - if (Config::isDevKitConsole()) { + if (EmulatorSettings.IsDevKit()) { total_size = is_neo ? ORBIS_KERNEL_TOTAL_MEM_DEV_PRO : ORBIS_KERNEL_TOTAL_MEM_DEV; } - s32 extra_dmem = Config::getExtraDmemInMbytes(); - if (Config::getExtraDmemInMbytes() != 0) { + s32 extra_dmem = EmulatorSettings.GetExtraDmemInMBytes(); + if (extra_dmem != 0) { LOG_WARNING(Kernel_Vmm, "extraDmemInMbytes is {} MB! Old Direct Size: {:#x} -> New Direct Size: {:#x}", extra_dmem, total_size, total_size + extra_dmem * 1_MB); diff --git a/src/core/module.h b/src/core/module.h index 778344e33..8dde0f467 100644 --- a/src/core/module.h +++ b/src/core/module.h @@ -1,12 +1,12 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include -#include "common/config.h" #include "common/types.h" +#include "core/emulator_settings.h" #include "core/loader/elf.h" #include "core/loader/symbols_resolver.h" @@ -166,7 +166,7 @@ public: } bool IsSystemLib() { - auto system_path = Config::getSysModulesPath(); + auto system_path = EmulatorSettings.GetSysModulesDir(); if (file.string().starts_with(system_path.string().c_str())) { return true; } diff --git a/src/core/user_manager.cpp b/src/core/user_manager.cpp new file mode 100644 index 000000000..9d0829fc3 --- /dev/null +++ b/src/core/user_manager.cpp @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include "emulator_settings.h" +#include "libraries/system/userservice.h" +#include "user_manager.h" +#include "user_settings.h" + +bool UserManager::AddUser(const User& user) { + for (const auto& u : m_users.user) { + if (u.user_id == user.user_id) + return false; // already exists + } + + m_users.user.push_back(user); + + // Create user home directory and subfolders + const auto user_dir = EmulatorSettings.GetHomeDir() / std::to_string(user.user_id); + + std::error_code ec; + if (!std::filesystem::exists(user_dir)) { + std::filesystem::create_directory(user_dir, ec); + std::filesystem::create_directory(user_dir / "savedata", ec); + std::filesystem::create_directory(user_dir / "trophy", ec); + std::filesystem::create_directory(user_dir / "inputs", ec); + } + + Save(); + return true; +} + +bool UserManager::RemoveUser(s32 user_id) { + auto it = std::remove_if(m_users.user.begin(), m_users.user.end(), + [user_id](const User& u) { return u.user_id == user_id; }); + if (it == m_users.user.end()) + return false; // not found + + const auto user_dir = EmulatorSettings.GetHomeDir() / std::to_string(user_id); + + if (std::filesystem::exists(user_dir)) { + std::error_code ec; + std::filesystem::remove_all(user_dir, ec); + } + + m_users.user.erase(it, m_users.user.end()); + Save(); + return true; +} + +bool UserManager::RenameUser(s32 user_id, const std::string& new_name) { + // Find user in the internal list + for (auto& user : m_users.user) { + if (user.user_id == user_id) { + if (user.user_name == new_name) + return true; // no change + + user.user_name = new_name; + return true; + } + } + Save(); + return false; +} + +User* UserManager::GetUserByID(s32 user_id) { + for (auto& u : m_users.user) { + if (u.user_id == user_id) + return &u; + } + return nullptr; +} + +User* UserManager::GetUserByPlayerIndex(s32 index) { + for (auto& u : m_users.user) { + if (u.player_index == index) + return &u; + } + return nullptr; +} + +const std::vector& UserManager::GetAllUsers() const { + return m_users.user; +} + +Users UserManager::CreateDefaultUsers() { + Users default_users; + default_users.user = { + { + .user_id = 1000, + .user_name = "shadPS4", + .user_color = 1, + .player_index = 1, + }, + { + .user_id = 1001, + .user_name = "shadPS4-2", + .user_color = 2, + .player_index = 2, + }, + { + .user_id = 1002, + .user_name = "shadPS4-3", + .user_color = 3, + .player_index = 3, + }, + { + .user_id = 1003, + .user_name = "shadPS4-4", + .user_color = 4, + .player_index = 4, + }, + }; + + for (auto& u : default_users.user) { + const auto user_dir = EmulatorSettings.GetHomeDir() / std::to_string(u.user_id); + + if (!std::filesystem::exists(user_dir)) { + std::filesystem::create_directory(user_dir); + std::filesystem::create_directory(user_dir / "savedata"); + std::filesystem::create_directory(user_dir / "trophy"); + std::filesystem::create_directory(user_dir / "inputs"); + } + } + + return default_users; +} + +bool UserManager::SetDefaultUser(u32 user_id) { + auto it = std::find_if(m_users.user.begin(), m_users.user.end(), + [user_id](const User& u) { return u.user_id == user_id; }); + if (it == m_users.user.end()) + return false; + + SetControllerPort(user_id, 1); // Set default user to port 1 + return Save(); +} + +User UserManager::GetDefaultUser() { + return *GetUserByPlayerIndex(1); +} + +void UserManager::SetControllerPort(u32 user_id, int port) { + for (auto& u : m_users.user) { + if (u.user_id != user_id && u.player_index == port) + u.player_index = -1; + if (u.user_id == user_id) + u.player_index = port; + } + Save(); +} +// Returns a list of users that have valid home directories +std::vector UserManager::GetValidUsers() const { + std::vector result; + result.reserve(m_users.user.size()); + + const auto home_dir = EmulatorSettings.GetHomeDir(); + + for (const auto& user : m_users.user) { + const auto user_dir = home_dir / std::to_string(user.user_id); + if (std::filesystem::exists(user_dir)) { + result.push_back(user); + } + } + + return result; +} + +LoggedInUsers UserManager::GetLoggedInUsers() const { + return logged_in_users; +} + +bool UserManager::Save() const { + return UserSettings.Save(); +} \ No newline at end of file diff --git a/src/core/user_manager.h b/src/core/user_manager.h new file mode 100644 index 000000000..77f612016 --- /dev/null +++ b/src/core/user_manager.h @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#include +#include +#include "common/types.h" + +struct User { + s32 user_id = -1; + std::string user_name = ""; + u32 user_color; + int player_index = 0; // 1-4 + + bool logged_in = false; +}; + +struct Users { + std::vector user{}; + std::string commit_hash{}; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, user_id, user_color, user_name, player_index) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Users, user, commit_hash) + +using LoggedInUsers = std::array; + +class UserManager { +public: + UserManager() = default; + + bool AddUser(const User& user); + bool RemoveUser(s32 user_id); + bool RenameUser(s32 user_id, const std::string& new_name); + User* GetUserByID(s32 user_id); + User* GetUserByPlayerIndex(s32 index); + const std::vector& GetAllUsers() const; + Users CreateDefaultUsers(); + bool SetDefaultUser(u32 user_id); + User GetDefaultUser(); + void SetControllerPort(u32 user_id, int port); + std::vector GetValidUsers() const; + LoggedInUsers GetLoggedInUsers() const; + + Users& GetUsers() { + return m_users; + } + const Users& GetUsers() const { + return m_users; + } + + bool Save() const; + +private: + Users m_users; + LoggedInUsers logged_in_users{}; +}; diff --git a/src/core/user_settings.cpp b/src/core/user_settings.cpp new file mode 100644 index 000000000..a2142dd9a --- /dev/null +++ b/src/core/user_settings.cpp @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "user_settings.h" + +using json = nlohmann::json; + +// Singleton storage +std::shared_ptr UserSettingsImpl::s_instance = nullptr; +std::mutex UserSettingsImpl::s_mutex; + +// Singleton +UserSettingsImpl::UserSettingsImpl() = default; + +UserSettingsImpl::~UserSettingsImpl() { + Save(); +} + +std::shared_ptr UserSettingsImpl::GetInstance() { + std::lock_guard lock(s_mutex); + if (!s_instance) + s_instance = std::make_shared(); + return s_instance; +} + +void UserSettingsImpl::SetInstance(std::shared_ptr instance) { + std::lock_guard lock(s_mutex); + s_instance = std::move(instance); +} + +bool UserSettingsImpl::Save() const { + const auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "users.json"; + try { + json j; + j["Users"] = m_userManager.GetUsers(); + j["Users"]["commit_hash"] = std::string(Common::g_scm_rev); + + std::ofstream out(path); + if (!out) { + LOG_ERROR(EmuSettings, "Failed to open user settings for writing: {}", path.string()); + return false; + } + out << std::setw(2) << j; + return !out.fail(); + } catch (const std::exception& e) { + LOG_ERROR(EmuSettings, "Error saving user settings: {}", e.what()); + return false; + } +} + +bool UserSettingsImpl::Load() { + const auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "users.json"; + try { + if (!std::filesystem::exists(path)) { + LOG_DEBUG(EmuSettings, "User settings file not found: {}", path.string()); + // Create default user if no file exists + if (m_userManager.GetUsers().user.empty()) { + m_userManager.GetUsers() = m_userManager.CreateDefaultUsers(); + } + Save(); // Save default users + return false; + } + + std::ifstream in(path); + if (!in) { + LOG_ERROR(EmuSettings, "Failed to open user settings: {}", path.string()); + return false; + } + + json j; + in >> j; + + // Create a default Users object + auto default_users = m_userManager.CreateDefaultUsers(); + + // Convert default_users to json for merging + json default_json; + default_json["Users"] = default_users; + + // Merge the loaded json with defaults (preserves existing data, adds missing fields) + if (j.contains("Users")) { + json current = default_json["Users"]; + current.update(j["Users"]); + m_userManager.GetUsers() = current.get(); + } else { + m_userManager.GetUsers() = default_users; + } + + if (m_userManager.GetUsers().commit_hash != Common::g_scm_rev) { + Save(); + } + + LOG_DEBUG(EmuSettings, "User settings loaded successfully"); + return true; + } catch (const std::exception& e) { + LOG_ERROR(EmuSettings, "Error loading user settings: {}", e.what()); + // Fall back to defaults + if (m_userManager.GetUsers().user.empty()) { + m_userManager.GetUsers() = m_userManager.CreateDefaultUsers(); + } + return false; + } +} \ No newline at end of file diff --git a/src/core/user_settings.h b/src/core/user_settings.h new file mode 100644 index 000000000..45cfa91dd --- /dev/null +++ b/src/core/user_settings.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "common/types.h" +#include "core/user_manager.h" + +#define UserSettings (*UserSettingsImpl::GetInstance()) + +#define UserManagement UserSettings.GetUserManager() + +// ------------------------------- +// User settings +// ------------------------------- + +class UserSettingsImpl { +public: + UserSettingsImpl(); + ~UserSettingsImpl(); + + UserManager& GetUserManager() { + return m_userManager; + } + + bool Save() const; + bool Load(); + + static std::shared_ptr GetInstance(); + static void SetInstance(std::shared_ptr instance); + +private: + UserManager m_userManager; + + static std::shared_ptr s_instance; + static std::mutex s_mutex; +}; \ No newline at end of file diff --git a/src/emulator.cpp b/src/emulator.cpp index cc862f8ab..5206a309d 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -10,11 +10,11 @@ #include #include -#include "common/config.h" #include "common/debug.h" #include "common/logging/backend.h" #include "common/logging/log.h" #include "common/thread.h" +#include "core/emulator_settings.h" #include "core/ipc/ipc.h" #ifdef ENABLE_DISCORD_RPC #include "common/discord_rpc_handler.h" @@ -197,18 +197,16 @@ void Emulator::Run(std::filesystem::path file, std::vector args, game_info.game_folder = game_folder; - Config::load(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".toml"), - true); - + EmulatorSettings.Load(id); if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / - (id + ".toml"))) { + (id + ".json"))) { EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(true); } else { EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(false); } // Initialize logging as soon as possible - if (!id.empty() && Config::getSeparateLogFilesEnabled()) { + if (!id.empty() && EmulatorSettings.IsSeparateLoggingEnabled()) { Common::Log::Initialize(id + ".log"); } else { Common::Log::Initialize(); @@ -227,31 +225,35 @@ void Emulator::Run(std::filesystem::path file, std::vector args, LOG_INFO(Loader, "Remote {}", Common::g_scm_remote_url); const bool has_game_config = std::filesystem::exists( - Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".toml")); + Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".json")); LOG_INFO(Config, "Game-specific config exists: {}", has_game_config); - LOG_INFO(Config, "General LogType: {}", Config::getLogType()); - LOG_INFO(Config, "General isIdenticalLogGrouped: {}", Config::groupIdenticalLogs()); - LOG_INFO(Config, "General isNeo: {}", Config::isNeoModeConsole()); - LOG_INFO(Config, "General isDevKit: {}", Config::isDevKitConsole()); - LOG_INFO(Config, "General isConnectedToNetwork: {}", Config::getIsConnectedToNetwork()); - LOG_INFO(Config, "General isPsnSignedIn: {}", Config::getPSNSignedIn()); - LOG_INFO(Config, "GPU isNullGpu: {}", Config::nullGpu()); - LOG_INFO(Config, "GPU readbacksMode: {}", Config::getReadbacksMode()); - LOG_INFO(Config, "GPU readbackLinearImages: {}", Config::readbackLinearImages()); - LOG_INFO(Config, "GPU directMemoryAccess: {}", Config::directMemoryAccess()); - LOG_INFO(Config, "GPU shouldDumpShaders: {}", Config::dumpShaders()); - LOG_INFO(Config, "GPU vblankFrequency: {}", Config::vblankFreq()); - LOG_INFO(Config, "GPU shouldCopyGPUBuffers: {}", Config::copyGPUCmdBuffers()); - LOG_INFO(Config, "Vulkan gpuId: {}", Config::getGpuId()); - LOG_INFO(Config, "Vulkan vkValidation: {}", Config::vkValidationEnabled()); - LOG_INFO(Config, "Vulkan vkValidationCore: {}", Config::vkValidationCoreEnabled()); - LOG_INFO(Config, "Vulkan vkValidationSync: {}", Config::vkValidationSyncEnabled()); - LOG_INFO(Config, "Vulkan vkValidationGpu: {}", Config::vkValidationGpuEnabled()); - LOG_INFO(Config, "Vulkan crashDiagnostics: {}", Config::getVkCrashDiagnosticEnabled()); - LOG_INFO(Config, "Vulkan hostMarkers: {}", Config::getVkHostMarkersEnabled()); - LOG_INFO(Config, "Vulkan guestMarkers: {}", Config::getVkGuestMarkersEnabled()); - LOG_INFO(Config, "Vulkan rdocEnable: {}", Config::isRdocEnabled()); + LOG_INFO(Config, "General LogType: {}", EmulatorSettings.GetLogType()); + LOG_INFO(Config, "General isIdenticalLogGrouped: {}", EmulatorSettings.IsIdenticalLogGrouped()); + LOG_INFO(Config, "General isNeo: {}", EmulatorSettings.IsNeo()); + LOG_INFO(Config, "General isDevKit: {}", EmulatorSettings.IsDevKit()); + LOG_INFO(Config, "General isConnectedToNetwork: {}", EmulatorSettings.IsConnectedToNetwork()); + LOG_INFO(Config, "General isPsnSignedIn: {}", EmulatorSettings.IsPSNSignedIn()); + LOG_INFO(Config, "GPU isNullGpu: {}", EmulatorSettings.IsNullGPU()); + LOG_INFO(Config, "GPU readbacksMode: {}", EmulatorSettings.GetReadbacksMode()); + LOG_INFO(Config, "GPU readbackLinearImages: {}", + EmulatorSettings.IsReadbackLinearImagesEnabled()); + LOG_INFO(Config, "GPU directMemoryAccess: {}", EmulatorSettings.IsDirectMemoryAccessEnabled()); + LOG_INFO(Config, "GPU shouldDumpShaders: {}", EmulatorSettings.IsDumpShaders()); + LOG_INFO(Config, "GPU vblankFrequency: {}", EmulatorSettings.GetVblankFrequency()); + LOG_INFO(Config, "GPU shouldCopyGPUBuffers: {}", EmulatorSettings.IsCopyGpuBuffers()); + LOG_INFO(Config, "Vulkan gpuId: {}", EmulatorSettings.GetGpuId()); + LOG_INFO(Config, "Vulkan vkValidation: {}", EmulatorSettings.IsVkValidationEnabled()); + LOG_INFO(Config, "Vulkan vkValidationCore: {}", EmulatorSettings.IsVkValidationCoreEnabled()); + LOG_INFO(Config, "Vulkan vkValidationSync: {}", EmulatorSettings.IsVkValidationSyncEnabled()); + LOG_INFO(Config, "Vulkan vkValidationGpu: {}", EmulatorSettings.IsVkValidationGpuEnabled()); + LOG_INFO(Config, "Vulkan crashDiagnostics: {}", EmulatorSettings.IsVkCrashDiagnosticEnabled()); + LOG_INFO(Config, "Vulkan hostMarkers: {}", EmulatorSettings.IsVkHostMarkersEnabled()); + LOG_INFO(Config, "Vulkan guestMarkers: {}", EmulatorSettings.IsVkGuestMarkersEnabled()); + LOG_INFO(Config, "Vulkan rdocEnable: {}", EmulatorSettings.IsRenderdocEnabled()); + LOG_INFO(Config, "Vulkan PipelineCacheEnabled: {}", EmulatorSettings.IsPipelineCacheEnabled()); + LOG_INFO(Config, "Vulkan PipelineCacheArchived: {}", + EmulatorSettings.IsPipelineCacheArchived()); hwinfo::Memory ram; hwinfo::OS os; @@ -328,8 +330,9 @@ void Emulator::Run(std::filesystem::path file, std::vector args, Common::g_scm_branch, Common::g_scm_desc, game_title); } } - window = std::make_unique( - Config::getWindowWidth(), Config::getWindowHeight(), controller, window_title); + window = std::make_unique(EmulatorSettings.GetWindowWidth(), + EmulatorSettings.GetWindowHeight(), controller, + window_title); g_window = window.get(); @@ -360,7 +363,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, VideoCore::SetOutputDir(mount_captures_dir, id); // Mount system fonts - const auto& fonts_dir = Config::getFontsPath(); + const auto& fonts_dir = EmulatorSettings.GetFontsDir(); if (!std::filesystem::exists(fonts_dir)) { std::filesystem::create_directory(fonts_dir); } @@ -399,7 +402,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, #ifdef ENABLE_DISCORD_RPC // Discord RPC - if (Config::getEnableDiscordRPC()) { + if (EmulatorSettings.IsDiscordRPCEnabled()) { auto* rpc = Common::Singleton::Instance(); if (rpc->getRPCEnabled() == false) { rpc->init(); diff --git a/src/imgui/renderer/imgui_core.cpp b/src/imgui/renderer/imgui_core.cpp index 452dee013..b52a68d22 100644 --- a/src/imgui/renderer/imgui_core.cpp +++ b/src/imgui/renderer/imgui_core.cpp @@ -1,13 +1,13 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include -#include "common/config.h" #include "common/path_util.h" #include "core/debug_state.h" #include "core/devtools/layer.h" +#include "core/emulator_settings.h" #include "imgui/imgui_layer.h" #include "imgui_core.h" #include "imgui_impl_sdl3.h" @@ -219,7 +219,7 @@ void Render(const vk::CommandBuffer& cmdbuf, const vk::ImageView& image_view, return; } - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{ .pLabelName = "ImGui Render", }); @@ -244,7 +244,7 @@ void Render(const vk::CommandBuffer& cmdbuf, const vk::ImageView& image_view, cmdbuf.beginRendering(render_info); Vulkan::RenderDrawData(*draw_data, cmdbuf); cmdbuf.endRendering(); - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.endDebugUtilsLabelEXT(); } } diff --git a/src/imgui/renderer/imgui_impl_sdl3.cpp b/src/imgui/renderer/imgui_impl_sdl3.cpp index fbca90efb..388c23b03 100644 --- a/src/imgui/renderer/imgui_impl_sdl3.cpp +++ b/src/imgui/renderer/imgui_impl_sdl3.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later // Based on imgui_impl_sdl3.cpp from Dear ImGui repository #include -#include "common/config.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/memory.h" #include "imgui_impl_sdl3.h" #include "input/controller.h" @@ -396,7 +396,7 @@ bool ProcessEvent(const SDL_Event* event) { if (mouse_pos.x != bd->prev_mouse_pos.x || mouse_pos.y != bd->prev_mouse_pos.y) { bd->prev_mouse_pos.x = mouse_pos.x; bd->prev_mouse_pos.y = mouse_pos.y; - if (Config::getCursorState() == Config::HideCursorState::Idle) { + if (EmulatorSettings.GetCursorState() == HideCursorState::Idle) { bd->lastCursorMoveTime = bd->time; } } @@ -656,16 +656,16 @@ static void UpdateMouseCursor() { return; SdlData* bd = GetBackendData(); - s16 cursorState = Config::getCursorState(); + s16 cursorState = EmulatorSettings.GetCursorState(); ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor(); if (io.MouseDrawCursor || imgui_cursor == ImGuiMouseCursor_None || - cursorState == Config::HideCursorState::Always) { + cursorState == HideCursorState::Always) { // Hide OS mouse cursor if imgui is drawing it or if it wants no cursor SDL_HideCursor(); - } else if (cursorState == Config::HideCursorState::Idle && + } else if (cursorState == HideCursorState::Idle && bd->time - bd->lastCursorMoveTime >= - Config::getCursorHideTimeout() * SDL_GetPerformanceFrequency()) { + EmulatorSettings.GetCursorHideTimeout() * SDL_GetPerformanceFrequency()) { bool wasCursorVisible = SDL_CursorVisible(); SDL_HideCursor(); diff --git a/src/imgui/renderer/texture_manager.cpp b/src/imgui/renderer/texture_manager.cpp index 49f912a92..f8ac04352 100644 --- a/src/imgui/renderer/texture_manager.cpp +++ b/src/imgui/renderer/texture_manager.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -6,11 +6,11 @@ #include #include "common/assert.h" -#include "common/config.h" #include "common/io_file.h" #include "common/polyfill_thread.h" #include "common/stb.h" #include "common/thread.h" +#include "core/emulator_settings.h" #include "imgui_impl_vulkan.h" #include "texture_manager.h" @@ -152,7 +152,7 @@ void WorkerLoop() { g_job_list.pop_front(); g_job_list_mtx.unlock(); - if (Config::getVkCrashDiagnosticEnabled()) { + if (EmulatorSettings.IsVkCrashDiagnosticEnabled()) { // FIXME: Crash diagnostic hangs when building the command buffer here continue; } diff --git a/src/main.cpp b/src/main.cpp index d2804ee62..fb9ff3078 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include "common/config.h" #include "common/key_manager.h" @@ -19,10 +20,10 @@ #include "core/file_sys/fs.h" #include "core/ipc/ipc.h" #include "emulator.h" - #ifdef _WIN32 #include #endif +#include int main(int argc, char* argv[]) { #ifdef _WIN32 @@ -33,6 +34,7 @@ int main(int argc, char* argv[]) { auto emu_state = std::make_shared(); EmulatorState::SetInstance(emu_state); + UserSettings.Load(); const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); Config::load(user_dir / "config.toml"); @@ -51,6 +53,11 @@ int main(int argc, char* argv[]) { } } + // Load configurations + std::shared_ptr emu_settings = std::make_shared(); + EmulatorSettingsImpl::SetInstance(emu_settings); + emu_settings->Load(); + CLI::App app{"shadPS4 Emulator CLI"}; // ---- CLI state ---- @@ -120,15 +127,15 @@ int main(int argc, char* argv[]) { // ---- Utility commands ---- if (addGameFolder) { - Config::addGameInstallDir(*addGameFolder); - Config::save(user_dir / "config.toml"); + EmulatorSettings.AddGameInstallDir(*addGameFolder); + EmulatorSettings.Save(); std::cout << "Game folder successfully saved.\n"; return 0; } if (setAddonFolder) { - Config::setAddonInstallDir(*setAddonFolder); - Config::save(user_dir / "config.toml"); + EmulatorSettings.SetAddonInstallDir(*setAddonFolder); + EmulatorSettings.Save(); std::cout << "Addon folder successfully saved.\n"; return 0; } @@ -160,9 +167,9 @@ int main(int argc, char* argv[]) { if (fullscreenStr) { if (*fullscreenStr == "true") { - Config::setIsFullscreen(true); + EmulatorSettings.SetFullScreen(true); } else if (*fullscreenStr == "false") { - Config::setIsFullscreen(false); + EmulatorSettings.SetFullScreen(false); } else { std::cerr << "Error: Invalid argument for --fullscreen (use true|false)\n"; return 1; @@ -170,13 +177,13 @@ int main(int argc, char* argv[]) { } if (showFps) - Config::setShowFpsCounter(true); + EmulatorSettings.SetShowFpsCounter(true); if (configClean) - Config::setConfigMode(Config::ConfigMode::Clean); + EmulatorSettings.SetConfigMode(ConfigMode::Clean); if (configGlobal) - Config::setConfigMode(Config::ConfigMode::Global); + EmulatorSettings.SetConfigMode(ConfigMode::Global); if (logAppend) Common::Log::SetAppend(); @@ -186,7 +193,7 @@ int main(int argc, char* argv[]) { if (!std::filesystem::exists(ebootPath)) { bool found = false; constexpr int maxDepth = 5; - for (const auto& installDir : Config::getGameInstallDirs()) { + for (const auto& installDir : EmulatorSettings.GetGameInstallDirs()) { if (auto foundPath = Common::FS::FindGameByID(installDir, *gamePath, maxDepth)) { ebootPath = *foundPath; found = true; diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index 554448b13..81f8d08d7 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -1,8 +1,8 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" -#include "common/config.h" +#include "core/emulator_settings.h" #include "shader_recompiler/backend/spirv/emit_spirv_bounds.h" #include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" #include "shader_recompiler/backend/spirv/spirv_emit_context.h" @@ -58,7 +58,7 @@ Id EmitGetUserData(EmitContext& ctx, IR::ScalarReg reg) { Id EmitReadConst(EmitContext& ctx, IR::Inst* inst, Id addr, Id offset) { const u32 flatbuf_off_dw = inst->Flags(); - if (!Config::directMemoryAccess()) { + if (!EmulatorSettings.IsDirectMemoryAccessEnabled()) { return ctx.EmitFlatbufferLoad(ctx.ConstU32(flatbuf_off_dw)); } // We can only provide a fallback for immediate offsets. diff --git a/src/shader_recompiler/frontend/translate/translate.cpp b/src/shader_recompiler/frontend/translate/translate.cpp index 611070a86..de3822296 100644 --- a/src/shader_recompiler/frontend/translate/translate.cpp +++ b/src/shader_recompiler/frontend/translate/translate.cpp @@ -1,9 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/io_file.h" #include "common/path_util.h" +#include "core/emulator_settings.h" #include "shader_recompiler/frontend/decode.h" #include "shader_recompiler/frontend/fetch_shader.h" #include "shader_recompiler/frontend/translate/translate.h" @@ -569,7 +569,7 @@ void Translator::EmitFetch(const GcnInst& inst) { const auto fetch_data = ParseFetchShader(info); ASSERT(fetch_data.has_value()); - if (Config::dumpShaders()) { + if (EmulatorSettings.IsDumpShaders()) { using namespace Common::FS; const auto dump_dir = GetUserPath(PathType::ShaderDir) / "dumps"; if (!std::filesystem::exists(dump_dir)) { 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 e1f9f2c5a..a23ee2319 100644 --- a/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp +++ b/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp @@ -1,16 +1,15 @@ - -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include -#include "common/config.h" #include "common/io_file.h" #include "common/logging/log.h" #include "common/path_util.h" #include "common/signal_context.h" +#include "core/emulator_settings.h" #include "core/signals.h" #include "shader_recompiler/info.h" #include "shader_recompiler/ir/breadth_first_search.h" @@ -229,7 +228,7 @@ static void GenerateSrtProgram(Info& info, PassInfo& pass_info) { info.srt_info.walker_func_size = c.getCurr() - reinterpret_cast(info.srt_info.walker_func); - if (Config::dumpShaders()) { + if (EmulatorSettings.IsDumpShaders()) { DumpSrtProgram(info, reinterpret_cast(info.srt_info.walker_func), info.srt_info.walker_func_size); } diff --git a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp index c298a1092..e26d3f078 100644 --- a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp +++ b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" +#include "core/emulator_settings.h" #include "shader_recompiler/ir/program.h" #include "shader_recompiler/profile.h" #include "video_core/buffer_cache/buffer_cache.h" @@ -176,7 +176,7 @@ void CollectShaderInfoPass(IR::Program& program, const Profile& profile) { // info.readconst_types |= Info::ReadConstType::Immediate; } - if (!Config::directMemoryAccess()) { + if (!EmulatorSettings.IsDirectMemoryAccessEnabled()) { info.uses_dma = false; info.readconst_types = Info::ReadConstType::None; } diff --git a/src/shader_recompiler/ir/program.cpp b/src/shader_recompiler/ir/program.cpp index 1d03ea9ab..926f8f29d 100644 --- a/src/shader_recompiler/ir/program.cpp +++ b/src/shader_recompiler/ir/program.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -6,9 +7,9 @@ #include -#include "common/config.h" #include "common/io_file.h" #include "common/path_util.h" +#include "core/emulator_settings.h" #include "shader_recompiler/ir/basic_block.h" #include "shader_recompiler/ir/program.h" #include "shader_recompiler/ir/value.h" @@ -18,7 +19,7 @@ namespace Shader::IR { void DumpProgram(const Program& program, const Info& info, const std::string& type) { using namespace Common::FS; - if (!Config::dumpShaders()) { + if (!EmulatorSettings.IsDumpShaders()) { return; } diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index c4f1d6695..0648df922 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -1,14 +1,14 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/polyfill_thread.h" #include "common/thread.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/process.h" #include "core/libraries/videoout/driver.h" #include "core/memory.h" @@ -229,8 +229,8 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(dcb.data()); while (!dcb.empty()) { @@ -899,7 +899,7 @@ template Liverpool::Task Liverpool::ProcessCompute(std::span acb, u32 vqid) { FIBER_ENTER(acb_task_name[vqid]); auto& queue = asc_queues[{vqid}]; - const bool host_markers_enabled = rasterizer && Config::getVkHostMarkersEnabled(); + const bool host_markers_enabled = rasterizer && EmulatorSettings.IsVkHostMarkersEnabled(); struct IndirectPatch { const PM4Header* header; @@ -1202,7 +1202,7 @@ Liverpool::CmdBuffer Liverpool::CopyCmdBuffers(std::span dcb, std::sp void Liverpool::SubmitGfx(std::span dcb, std::span ccb) { auto& queue = mapped_queues[GfxQueueId]; - if (Config::copyGPUCmdBuffers()) { + if (EmulatorSettings.IsCopyGpuBuffers()) { std::tie(dcb, ccb) = CopyCmdBuffers(dcb, ccb); } diff --git a/src/video_core/buffer_cache/memory_tracker.h b/src/video_core/buffer_cache/memory_tracker.h index 2ec86de35..a093be8dd 100644 --- a/src/video_core/buffer_cache/memory_tracker.h +++ b/src/video_core/buffer_cache/memory_tracker.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -11,6 +11,7 @@ #include "common/debug.h" #include "common/types.h" +#include "core/emulator_settings.h" #include "video_core/buffer_cache/region_manager.h" namespace VideoCore { @@ -73,7 +74,7 @@ public: // modified. If we need to flush the flush function is going to perform CPU // state change. std::scoped_lock lk{manager->lock}; - if (Config::getReadbacksMode() != Config::GpuReadbacksMode::Disabled && + if (EmulatorSettings.GetReadbacksMode() != GpuReadbacksMode::Disabled && manager->template IsRegionModified(offset, size)) { return true; } diff --git a/src/video_core/buffer_cache/region_manager.h b/src/video_core/buffer_cache/region_manager.h index ecf9406af..a760dd596 100644 --- a/src/video_core/buffer_cache/region_manager.h +++ b/src/video_core/buffer_cache/region_manager.h @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "common/config.h" #include "common/div_ceil.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #ifdef __linux__ #include "common/adaptive_mutex.h" @@ -95,7 +95,7 @@ public: } if constexpr (type == Type::CPU) { UpdateProtection(); - } else if (Config::getReadbacksMode() == Config::GpuReadbacksMode::Precise) { + } else if (EmulatorSettings.GetReadbacksMode() == GpuReadbacksMode::Precise) { UpdateProtection(); } } @@ -126,7 +126,7 @@ public: bits.UnsetRange(start_page, end_page); if constexpr (type == Type::CPU) { UpdateProtection(); - } else if (Config::getReadbacksMode() != Config::GpuReadbacksMode::Disabled) { + } else if (EmulatorSettings.GetReadbacksMode() != GpuReadbacksMode::Disabled) { UpdateProtection(); } } diff --git a/src/video_core/cache_storage.cpp b/src/video_core/cache_storage.cpp index 1c46a4cf5..8d6abf9b5 100644 --- a/src/video_core/cache_storage.cpp +++ b/src/video_core/cache_storage.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/elf_info.h" #include "common/io_file.h" #include "common/polyfill_thread.h" #include "common/thread.h" +#include "core/emulator_settings.h" #include "video_core/cache_storage.h" #include "video_core/renderer_vulkan/vk_instance.h" @@ -95,7 +95,7 @@ void DataBase::Open() { const auto& game_info = Common::ElfInfo::Instance(); using namespace Common::FS; - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { mz_zip_zero_struct(&zip_ar); cache_path = GetUserPath(PathType::CacheDir) / @@ -128,7 +128,7 @@ void DataBase::Close() { io_worker.request_stop(); io_worker.join(); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { mz_zip_writer_finalize_archive(&zip_ar); mz_zip_writer_end(&zip_ar); } @@ -142,7 +142,7 @@ bool WriteVector(const BlobType type, std::filesystem::path&& path_, std::vector auto request = std::packaged_task{[=]() { auto path{path_}; path.replace_extension(GetBlobFileExtension(type)); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { ASSERT_MSG(!ar_is_read_only, "The archive is read-only. Did you forget to call `FinishPreload`?"); if (!mz_zip_writer_add_mem(&zip_ar, path.string().c_str(), v.data(), @@ -169,7 +169,7 @@ template void LoadVector(BlobType type, std::filesystem::path& path, std::vector& v) { using namespace Common::FS; path.replace_extension(GetBlobFileExtension(type)); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { int index{-1}; index = mz_zip_reader_locate_file(&zip_ar, path.string().c_str(), nullptr, 0); if (index < 0) { @@ -192,7 +192,8 @@ bool DataBase::Save(BlobType type, const std::string& name, std::vector&& da return false; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return WriteVector(type, std::move(path), std::move(data)); } @@ -201,7 +202,8 @@ bool DataBase::Save(BlobType type, const std::string& name, std::vector&& d return false; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return WriteVector(type, std::move(path), std::move(data)); } @@ -210,7 +212,8 @@ void DataBase::Load(BlobType type, const std::string& name, std::vector& dat return; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return LoadVector(type, path, data); } @@ -219,13 +222,14 @@ void DataBase::Load(BlobType type, const std::string& name, std::vector& da return; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return LoadVector(type, path, data); } void DataBase::ForEachBlob(BlobType type, const std::function&& data)>& func) { const auto& ext = GetBlobFileExtension(type); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { const auto num_files = mz_zip_reader_get_num_files(&zip_ar); for (int index = 0; index < num_files; ++index) { std::array file_name{}; @@ -255,7 +259,7 @@ void DataBase::ForEachBlob(BlobType type, const std::function @@ -31,7 +31,7 @@ void LoadRenderDoc() { // Check if we are running by RDoc GUI HMODULE mod = GetModuleHandleA("renderdoc.dll"); - if (!mod && Config::isRdocEnabled()) { + if (!mod && EmulatorSettings.IsRenderdocEnabled()) { // If enabled in config, try to load RDoc runtime in offline mode HKEY h_reg_key; LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, @@ -67,7 +67,7 @@ void LoadRenderDoc() { #endif // Check if we are running by RDoc GUI void* mod = dlopen(RENDERDOC_LIB, RTLD_NOW | RTLD_NOLOAD); - if (!mod && Config::isRdocEnabled()) { + if (!mod && EmulatorSettings.IsRenderdocEnabled()) { // If enabled in config, try to load RDoc runtime in offline mode if ((mod = dlopen(RENDERDOC_LIB, RTLD_NOW))) { const auto RENDERDOC_GetAPI = diff --git a/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp b/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp index 373ec1711..a4ebc859d 100644 --- a/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp +++ b/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp @@ -1,8 +1,8 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" -#include "common/config.h" +#include "core/emulator_settings.h" #include "video_core/host_shaders/fsr_comp.h" #include "video_core/renderer_vulkan/host_passes/fsr_pass.h" #include "video_core/renderer_vulkan/vk_platform.h" @@ -164,7 +164,7 @@ vk::ImageView FsrPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input, CreateImages(img); } - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{ .pLabelName = "Host/FSR", }); @@ -387,7 +387,7 @@ vk::ImageView FsrPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input, .pImageMemoryBarriers = return_barrier.data(), }); - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.endDebugUtilsLabelEXT(); } diff --git a/src/video_core/renderer_vulkan/host_passes/pp_pass.cpp b/src/video_core/renderer_vulkan/host_passes/pp_pass.cpp index 5c1fb4638..4b073c5fe 100644 --- a/src/video_core/renderer_vulkan/host_passes/pp_pass.cpp +++ b/src/video_core/renderer_vulkan/host_passes/pp_pass.cpp @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "video_core/renderer_vulkan/host_passes/pp_pass.h" #include "common/assert.h" -#include "common/config.h" +#include "core/emulator_settings.h" #include "video_core/host_shaders/fs_tri_vert.h" #include "video_core/host_shaders/post_process_frag.h" #include "video_core/renderer_vulkan/vk_platform.h" @@ -188,7 +188,7 @@ void PostProcessingPass::Create(vk::Device device, const vk::Format surface_form void PostProcessingPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input, vk::Extent2D input_size, Frame& frame, Settings settings) { - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{ .pLabelName = "Host/Post processing", }); @@ -279,7 +279,7 @@ void PostProcessingPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input, .pImageMemoryBarriers = &post_barrier, }); - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.endDebugUtilsLabelEXT(); } } diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 1b0af1d17..2666f05d3 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -1,13 +1,13 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/hash.h" #include "common/io_file.h" #include "common/path_util.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "shader_recompiler/backend/spirv/emit_spirv.h" #include "shader_recompiler/info.h" #include "shader_recompiler/recompiler.h" @@ -300,7 +300,7 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline() { RegisterPipelineData(graphics_key, pipeline_hash, sdata); ++num_new_pipelines; - if (Config::collectShadersForDebug()) { + if (EmulatorSettings.IsShaderCollect()) { for (auto stage = 0; stage < MaxShaderStages; ++stage) { if (infos[stage]) { auto& m = modules[stage]; @@ -329,7 +329,7 @@ const ComputePipeline* PipelineCache::GetComputePipeline() { RegisterPipelineData(compute_key, sdata); ++num_new_pipelines; - if (Config::collectShadersForDebug()) { + if (EmulatorSettings.IsShaderCollect()) { auto& m = modules[0]; module_related_pipelines[m].emplace_back(compute_key); } @@ -554,7 +554,7 @@ vk::ShaderModule PipelineCache::CompileModule(Shader::Info& info, Shader::Runtim vk::ShaderModule module; auto patch = GetShaderPatch(info.pgm_hash, info.stage, perm_idx, "spv"); - const bool is_patched = patch && Config::patchShaders(); + const bool is_patched = patch && EmulatorSettings.IsPatchShaders(); if (is_patched) { LOG_INFO(Loader, "Loaded patch for {} shader {:#x}", info.stage, info.pgm_hash); module = CompileSPV(*patch, instance.GetDevice()); @@ -566,7 +566,7 @@ vk::ShaderModule PipelineCache::CompileModule(Shader::Info& info, Shader::Runtim const auto name = GetShaderName(info.stage, info.pgm_hash, perm_idx); Vulkan::SetObjectName(instance.GetDevice(), module, name); - if (Config::collectShadersForDebug()) { + if (EmulatorSettings.IsShaderCollect()) { DebugState.CollectShader(name, info.l_stage, module, spv, code, patch ? *patch : std::span{}, is_patched); } @@ -659,7 +659,7 @@ std::string PipelineCache::GetShaderName(Shader::Stage stage, u64 hash, void PipelineCache::DumpShader(std::span code, u64 hash, Shader::Stage stage, size_t perm_idx, std::string_view ext) { - if (!Config::dumpShaders()) { + if (!EmulatorSettings.IsDumpShaders()) { return; } diff --git a/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp b/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp index 61c4bac7e..f36d9c61c 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/serdes.h" +#include "core/emulator_settings.h" #include "shader_recompiler/frontend/fetch_shader.h" #include "shader_recompiler/info.h" #include "video_core/cache_storage.h" @@ -295,7 +295,7 @@ bool PipelineCache::LoadPipelineStage(Serialization::Archive& ar, size_t stage) } void PipelineCache::WarmUp() { - if (!Config::isPipelineCacheEnabled()) { + if (!EmulatorSettings.IsPipelineCacheEnabled()) { return; } diff --git a/src/video_core/renderer_vulkan/vk_platform.cpp b/src/video_core/renderer_vulkan/vk_platform.cpp index 7027d62f8..7f02fcaf1 100644 --- a/src/video_core/renderer_vulkan/vk_platform.cpp +++ b/src/video_core/renderer_vulkan/vk_platform.cpp @@ -17,9 +17,9 @@ #include #include "common/assert.h" -#include "common/config.h" #include "common/logging/log.h" #include "common/path_util.h" +#include "core/emulator_settings.h" #include "sdl_window.h" #include "video_core/renderer_vulkan/vk_platform.h" @@ -87,7 +87,7 @@ vk::SurfaceKHR CreateSurface(vk::Instance instance, const Frontend::WindowSDL& e UNREACHABLE(); } } else if (window_info.type == Frontend::WindowSystemType::Wayland) { - if (Config::isRdocEnabled()) { + if (EmulatorSettings.IsRenderdocEnabled()) { LOG_ERROR(Render_Vulkan, "RenderDoc is not compatible with Wayland, use an X11 window instead."); } @@ -200,7 +200,7 @@ std::vector GetInstanceExtensions(Frontend::WindowSystemType window extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME); } - if (Config::allowHDR()) { + if (EmulatorSettings.IsHdrAllowed()) { extensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); } @@ -306,9 +306,9 @@ vk::UniqueInstance CreateInstance(Frontend::WindowSystemType window_type, bool e LOG_INFO(Render_Vulkan, "Enabled instance layers: {}", layers_string); // Validation settings - vk::Bool32 enable_core = Config::vkValidationCoreEnabled() ? vk::True : vk::False; - vk::Bool32 enable_sync = Config::vkValidationSyncEnabled() ? vk::True : vk::False; - vk::Bool32 enable_gpuav = Config::vkValidationGpuEnabled() ? vk::True : vk::False; + vk::Bool32 enable_core = EmulatorSettings.IsVkValidationCoreEnabled() ? vk::True : vk::False; + vk::Bool32 enable_sync = EmulatorSettings.IsVkValidationSyncEnabled() ? vk::True : vk::False; + vk::Bool32 enable_gpuav = EmulatorSettings.IsVkValidationGpuEnabled() ? vk::True : vk::False; // Crash diagnostics settings static const auto crash_diagnostic_path = diff --git a/src/video_core/renderer_vulkan/vk_presenter.cpp b/src/video_core/renderer_vulkan/vk_presenter.cpp index 1694d137f..c2a2a6621 100644 --- a/src/video_core/renderer_vulkan/vk_presenter.cpp +++ b/src/video_core/renderer_vulkan/vk_presenter.cpp @@ -1,12 +1,12 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/debug.h" #include "common/elf_info.h" #include "common/singleton.h" #include "core/debug_state.h" #include "core/devtools/layer.h" +#include "core/emulator_settings.h" #include "core/libraries/system/systemservice.h" #include "imgui/renderer/imgui_core.h" #include "imgui/renderer/imgui_impl_vulkan.h" @@ -104,8 +104,8 @@ static vk::Rect2D FitImage(s32 frame_width, s32 frame_height, s32 swapchain_widt Presenter::Presenter(Frontend::WindowSDL& window_, AmdGpu::Liverpool* liverpool_) : window{window_}, liverpool{liverpool_}, - instance{window, Config::getGpuId(), Config::vkValidationEnabled(), - Config::getVkCrashDiagnosticEnabled()}, + instance{window, EmulatorSettings.GetGpuId(), EmulatorSettings.IsVkValidationEnabled(), + EmulatorSettings.IsVkCrashDiagnosticEnabled()}, draw_scheduler{instance}, present_scheduler{instance}, flip_scheduler{instance}, swapchain{instance, window}, rasterizer{std::make_unique(instance, draw_scheduler, liverpool)}, @@ -124,9 +124,10 @@ Presenter::Presenter(Frontend::WindowSDL& window_, AmdGpu::Liverpool* liverpool_ free_queue.push(&frame); } - fsr_settings.enable = Config::getFsrEnabled(); - fsr_settings.use_rcas = Config::getRcasEnabled(); - fsr_settings.rcas_attenuation = static_cast(Config::getRcasAttenuation() / 1000.f); + fsr_settings.enable = EmulatorSettings.IsFsrEnabled(); + fsr_settings.use_rcas = EmulatorSettings.IsRcasEnabled(); + fsr_settings.rcas_attenuation = + static_cast(EmulatorSettings.GetRcasAttenuation() / 1000.f); fsr_pass.Create(device, instance.GetAllocator(), num_images); pp_pass.Create(device, swapchain.GetSurfaceFormat().format); @@ -465,7 +466,7 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) { auto& scheduler = present_scheduler; const auto cmdbuf = scheduler.CommandBuffer(); - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{ .pLabelName = "Present", }); @@ -577,7 +578,7 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) { ImGui::SetCursorPos(ImGui::GetCursorStartPos() + offset); ImGui::Image(game_texture, size); - if (Config::nullGpu()) { + if (EmulatorSettings.IsNullGPU()) { Core::Devtools::Layer::DrawNullGpuNotice(); } } @@ -595,8 +596,7 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) { TracyVkCollect(profiler_ctx, cmdbuf); } } - - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.endDebugUtilsLabelEXT(); } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 80af19372..800941fe3 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1,8 +1,8 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/debug.h" +#include "core/emulator_settings.h" #include "core/memory.h" #include "shader_recompiler/runtime_info.h" #include "video_core/amdgpu/liverpool.h" @@ -38,7 +38,7 @@ Rasterizer::Rasterizer(const Instance& instance_, Scheduler& scheduler_, texture_cache{instance, scheduler, liverpool_, buffer_cache, page_manager}, liverpool{liverpool_}, memory{Core::Memory::Instance()}, pipeline_cache{instance, scheduler, liverpool} { - if (!Config::nullGpu()) { + if (!EmulatorSettings.IsNullGPU()) { liverpool->BindRasterizer(this); } memory->SetRasterizer(this); @@ -1313,8 +1313,8 @@ void Rasterizer::UpdateColorBlendingState(const GraphicsPipeline* pipeline) cons } void Rasterizer::ScopeMarkerBegin(const std::string_view& str, bool from_guest) { - if ((from_guest && !Config::getVkGuestMarkersEnabled()) || - (!from_guest && !Config::getVkHostMarkersEnabled())) { + if ((from_guest && !EmulatorSettings.IsVkGuestMarkersEnabled()) || + (!from_guest && !EmulatorSettings.IsVkHostMarkersEnabled())) { return; } const auto cmdbuf = scheduler.CommandBuffer(); @@ -1324,8 +1324,8 @@ void Rasterizer::ScopeMarkerBegin(const std::string_view& str, bool from_guest) } void Rasterizer::ScopeMarkerEnd(bool from_guest) { - if ((from_guest && !Config::getVkGuestMarkersEnabled()) || - (!from_guest && !Config::getVkHostMarkersEnabled())) { + if ((from_guest && !EmulatorSettings.IsVkGuestMarkersEnabled()) || + (!from_guest && !EmulatorSettings.IsVkHostMarkersEnabled())) { return; } const auto cmdbuf = scheduler.CommandBuffer(); @@ -1333,8 +1333,8 @@ void Rasterizer::ScopeMarkerEnd(bool from_guest) { } void Rasterizer::ScopedMarkerInsert(const std::string_view& str, bool from_guest) { - if ((from_guest && !Config::getVkGuestMarkersEnabled()) || - (!from_guest && !Config::getVkHostMarkersEnabled())) { + if ((from_guest && !EmulatorSettings.IsVkGuestMarkersEnabled()) || + (!from_guest && !EmulatorSettings.IsVkHostMarkersEnabled())) { return; } const auto cmdbuf = scheduler.CommandBuffer(); @@ -1345,8 +1345,8 @@ void Rasterizer::ScopedMarkerInsert(const std::string_view& str, bool from_guest void Rasterizer::ScopedMarkerInsertColor(const std::string_view& str, const u32 color, bool from_guest) { - if ((from_guest && !Config::getVkGuestMarkersEnabled()) || - (!from_guest && !Config::getVkHostMarkersEnabled())) { + if ((from_guest && !EmulatorSettings.IsVkGuestMarkersEnabled()) || + (!from_guest && !EmulatorSettings.IsVkHostMarkersEnabled())) { return; } const auto cmdbuf = scheduler.CommandBuffer(); diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index 4dd3bd502..04f9d8504 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include "common/assert.h" -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "imgui/renderer/imgui_core.h" #include "sdl_window.h" #include "video_core/renderer_vulkan/vk_instance.h" @@ -164,7 +164,7 @@ void Swapchain::FindPresentFormat() { return format == SURFACE_FORMAT_HDR; }) != formats.end(); // Also make sure that user allowed us to use HDR - supports_hdr &= Config::allowHDR(); + supports_hdr &= EmulatorSettings.IsHdrAllowed(); // If there is a single undefined surface format, the device doesn't care, so we'll just use // RGBA sRGB. @@ -199,7 +199,7 @@ void Swapchain::FindPresentMode() { return; } - const auto requested_mode = Config::getPresentMode(); + const auto requested_mode = EmulatorSettings.GetPresentMode(); if (requested_mode == "Mailbox") { present_mode = vk::PresentModeKHR::eMailbox; } else if (requested_mode == "Fifo") { @@ -208,7 +208,7 @@ void Swapchain::FindPresentMode() { present_mode = vk::PresentModeKHR::eImmediate; } else { LOG_ERROR(Render_Vulkan, "Unknown present mode {}, defaulting to Mailbox.", - Config::getPresentMode()); + EmulatorSettings.GetPresentMode()); present_mode = vk::PresentModeKHR::eMailbox; } From 060703f6273d11e3abe0f8581ad9161b0166ee3d Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sun, 22 Mar 2026 00:27:55 +0200 Subject: [PATCH 32/44] Initial Openal (Part 2 of 0.15.1 branch) (#4155) * using new emulator_settings * the default user is now just player one * transfer install, addon dirs * fix load custom config issue * initial openal backend * linux fix? --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> --- CMakeLists.txt | 7 + src/core/libraries/audio/audioin.h | 1 + src/core/libraries/audio/audioout.cpp | 11 +- src/core/libraries/audio/audioout_backend.h | 7 +- src/core/libraries/audio/openal_audio_out.cpp | 832 +++++++++++++++ src/core/libraries/audio/openal_manager.h | 226 ++++ src/core/libraries/audio3d/audio3d_openal.cpp | 997 ++++++++++++++++++ src/core/libraries/audio3d/audio3d_openal.h | 181 ++++ src/core/libraries/libs.cpp | 10 +- 9 files changed, 2264 insertions(+), 8 deletions(-) create mode 100644 src/core/libraries/audio/openal_audio_out.cpp create mode 100644 src/core/libraries/audio/openal_manager.h create mode 100644 src/core/libraries/audio3d/audio3d_openal.cpp create mode 100644 src/core/libraries/audio3d/audio3d_openal.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ab204b7a2..06f8cb6ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -295,8 +295,15 @@ set(AUDIO_LIB src/core/libraries/audio/audioin.cpp src/core/libraries/audio/audioout_backend.h src/core/libraries/audio/audioout_error.h src/core/libraries/audio/sdl_audio_out.cpp + src/core/libraries/audio/openal_audio_out.cpp + src/core/libraries/audio/openal_manager.h src/core/libraries/ngs2/ngs2.cpp src/core/libraries/ngs2/ngs2.h + src/core/libraries/audio3d/audio3d.cpp + src/core/libraries/audio3d/audio3d_openal.cpp + src/core/libraries/audio3d/audio3d_openal.h + src/core/libraries/audio3d/audio3d.h + src/core/libraries/audio3d/audio3d_error.h ) set(GNM_LIB src/core/libraries/gnmdriver/gnmdriver.cpp diff --git a/src/core/libraries/audio/audioin.h b/src/core/libraries/audio/audioin.h index 0eda2013e..be43315e0 100644 --- a/src/core/libraries/audio/audioin.h +++ b/src/core/libraries/audio/audioin.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include "common/types.h" diff --git a/src/core/libraries/audio/audioout.cpp b/src/core/libraries/audio/audioout.cpp index 100ddd51c..e009f1f39 100644 --- a/src/core/libraries/audio/audioout.cpp +++ b/src/core/libraries/audio/audioout.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -6,10 +6,9 @@ #include #include #include +#include #include - #include "common/assert.h" -#include "common/config.h" #include "common/logging/log.h" #include "common/thread.h" #include "core/libraries/audio/audioout.h" @@ -206,7 +205,11 @@ s32 PS4_SYSV_ABI sceAudioOutInit() { return ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT; } - audio = std::make_unique(); + if (EmulatorSettings.GetAudioBackend() == AudioBackend::OpenAL) { + audio = std::make_unique(); + } else { + audio = std::make_unique(); + } LOG_INFO(Lib_AudioOut, "Audio system initialized"); return ORBIS_OK; diff --git a/src/core/libraries/audio/audioout_backend.h b/src/core/libraries/audio/audioout_backend.h index 0f36f19c8..e71abfefb 100644 --- a/src/core/libraries/audio/audioout_backend.h +++ b/src/core/libraries/audio/audioout_backend.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -31,4 +31,9 @@ public: std::unique_ptr Open(PortOut& port) override; }; +class OpenALAudioOut final : public AudioOutBackend { +public: + std::unique_ptr Open(PortOut& port) override; +}; + } // namespace Libraries::AudioOut diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp new file mode 100644 index 000000000..d40f01588 --- /dev/null +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -0,0 +1,832 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "core/emulator_settings.h" +#include "core/libraries/audio/audioout.h" +#include "core/libraries/audio/audioout_backend.h" +#include "core/libraries/audio/openal_manager.h" +#include "core/libraries/kernel/threads.h" + +// SIMD support detection +#if defined(__x86_64__) || defined(_M_X64) +#include +#define HAS_SSE2 +#endif + +namespace Libraries::AudioOut { + +// Volume constants +constexpr float VOLUME_0DB = 32768.0f; // 1 << 15 +constexpr float INV_VOLUME_0DB = 1.0f / VOLUME_0DB; +constexpr float VOLUME_EPSILON = 0.001f; +// Timing constants +constexpr u64 VOLUME_CHECK_INTERVAL_US = 50000; // Check every 50ms +constexpr u64 MIN_SLEEP_THRESHOLD_US = 10; +constexpr u64 TIMING_RESYNC_THRESHOLD_US = 100000; // Resync if >100ms behind + +// OpenAL constants +constexpr ALsizei NUM_BUFFERS = 6; +constexpr ALsizei BUFFER_QUEUE_THRESHOLD = 2; // Queue more buffers when below this + +// Channel positions +enum ChannelPos : u8 { + FL = 0, + FR = 1, + FC = 2, + LF = 3, + SL = 4, + SR = 5, + BL = 6, + BR = 7, + STD_SL = 6, + STD_SR = 7, + STD_BL = 4, + STD_BR = 5 +}; + +class OpenALPortBackend : public PortBackend { +public: + explicit OpenALPortBackend(const PortOut& port) + : frame_size(port.format_info.FrameSize()), guest_buffer_size(port.BufferSize()), + buffer_frames(port.buffer_frames), sample_rate(port.sample_rate), + num_channels(port.format_info.num_channels), is_float(port.format_info.is_float), + is_std(port.format_info.is_std), channel_layout(port.format_info.channel_layout), + device_registered(false), device_name(GetDeviceName(port.type)) { + + if (!Initialize(port.type)) { + LOG_ERROR(Lib_AudioOut, "Failed to initialize OpenAL audio backend"); + } + } + + ~OpenALPortBackend() override { + // Unregister port before cleanup + if (device_registered) { + OpenALDevice::GetInstance().UnregisterPort(device_name); + } + Cleanup(); + } + + void Output(void* ptr) override { + if (!source || !convert) [[unlikely]] { + return; + } + if (ptr == nullptr) [[unlikely]] { + return; + } + if (!device_context->MakeCurrent(device_name)) { + return; + } + + UpdateVolumeIfChanged(); + const u64 current_time = Kernel::sceKernelGetProcessTime(); + + // Convert audio data ONCE per call + if (use_native_float) { + convert(ptr, al_buffer_float.data(), buffer_frames, nullptr); + } else { + convert(ptr, al_buffer_s16.data(), buffer_frames, nullptr); + } + + // Reclaim processed buffers + ALint processed = 0; + alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); + + while (processed > 0) { + ALuint buffer_id; + alSourceUnqueueBuffers(source, 1, &buffer_id); + if (alGetError() == AL_NO_ERROR) { + available_buffers.push_back(buffer_id); + processed--; + } else { + break; + } + } + + // Queue buffer + if (!available_buffers.empty()) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + + if (use_native_float) { + alBufferData(buffer_id, format, al_buffer_float.data(), buffer_size_bytes, + sample_rate); + } else { + alBufferData(buffer_id, format, al_buffer_s16.data(), buffer_size_bytes, + sample_rate); + } + alSourceQueueBuffers(source, 1, &buffer_id); + } + + // Check state and queue health + ALint state = 0; + ALint queued = 0; + alGetSourcei(source, AL_SOURCE_STATE, &state); + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + + if (state != AL_PLAYING && queued > 0) { + LOG_DEBUG(Lib_AudioOut, "Audio underrun detected (queued: {}), restarting source", + queued); + alSourcePlay(source); + } + + // Only sleep if we have healthy buffer queue + if (queued >= 2) { + HandleTiming(current_time); + } else { + next_output_time = current_time + period_us; + } + + last_output_time.store(current_time, std::memory_order_release); + output_count++; + } + void SetVolume(const std::array& ch_volumes) override { + if (!device_context->MakeCurrent(device_name)) { + return; + } + + if (!source) [[unlikely]] { + return; + } + + float max_channel_gain = 0.0f; + const u32 channels_to_check = std::min(num_channels, 8u); + + for (u32 i = 0; i < channels_to_check; i++) { + const float channel_gain = static_cast(ch_volumes[i]) * INV_VOLUME_0DB; + max_channel_gain = std::max(max_channel_gain, channel_gain); + } + + const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; + const float total_gain = max_channel_gain * slider_gain; + + const float current = current_gain.load(std::memory_order_acquire); + if (std::abs(total_gain - current) < VOLUME_EPSILON) { + return; + } + + alSourcef(source, AL_GAIN, total_gain); + + ALenum error = alGetError(); + if (error == AL_NO_ERROR) { + current_gain.store(total_gain, std::memory_order_release); + LOG_DEBUG(Lib_AudioOut, + "Set combined audio gain to {:.3f} (channel: {:.3f}, slider: {:.3f})", + total_gain, max_channel_gain, slider_gain); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to set OpenAL source gain: {}", + GetALErrorString(error)); + } + } + + u64 GetLastOutputTime() const { + return last_output_time.load(std::memory_order_acquire); + } + +private: + bool Initialize(OrbisAudioOutPort type) { + // Register this port with the device manager + if (!OpenALDevice::GetInstance().RegisterPort(device_name)) { + if (device_name == "None") { + LOG_INFO(Lib_AudioOut, "Audio device disabled for port type {}", + static_cast(type)); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to register OpenAL device '{}'", device_name); + } + return false; + } + + device_registered = true; + device_context = &OpenALDevice::GetInstance(); + + // Make this device's context current + if (!device_context->MakeCurrent(device_name)) { + LOG_ERROR(Lib_AudioOut, "Failed to make OpenAL context current for device '{}'", + device_name); + return false; + } + + // Log device info + LOG_INFO(Lib_AudioOut, "Using OpenAL device for port type {}: '{}'", static_cast(type), + device_name); + + // Calculate timing parameters + period_us = (1000000ULL * buffer_frames + sample_rate / 2) / sample_rate; + + // Check for AL_EXT_FLOAT32 extension + has_float_ext = alIsExtensionPresent("AL_EXT_FLOAT32"); + if (has_float_ext && is_float) { + LOG_INFO(Lib_AudioOut, "AL_EXT_FLOAT32 extension detected - using native float format"); + } + + // Determine OpenAL format + if (!DetermineOpenALFormat()) { + LOG_ERROR(Lib_AudioOut, "Unsupported audio format for OpenAL"); + return false; + } + + // Allocate buffers based on format + if (use_native_float) { + al_buffer_float.resize(buffer_frames * num_channels); + buffer_size_bytes = buffer_frames * num_channels * sizeof(float); + } else { + al_buffer_s16.resize(buffer_frames * num_channels); + buffer_size_bytes = buffer_frames * num_channels * sizeof(s16); + } + + // Select optimal converter function + if (!SelectConverter()) { + return false; + } + + // Generate OpenAL source and buffers + if (!CreateOpenALObjects()) { + return false; + } + + // Initialize current gain + current_gain.store(EmulatorSettings.GetVolumeSlider() * 0.01f, std::memory_order_relaxed); + alSourcef(source, AL_GAIN, current_gain.load(std::memory_order_relaxed)); + + // Prime buffers with silence + if (use_native_float) { + std::vector silence(buffer_frames * num_channels, 0.0f); + for (size_t i = 0; i < buffers.size() - 1; i++) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + alBufferData(buffer_id, format, silence.data(), buffer_size_bytes, sample_rate); + alSourceQueueBuffers(source, 1, &buffer_id); + } + } else { + std::vector silence(buffer_frames * num_channels, 0); + for (size_t i = 0; i < buffers.size() - 1; i++) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + alBufferData(buffer_id, format, silence.data(), buffer_size_bytes, sample_rate); + alSourceQueueBuffers(source, 1, &buffer_id); + } + } + + alSourcePlay(source); + + LOG_INFO(Lib_AudioOut, + "Initialized OpenAL backend ({} Hz, {} ch, {} format, {}) for device '{}'", + sample_rate, num_channels, is_float ? "float" : "int16", + use_native_float ? "native" : "converted", device_name); + return true; + } + + void Cleanup() { + if (!device_context || !device_context->MakeCurrent(device_name)) { + return; + } + + if (source) { + alSourceStop(source); + + ALint queued = 0; + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + while (queued-- > 0) { + ALuint buf; + alSourceUnqueueBuffers(source, 1, &buf); + } + + alDeleteSources(1, &source); + source = 0; + } + + if (!buffers.empty()) { + alDeleteBuffers(static_cast(buffers.size()), buffers.data()); + buffers.clear(); + } + } + + std::string GetDeviceName(OrbisAudioOutPort type) const { + switch (type) { + case OrbisAudioOutPort::Main: + case OrbisAudioOutPort::Bgm: + return EmulatorSettings.GetOpenALMainOutputDevice(); + case OrbisAudioOutPort::PadSpk: + return EmulatorSettings.GetOpenALPadSpkOutputDevice(); + default: + return EmulatorSettings.GetOpenALMainOutputDevice(); + } + } + + void UpdateVolumeIfChanged() { + const u64 current_time = Kernel::sceKernelGetProcessTime(); + + if (current_time - last_volume_check_time < VOLUME_CHECK_INTERVAL_US) { + return; + } + + last_volume_check_time = current_time; + + const float config_volume = EmulatorSettings.GetVolumeSlider() * 0.01f; + const float stored_gain = current_gain.load(std::memory_order_acquire); + + if (std::abs(config_volume - stored_gain) > VOLUME_EPSILON) { + alSourcef(source, AL_GAIN, config_volume); + + ALenum error = alGetError(); + if (error == AL_NO_ERROR) { + current_gain.store(config_volume, std::memory_order_release); + LOG_DEBUG(Lib_AudioOut, "Updated audio gain to {:.3f}", config_volume); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to set audio gain: {}", GetALErrorString(error)); + } + } + } + + void HandleTiming(u64 current_time) { + if (next_output_time == 0) [[unlikely]] { + next_output_time = current_time + period_us; + return; + } + + const s64 time_diff = static_cast(current_time - next_output_time); + + if (time_diff > static_cast(TIMING_RESYNC_THRESHOLD_US)) [[unlikely]] { + next_output_time = current_time + period_us; + } else if (time_diff < 0) { + const u64 time_to_wait = static_cast(-time_diff); + next_output_time += period_us; + + if (time_to_wait > MIN_SLEEP_THRESHOLD_US) { + const u64 sleep_duration = time_to_wait - MIN_SLEEP_THRESHOLD_US; + std::this_thread::sleep_for(std::chrono::microseconds(sleep_duration)); + } + } else { + next_output_time += period_us; + } + } + + bool DetermineOpenALFormat() { + // Try to use native float formats if extension is available + if (is_float && has_float_ext) { + switch (num_channels) { + case 1: + format = AL_FORMAT_MONO_FLOAT32; + use_native_float = true; + return true; + case 2: + format = AL_FORMAT_STEREO_FLOAT32; + use_native_float = true; + return true; + case 4: + format = alGetEnumValue("AL_FORMAT_QUAD32"); + if (format != 0 && alGetError() == AL_NO_ERROR) { + use_native_float = true; + return true; + } + break; + case 6: + format = alGetEnumValue("AL_FORMAT_51CHN32"); + if (format != 0 && alGetError() == AL_NO_ERROR) { + use_native_float = true; + return true; + } + break; + case 8: + format = alGetEnumValue("AL_FORMAT_71CHN32"); + if (format != 0 && alGetError() == AL_NO_ERROR) { + use_native_float = true; + return true; + } + break; + } + + LOG_WARNING( + Lib_AudioOut, + "Float format for {} channels not supported, falling back to S16 conversion", + num_channels); + } + + // Fall back to S16 formats (with conversion if needed) + use_native_float = false; + + if (is_float) { + // Will need to convert float to S16 + format = AL_FORMAT_MONO16; + + switch (num_channels) { + case 1: + format = AL_FORMAT_MONO16; + break; + case 2: + format = AL_FORMAT_STEREO16; + break; + case 6: + format = alGetEnumValue("AL_FORMAT_51CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + case 8: + format = alGetEnumValue("AL_FORMAT_71CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else { + // Native 16-bit integer formats + switch (num_channels) { + case 1: + format = AL_FORMAT_MONO16; + break; + case 2: + format = AL_FORMAT_STEREO16; + break; + case 6: + format = alGetEnumValue("AL_FORMAT_51CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + case 8: + format = alGetEnumValue("AL_FORMAT_71CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels); + return false; + } + } + + return true; + } + + bool CreateOpenALObjects() { + alGenSources(1, &source); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_AudioOut, "Failed to generate OpenAL source"); + return false; + } + + buffers.resize(NUM_BUFFERS); + alGenBuffers(static_cast(buffers.size()), buffers.data()); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_AudioOut, "Failed to generate OpenAL buffers"); + alDeleteSources(1, &source); + source = 0; + return false; + } + + available_buffers = buffers; + + alSourcef(source, AL_PITCH, 1.0f); + alSourcef(source, AL_GAIN, 1.0f); + alSource3f(source, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + alSourcei(source, AL_LOOPING, AL_FALSE); + alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); + + LOG_DEBUG(Lib_AudioOut, "Created OpenAL source {} with {} buffers", source, buffers.size()); + return true; + } + + bool SelectConverter() { + if (is_float && use_native_float) { + // Native float - just copy/remap if needed + switch (num_channels) { + case 1: + convert = &ConvertF32Mono; + break; + case 2: + convert = &ConvertF32Stereo; + break; + case 8: + convert = is_std ? &ConvertF32Std8CH : &ConvertF32_8CH; + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else if (is_float && !use_native_float) { + // Float to S16 conversion needed + switch (num_channels) { + case 1: + convert = &ConvertF32ToS16Mono; + break; + case 2: +#ifdef HAS_SSE2 + convert = &ConvertF32ToS16StereoSIMD; +#else + convert = &ConvertF32ToS16Stereo; +#endif + break; + case 8: +#ifdef HAS_SSE2 + convert = is_std ? &ConvertF32ToS16Std8CH : &ConvertF32ToS16_8CH_SIMD; +#else + convert = is_std ? &ConvertF32ToS16Std8CH : &ConvertF32ToS16_8CH; +#endif + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else { + // S16 native - just copy + switch (num_channels) { + case 1: + convert = &ConvertS16Mono; + break; + case 2: + convert = &ConvertS16Stereo; + break; + case 8: + convert = &ConvertS16_8CH; + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels); + return false; + } + } + + return true; + } + + const char* GetALErrorString(ALenum error) { + switch (error) { + case AL_NO_ERROR: + return "AL_NO_ERROR"; + case AL_INVALID_NAME: + return "AL_INVALID_NAME"; + case AL_INVALID_ENUM: + return "AL_INVALID_ENUM"; + case AL_INVALID_VALUE: + return "AL_INVALID_VALUE"; + case AL_INVALID_OPERATION: + return "AL_INVALID_OPERATION"; + case AL_OUT_OF_MEMORY: + return "AL_OUT_OF_MEMORY"; + default: + return "Unknown AL error"; + } + } + + // Converter function type + using ConverterFunc = void (*)(const void* src, void* dst, u32 frames, const float* volumes); + + static inline s16 OrbisFloatToS16(float v) { + if (std::abs(v) < 1.0e-20f) + v = 0.0f; + + // Sony behavior: +1.0f -> 32767, -1.0f -> -32768 + const float scaled = v * 32768.0f; + + if (scaled >= 32767.0f) + return 32767; + if (scaled <= -32768.0f) + return -32768; + + return static_cast(scaled + (scaled >= 0 ? 0.5f : -0.5f)); + } + static void ConvertS16Mono(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + std::memcpy(d, s, frames * sizeof(s16)); + } + + static void ConvertS16Stereo(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 1; + std::memcpy(d, s, num_samples * sizeof(s16)); + } + + static void ConvertS16_8CH(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 3; + std::memcpy(d, s, num_samples * sizeof(s16)); + } + + // Float passthrough converters (for AL_EXT_FLOAT32) + static void ConvertF32Mono(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + std::memcpy(d, s, frames * sizeof(float)); + } + + static void ConvertF32Stereo(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + std::memcpy(d, s, frames * 2 * sizeof(float)); + } + + static void ConvertF32_8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + std::memcpy(d, s, frames * 8 * sizeof(float)); + } + + static void ConvertF32Std8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) { + const u32 offset = i << 3; + d[offset + FL] = s[offset + FL]; + d[offset + FR] = s[offset + FR]; + d[offset + FC] = s[offset + FC]; + d[offset + LF] = s[offset + LF]; + d[offset + SL] = s[offset + STD_SL]; + d[offset + SR] = s[offset + STD_SR]; + d[offset + BL] = s[offset + STD_BL]; + d[offset + BR] = s[offset + STD_BR]; + } + } + + // Float to S16 converters for OpenAL + static void ConvertF32ToS16Mono(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) + d[i] = OrbisFloatToS16(s[i]); + } +#ifdef HAS_SSE2 + static void ConvertF32ToS16StereoSIMD(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(32768.0f); + const __m128 min_val = _mm_set1_ps(-32768.0f); + const __m128 max_val = _mm_set1_ps(32767.0f); + + const u32 num_samples = frames << 1; + u32 i = 0; + + // Process 8 samples at a time + for (; i + 8 <= num_samples; i += 8) { + // Load 8 floats + __m128 f1 = _mm_loadu_ps(&s[i]); + __m128 f2 = _mm_loadu_ps(&s[i + 4]); + + // Scale and clamp + f1 = _mm_mul_ps(f1, scale); + f2 = _mm_mul_ps(f2, scale); + f1 = _mm_max_ps(f1, min_val); + f2 = _mm_max_ps(f2, min_val); + f1 = _mm_min_ps(f1, max_val); + f2 = _mm_min_ps(f2, max_val); + + // Convert to int32 + __m128i i1 = _mm_cvtps_epi32(f1); + __m128i i2 = _mm_cvtps_epi32(f2); + + // Pack to int16 + __m128i packed = _mm_packs_epi32(i1, i2); + + // Store + _mm_storeu_si128(reinterpret_cast<__m128i*>(&d[i]), packed); + } + + // Handle remaining samples + for (; i < num_samples; i++) { + d[i] = OrbisFloatToS16(s[i]); + } + } +#elif + static void ConvertF32ToS16Stereo(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 1; + for (u32 i = 0; i < num_samples; i++) + d[i] = OrbisFloatToS16(s[i]); + } +#endif + +#ifdef HAS_SSE2 + static void ConvertF32ToS16_8CH_SIMD(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(32768.0f); + const __m128 min_val = _mm_set1_ps(-32768.0f); + const __m128 max_val = _mm_set1_ps(32767.0f); + + const u32 num_samples = frames << 3; + u32 i = 0; + + // Process 8 samples at a time (1 frame of 8CH audio) + for (; i + 8 <= num_samples; i += 8) { + __m128 f1 = _mm_loadu_ps(&s[i]); + __m128 f2 = _mm_loadu_ps(&s[i + 4]); + + f1 = _mm_mul_ps(f1, scale); + f2 = _mm_mul_ps(f2, scale); + f1 = _mm_max_ps(_mm_min_ps(f1, max_val), min_val); + f2 = _mm_max_ps(_mm_min_ps(f2, max_val), min_val); + + __m128i i1 = _mm_cvtps_epi32(f1); + __m128i i2 = _mm_cvtps_epi32(f2); + __m128i packed = _mm_packs_epi32(i1, i2); + + _mm_storeu_si128(reinterpret_cast<__m128i*>(&d[i]), packed); + } + + for (; i < num_samples; i++) { + d[i] = OrbisFloatToS16(s[i]); + } + } +#elif + static void ConvertF32ToS16_8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 3; + for (u32 i = 0; i < num_samples; i++) + d[i] = OrbisFloatToS16(s[i]); + } +#endif + static void ConvertF32ToS16Std8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) { + const u32 offset = i << 3; + + d[offset + FL] = OrbisFloatToS16(s[offset + FL]); + d[offset + FR] = OrbisFloatToS16(s[offset + FR]); + d[offset + FC] = OrbisFloatToS16(s[offset + FC]); + d[offset + LF] = OrbisFloatToS16(s[offset + LF]); + d[offset + SL] = OrbisFloatToS16(s[offset + STD_SL]); + d[offset + SR] = OrbisFloatToS16(s[offset + STD_SR]); + d[offset + BL] = OrbisFloatToS16(s[offset + STD_BL]); + d[offset + BR] = OrbisFloatToS16(s[offset + STD_BR]); + } + } + + // Audio format parameters + const u32 frame_size; + const u32 guest_buffer_size; + const u32 buffer_frames; + const u32 sample_rate; + const u32 num_channels; + const bool is_float; + const bool is_std; + const std::array channel_layout; + + alignas(64) u64 period_us{0}; + alignas(64) std::atomic last_output_time{0}; + u64 next_output_time{0}; + u64 last_volume_check_time{0}; + u32 output_count{0}; + + // OpenAL objects + OpenALDevice* device_context{nullptr}; + ALuint source{0}; + std::vector buffers; + std::vector available_buffers; + ALenum format{AL_FORMAT_STEREO16}; + + // Buffer management + u32 buffer_size_bytes{0}; + std::vector al_buffer_s16; // For S16 formats + std::vector al_buffer_float; // For float formats + + // Extension support + bool has_float_ext{false}; + bool use_native_float{false}; + + // Converter function pointer + ConverterFunc convert{nullptr}; + + // Volume management + alignas(64) std::atomic current_gain{1.0f}; + + std::string device_name; + bool device_registered; +}; + +std::unique_ptr OpenALAudioOut::Open(PortOut& port) { + return std::make_unique(port); +} + +} // namespace Libraries::AudioOut \ No newline at end of file diff --git a/src/core/libraries/audio/openal_manager.h b/src/core/libraries/audio/openal_manager.h new file mode 100644 index 000000000..4a6ef7920 --- /dev/null +++ b/src/core/libraries/audio/openal_manager.h @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include +#include +#include +#include +#include +#include + +namespace Libraries::AudioOut { + +struct DeviceContext { + ALCdevice* device{nullptr}; + ALCcontext* context{nullptr}; + std::string device_name; + int port_count{0}; + + bool IsValid() const { + return device != nullptr && context != nullptr; + } + + void Cleanup() { + if (context) { + alcDestroyContext(context); + context = nullptr; + } + if (device) { + alcCloseDevice(device); + device = nullptr; + } + port_count = 0; + } +}; + +class OpenALDevice { +public: + static OpenALDevice& GetInstance() { + static OpenALDevice instance; + return instance; + } + + // Register a port that uses this device + bool RegisterPort(const std::string& device_name) { + std::lock_guard lock(mutex); + + // Handle "Default Device" alias + std::string actual_device_name = device_name; + if (actual_device_name.empty() || actual_device_name == "Default Device") { + actual_device_name = GetDefaultDeviceName(); + } + + // Find or create device context for this device name + auto it = devices.find(actual_device_name); + if (it != devices.end()) { + // Device exists, increment count + it->second.port_count++; + LOG_INFO(Lib_AudioOut, "Reusing OpenAL device '{}', port count: {}", actual_device_name, + it->second.port_count); + return true; + } + + // Create new device + DeviceContext ctx; + if (!InitializeDevice(ctx, actual_device_name)) { + LOG_ERROR(Lib_AudioOut, "Failed to initialize OpenAL device '{}'", actual_device_name); + return false; + } + + ctx.port_count = 1; + devices[actual_device_name] = ctx; + + LOG_INFO(Lib_AudioOut, "Created new OpenAL device '{}'", actual_device_name); + return true; + } + + // Unregister a port + void UnregisterPort(const std::string& device_name) { + std::lock_guard lock(mutex); + + std::string actual_device_name = device_name; + if (actual_device_name.empty() || actual_device_name == "Default Device") { + actual_device_name = GetDefaultDeviceName(); + } + + auto it = devices.find(actual_device_name); + if (it != devices.end()) { + it->second.port_count--; + LOG_INFO(Lib_AudioOut, "Port unregistered from '{}', remaining ports: {}", + actual_device_name, it->second.port_count); + + if (it->second.port_count <= 0) { + LOG_INFO(Lib_AudioOut, "Cleaning up OpenAL device '{}'", actual_device_name); + it->second.Cleanup(); + devices.erase(it); + } + } + } + + bool MakeCurrent(const std::string& device_name) { + std::lock_guard lock(mutex); + + std::string actual_device_name = device_name; + if (actual_device_name.empty() || actual_device_name == "Default Device") { + actual_device_name = GetDefaultDeviceName(); + } + + auto it = devices.find(actual_device_name); + if (it == devices.end() || !it->second.IsValid()) { + return false; + } + + // Store current device for this thread (simplified - in practice you might want + // thread-local storage) + current_context = it->second.context; + return alcMakeContextCurrent(it->second.context); + } + + void ReleaseContext() { + std::lock_guard lock(mutex); + alcMakeContextCurrent(nullptr); + current_context = nullptr; + } + + // Get the default device name + static std::string GetDefaultDeviceName() { + const ALCchar* default_device = alcGetString(nullptr, ALC_DEFAULT_DEVICE_SPECIFIER); + return default_device ? default_device : "Default Device"; + } + + // Check if device enumeration is supported + static bool IsDeviceEnumerationSupported() { + return alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT") || + alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT"); + } + + // Get list of available devices + static std::vector GetAvailableDevices() { + std::vector devices_list; + + if (!alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT")) + return devices_list; + + const ALCchar* devices = nullptr; + if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) { + devices = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); + } else { + devices = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); + } + + if (!devices) + return devices_list; + + const ALCchar* ptr = devices; + while (*ptr != '\0') { + devices_list.emplace_back(ptr); + ptr += std::strlen(ptr) + 1; + } + + return devices_list; + } + +private: + OpenALDevice() = default; + ~OpenALDevice() { + std::lock_guard lock(mutex); + for (auto& [name, ctx] : devices) { + ctx.Cleanup(); + } + devices.clear(); + } + + OpenALDevice(const OpenALDevice&) = delete; + OpenALDevice& operator=(const OpenALDevice&) = delete; + + bool InitializeDevice(DeviceContext& ctx, const std::string& device_name) { + // Handle disabled audio + if (device_name == "None") { + return false; + } + + // Open the requested device + if (device_name.empty() || device_name == "Default Device") { + ctx.device = alcOpenDevice(nullptr); + } else { + ctx.device = alcOpenDevice(device_name.c_str()); + if (!ctx.device) { + LOG_WARNING(Lib_AudioOut, "Device '{}' not found, falling back to default", + device_name); + ctx.device = alcOpenDevice(nullptr); + } + } + + if (!ctx.device) { + LOG_ERROR(Lib_AudioOut, "Failed to open OpenAL device"); + return false; + } + + // Create context + ctx.context = alcCreateContext(ctx.device, nullptr); + if (!ctx.context) { + LOG_ERROR(Lib_AudioOut, "Failed to create OpenAL context"); + alcCloseDevice(ctx.device); + ctx.device = nullptr; + return false; + } + + // Get actual device name + const ALCchar* actual_name = nullptr; + if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) { + actual_name = alcGetString(ctx.device, ALC_ALL_DEVICES_SPECIFIER); + } else { + actual_name = alcGetString(ctx.device, ALC_DEVICE_SPECIFIER); + } + ctx.device_name = actual_name ? actual_name : "Unknown"; + + LOG_INFO(Lib_AudioOut, "OpenAL device initialized: '{}'", ctx.device_name); + return true; + } + + std::unordered_map devices; + mutable std::mutex mutex; + ALCcontext* current_context{nullptr}; // For thread-local tracking +}; + +} // namespace Libraries::AudioOut \ No newline at end of file diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp new file mode 100644 index 000000000..53bbb8b24 --- /dev/null +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -0,0 +1,997 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "common/assert.h" +#include "common/logging/log.h" +#include "core/libraries/audio/audioout.h" +#include "core/libraries/audio/audioout_error.h" +#include "core/libraries/audio3d/audio3d_error.h" +#include "core/libraries/audio3d/audio3d_openal.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" + +namespace Libraries::Audio3dOpenAL { + +static constexpr u32 AUDIO3D_SAMPLE_RATE = 48000; + +static constexpr AudioOut::OrbisAudioOutParamFormat AUDIO3D_OUTPUT_FORMAT = + AudioOut::OrbisAudioOutParamFormat::S16Stereo; +static constexpr u32 AUDIO3D_OUTPUT_NUM_CHANNELS = 2; + +static std::unique_ptr state; + +s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) { + LOG_INFO(Lib_Audio3d, "called, handle = {}", handle); + + // Remove from any port that was tracking this handle. + if (state) { + for (auto& [port_id, port] : state->ports) { + std::scoped_lock lock{port.mutex}; + auto& handles = port.audioout_handles; + handles.erase(std::remove(handles.begin(), handles.end(), handle), handles.end()); + } + } + + return AudioOut::sceAudioOutClose(handle); +} + +s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen( + const OrbisAudio3dPortId port_id, const Libraries::UserService::OrbisUserServiceUserId user_id, + s32 type, const s32 index, const u32 len, const u32 freq, + const AudioOut::OrbisAudioOutParamExtendedInformation param) { + LOG_INFO(Lib_Audio3d, + "called, port_id = {}, user_id = {}, type = {}, index = {}, len = {}, freq = {}", + port_id, user_id, type, index, len, freq); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + std::scoped_lock lock{state->ports[port_id].mutex}; + if (len != state->ports[port_id].parameters.granularity) { + LOG_ERROR(Lib_Audio3d, "len != state->ports[port_id].parameters.granularity"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const s32 handle = sceAudioOutOpen(user_id, static_cast(type), + index, len, freq, param); + if (handle < 0) { + return handle; + } + + // Track this handle in the port so sceAudio3dPortFlush can use it for sync. + state->ports[port_id].audioout_handles.push_back(handle); + return handle; +} + +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(const s32 handle, void* ptr) { + LOG_DEBUG(Lib_Audio3d, "called, handle = {}, ptr = {}", handle, ptr); + + if (!ptr) { + LOG_ERROR(Lib_Audio3d, "!ptr"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (handle < 0 || (handle & 0xFFFF) > 25) { + LOG_ERROR(Lib_Audio3d, "handle < 0 || (handle & 0xFFFF) > 25"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + return AudioOut::sceAudioOutOutput(handle, ptr); +} + +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* param, + const u32 num) { + LOG_DEBUG(Lib_Audio3d, "called, param = {}, num = {}", static_cast(param), num); + + if (!param || !num) { + LOG_ERROR(Lib_Audio3d, "!param || !num"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + return AudioOut::sceAudioOutOutputs(param, num); +} + +static s32 ConvertAndEnqueue(std::deque& queue, const OrbisAudio3dPcm& pcm, + const u32 num_channels, const u32 granularity) { + if (!pcm.sample_buffer || !pcm.num_samples) { + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const u32 bytes_per_sample = + (pcm.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) ? sizeof(s16) : sizeof(float); + + // Always allocate exactly granularity samples (zeroed = silence for padding). + const u32 dst_bytes = granularity * num_channels * bytes_per_sample; + u8* copy = static_cast(std::calloc(1, dst_bytes)); + if (!copy) { + return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; + } + + // Copy min(provided, granularity) samples — extra are dropped, shortage stays zero. + const u32 samples_to_copy = std::min(pcm.num_samples, granularity); + std::memcpy(copy, pcm.sample_buffer, samples_to_copy * num_channels * bytes_per_sample); + + queue.emplace_back(AudioData{ + .sample_buffer = copy, + .num_samples = granularity, + .num_channels = num_channels, + .format = pcm.format, + }); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dBedWrite(const OrbisAudio3dPortId port_id, const u32 num_channels, + const OrbisAudio3dFormat format, void* buffer, + const u32 num_samples) { + return sceAudio3dBedWrite2(port_id, num_channels, format, buffer, num_samples, + OrbisAudio3dOutputRoute::ORBIS_AUDIO3D_OUTPUT_BOTH, false); +} + +s32 PS4_SYSV_ABI sceAudio3dBedWrite2(const OrbisAudio3dPortId port_id, const u32 num_channels, + const OrbisAudio3dFormat format, void* buffer, + const u32 num_samples, + const OrbisAudio3dOutputRoute output_route, + const bool restricted) { + LOG_DEBUG( + Lib_Audio3d, + "called, port_id = {}, num_channels = {}, format = {}, num_samples = {}, output_route " + "= {}, restricted = {}", + port_id, num_channels, magic_enum::enum_name(format), num_samples, + magic_enum::enum_name(output_route), restricted); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (output_route > OrbisAudio3dOutputRoute::ORBIS_AUDIO3D_OUTPUT_BOTH) { + LOG_ERROR(Lib_Audio3d, "output_route > ORBIS_AUDIO3D_OUTPUT_BOTH"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (format > OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_FLOAT) { + LOG_ERROR(Lib_Audio3d, "format > ORBIS_AUDIO3D_FORMAT_FLOAT"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (num_channels != 2 && num_channels != 6 && num_channels != 8) { + LOG_ERROR(Lib_Audio3d, "num_channels must be 2, 6, or 8"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (!buffer || !num_samples) { + LOG_ERROR(Lib_Audio3d, "!buffer || !num_samples"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_FLOAT) { + if ((reinterpret_cast(buffer) & 3) != 0) { + LOG_ERROR(Lib_Audio3d, "buffer & 3 != 0"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + } else if (format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) { + if ((reinterpret_cast(buffer) & 1) != 0) { + LOG_ERROR(Lib_Audio3d, "buffer & 1 != 0"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + } + + std::scoped_lock lock{state->ports[port_id].mutex}; + return ConvertAndEnqueue(state->ports[port_id].bed_queue, + OrbisAudio3dPcm{ + .format = format, + .sample_buffer = buffer, + .num_samples = num_samples, + }, + num_channels, state->ports[port_id].parameters.granularity); +} + +s32 PS4_SYSV_ABI sceAudio3dCreateSpeakerArray() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* params) { + LOG_DEBUG(Lib_Audio3d, "called"); + if (params) { + auto default_params = OrbisAudio3dOpenParameters{ + .size_this = 0x20, + .granularity = 0x100, + .rate = OrbisAudio3dRate::ORBIS_AUDIO3D_RATE_48000, + .max_objects = 512, + .queue_depth = 2, + .buffer_mode = OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH, + }; + memcpy(params, &default_params, 0x20); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMemorySize() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients2() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dInitialize(const s64 reserved) { + LOG_INFO(Lib_Audio3d, "called, reserved = {}", reserved); + + if (reserved != 0) { + LOG_ERROR(Lib_Audio3d, "reserved != 0"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (state) { + LOG_ERROR(Lib_Audio3d, "already initialized"); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + state = std::make_unique(); + + if (const auto init_ret = AudioOut::sceAudioOutInit(); + init_ret < 0 && init_ret != ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT) { + return init_ret; + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectReserve(const OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId* object_id) { + LOG_INFO(Lib_Audio3d, "called, port_id = {}, object_id = {}", port_id, + static_cast(object_id)); + + if (!object_id) { + LOG_ERROR(Lib_Audio3d, "!object_id"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + *object_id = ORBIS_AUDIO3D_OBJECT_INVALID; + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + + // Enforce the max_objects limit set at PortOpen time. + if (port.objects.size() >= port.parameters.max_objects) { + LOG_ERROR(Lib_Audio3d, "port has no available objects (max_objects = {})", + port.parameters.max_objects); + return ORBIS_AUDIO3D_ERROR_OUT_OF_RESOURCES; + } + + // Counter lives in the Port so it resets when the port is closed and reopened. + do { + ++port.next_object_id; + } while (port.next_object_id == 0 || + port.next_object_id == static_cast(ORBIS_AUDIO3D_OBJECT_INVALID) || + port.objects.contains(port.next_object_id)); + + *object_id = port.next_object_id; + port.objects.emplace(*object_id, ObjectState{}); + LOG_INFO(Lib_Audio3d, "reserved object_id = {}", *object_id); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttribute(const OrbisAudio3dPortId port_id, + const OrbisAudio3dObjectId object_id, + const OrbisAudio3dAttributeId attribute_id, + const void* attribute, const u64 attribute_size) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, object_id = {}, attribute_id = {:#x}, size = {}", + port_id, object_id, static_cast(attribute_id), attribute_size); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + if (!port.objects.contains(object_id)) { + LOG_DEBUG(Lib_Audio3d, "object_id {} not reserved (race with Unreserve?), no-op", + object_id); + return ORBIS_OK; + } + + if (!attribute_size && + attribute_id != OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE) { + LOG_ERROR(Lib_Audio3d, "!attribute_size for non-reset attribute"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + auto& obj = port.objects[object_id]; + + // RESET_STATE clears all attributes and queued PCM; it takes no value. + if (attribute_id == OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE) { + for (auto& data : obj.pcm_queue) { + std::free(data.sample_buffer); + } + obj.pcm_queue.clear(); + obj.persistent_attributes.clear(); + LOG_DEBUG(Lib_Audio3d, "RESET_STATE for object {}", object_id); + return ORBIS_OK; + } + + // Store the attribute so it's available when we implement it. + const auto* src = static_cast(attribute); + obj.persistent_attributes[static_cast(attribute_id)].assign(src, src + attribute_size); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id, + const u64 num_attributes, + const OrbisAudio3dAttribute* attribute_array) { + LOG_DEBUG(Lib_Audio3d, + "called, port_id = {}, object_id = {}, num_attributes = {}, attribute_array = {}", + port_id, object_id, num_attributes, fmt::ptr(attribute_array)); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (!num_attributes || !attribute_array) { + LOG_ERROR(Lib_Audio3d, "!num_attributes || !attribute_array"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + if (!port.objects.contains(object_id)) { + LOG_DEBUG(Lib_Audio3d, "object_id {} not reserved", object_id); + return ORBIS_OK; + } + + auto& obj = port.objects[object_id]; + + // First pass: handle RESET_STATE. + for (u64 i = 0; i < num_attributes; i++) { + if (attribute_array[i].attribute_id == + OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE) { + for (auto& data : obj.pcm_queue) { + std::free(data.sample_buffer); + } + obj.pcm_queue.clear(); + obj.persistent_attributes.clear(); + LOG_DEBUG(Lib_Audio3d, "RESET_STATE for object {}", object_id); + break; // Only one reset is needed even if listed multiple times. + } + } + + // Second pass: apply all other attributes. + for (u64 i = 0; i < num_attributes; i++) { + const auto& attr = attribute_array[i]; + + switch (attr.attribute_id) { + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE: + break; // Already applied in first pass above. + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_PCM: { + if (attr.value_size < sizeof(OrbisAudio3dPcm)) { + LOG_ERROR(Lib_Audio3d, "PCM attribute value_size too small"); + continue; + } + const auto pcm = static_cast(attr.value); + // Object audio is always mono (1 channel). + if (const auto ret = + ConvertAndEnqueue(obj.pcm_queue, *pcm, 1, port.parameters.granularity); + ret != ORBIS_OK) { + return ret; + } + break; + } + default: { + // Store the other attributes in the ObjectState so they're available when we + // implement them. + if (attr.value && attr.value_size > 0) { + const auto* src = static_cast(attr.value); + obj.persistent_attributes[static_cast(attr.attribute_id)].assign( + src, src + attr.value_size); + } + LOG_DEBUG(Lib_Audio3d, "Stored attribute {:#x} for object {}", + static_cast(attr.attribute_id), object_id); + break; + } + } + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(const OrbisAudio3dPortId port_id, + const OrbisAudio3dObjectId object_id) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, object_id = {}", port_id, object_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + + if (!port.objects.contains(object_id)) { + LOG_ERROR(Lib_Audio3d, "object_id not reserved"); + return ORBIS_AUDIO3D_ERROR_INVALID_OBJECT; + } + + // Free any queued PCM audio for this object. + for (auto& data : port.objects[object_id].pcm_queue) { + std::free(data.sample_buffer); + } + + port.objects.erase(object_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}", port_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + + if (port.parameters.buffer_mode == OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_NO_ADVANCE) { + LOG_ERROR(Lib_Audio3d, "port doesn't have advance capability"); + return ORBIS_AUDIO3D_ERROR_NOT_SUPPORTED; + } + + if (port.mixed_queue.size() >= port.parameters.queue_depth) { + LOG_WARNING(Lib_Audio3d, "mixed queue full (depth={}), dropping advance", + port.parameters.queue_depth); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + const u32 granularity = port.parameters.granularity; + const u32 out_samples = granularity * AUDIO3D_OUTPUT_NUM_CHANNELS; + + // ---- FLOAT MIX BUFFER ---- + float* mix_float = static_cast(std::calloc(out_samples, sizeof(float))); + if (!mix_float) + return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; + + auto mix_in = [&](std::deque& queue, const float gain) { + if (queue.empty()) + return; + + // default gain is 0.0 — objects with no GAIN set are silent. + if (gain == 0.0f) { + AudioData data = queue.front(); + queue.pop_front(); + std::free(data.sample_buffer); + return; + } + + AudioData data = queue.front(); + queue.pop_front(); + + const u32 frames = std::min(granularity, data.num_samples); + const u32 channels = data.num_channels; + + if (data.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) { + const s16* src = reinterpret_cast(data.sample_buffer); + + for (u32 i = 0; i < frames; i++) { + float left = 0.0f; + float right = 0.0f; + + if (channels == 1) { + float v = src[i] / 32768.0f; + left = v; + right = v; + } else { + left = src[i * channels + 0] / 32768.0f; + right = src[i * channels + 1] / 32768.0f; + } + + mix_float[i * 2 + 0] += left * gain; + mix_float[i * 2 + 1] += right * gain; + } + } else { // FLOAT input + const float* src = reinterpret_cast(data.sample_buffer); + + for (u32 i = 0; i < frames; i++) { + float left = 0.0f; + float right = 0.0f; + + if (channels == 1) { + left = src[i]; + right = src[i]; + } else { + left = src[i * channels + 0]; + right = src[i * channels + 1]; + } + + mix_float[i * 2 + 0] += left * gain; + mix_float[i * 2 + 1] += right * gain; + } + } + + std::free(data.sample_buffer); + }; + + // Bed is mixed at full gain (1.0). + mix_in(port.bed_queue, 1.0f); + + // Mix all object PCM queues, applying each object's GAIN persistent attribute. + for (auto& [obj_id, obj] : port.objects) { + float gain = 0.0f; + const auto gain_key = + static_cast(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_GAIN); + if (obj.persistent_attributes.contains(gain_key)) { + const auto& blob = obj.persistent_attributes.at(gain_key); + if (blob.size() >= sizeof(float)) { + std::memcpy(&gain, blob.data(), sizeof(float)); + } + } + mix_in(obj.pcm_queue, gain); + } + + s16* mix_s16 = static_cast(std::malloc(out_samples * sizeof(s16))); + if (!mix_s16) { + std::free(mix_float); + return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; + } + + for (u32 i = 0; i < out_samples; i++) { + float v = std::clamp(mix_float[i], -1.0f, 1.0f); + mix_s16[i] = static_cast(v * 32767.0f); + } + + std::free(mix_float); + + port.mixed_queue.push_back(AudioData{.sample_buffer = reinterpret_cast(mix_s16), + .num_samples = granularity, + .num_channels = AUDIO3D_OUTPUT_NUM_CHANNELS, + .format = OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortClose(const OrbisAudio3dPortId port_id) { + LOG_INFO(Lib_Audio3d, "called, port_id = {}", port_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + { + std::scoped_lock lock{port.mutex}; + + if (port.audio_out_handle >= 0) { + AudioOut::sceAudioOutClose(port.audio_out_handle); + port.audio_out_handle = -1; + } + + for (const s32 handle : port.audioout_handles) { + AudioOut::sceAudioOutClose(handle); + } + port.audioout_handles.clear(); + + for (auto& data : port.mixed_queue) { + std::free(data.sample_buffer); + } + + for (auto& data : port.bed_queue) { + std::free(data.sample_buffer); + } + + for (auto& [obj_id, obj] : port.objects) { + for (auto& data : obj.pcm_queue) { + std::free(data.sample_buffer); + } + } + } + + state->ports.erase(port_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortCreate() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortDestroy() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}", port_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + + if (!port.audioout_handles.empty()) { + for (const s32 handle : port.audioout_handles) { + const s32 ret = AudioOut::sceAudioOutOutput(handle, nullptr); + if (ret < 0) { + return ret; + } + } + return ORBIS_OK; + } + + if (port.mixed_queue.empty()) { + // Only mix if there's actually something to mix. + if (!port.bed_queue.empty() || + std::any_of(port.objects.begin(), port.objects.end(), + [](const auto& kv) { return !kv.second.pcm_queue.empty(); })) { + const s32 ret = sceAudio3dPortAdvance(port_id); + if (ret != ORBIS_OK && ret != ORBIS_AUDIO3D_ERROR_NOT_READY) { + return ret; + } + } + } + + if (port.mixed_queue.empty()) { + return ORBIS_OK; + } + + if (port.audio_out_handle < 0) { + AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; + ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); + port.audio_out_handle = + AudioOut::sceAudioOutOpen(0xFF, AudioOut::OrbisAudioOutPort::Audio3d, 0, + port.parameters.granularity, AUDIO3D_SAMPLE_RATE, ext_info); + if (port.audio_out_handle < 0) { + return port.audio_out_handle; + } + } + + // Drain all queued mixed frames, blocking on each until consumed. + while (!port.mixed_queue.empty()) { + AudioData frame = port.mixed_queue.front(); + port.mixed_queue.pop_front(); + const s32 ret = AudioOut::sceAudioOutOutput(port.audio_out_handle, frame.sample_buffer); + std::free(frame.sample_buffer); + if (ret < 0) { + return ret; + } + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortFreeState() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetList() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetParameters() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(const OrbisAudio3dPortId port_id, u32* queue_level, + u32* queue_available) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, queue_level = {}, queue_available = {}", port_id, + static_cast(queue_level), static_cast(queue_available)); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (!queue_level && !queue_available) { + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + const size_t size = port.mixed_queue.size(); + + if (queue_level) { + *queue_level = static_cast(size); + } + + if (queue_available) { + const u32 depth = port.parameters.queue_depth; + *queue_available = (size < depth) ? static_cast(depth - size) : 0u; + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetState() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetStatus() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortOpen(const Libraries::UserService::OrbisUserServiceUserId user_id, + const OrbisAudio3dOpenParameters* parameters, + OrbisAudio3dPortId* port_id) { + LOG_INFO(Lib_Audio3d, "called, user_id = {}, parameters = {}, id = {}", user_id, + static_cast(parameters), static_cast(port_id)); + + if (!state) { + LOG_ERROR(Lib_Audio3d, "!initialized"); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + if (!parameters || !port_id) { + LOG_ERROR(Lib_Audio3d, "!parameters || !id"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const int id = static_cast(state->ports.size()) + 1; + + if (id > 3) { + LOG_ERROR(Lib_Audio3d, "id > 3"); + return ORBIS_AUDIO3D_ERROR_OUT_OF_RESOURCES; + } + + *port_id = id; + auto& port = state->ports[id]; + std::memcpy( + &port.parameters, parameters, + std::min(parameters->size_this, static_cast(sizeof(OrbisAudio3dOpenParameters)))); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, + const OrbisAudio3dBlocking blocking) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, blocking = {}", port_id, + magic_enum::enum_name(blocking)); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + + if (port.parameters.buffer_mode != + OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH) { + LOG_ERROR(Lib_Audio3d, "port doesn't have push capability"); + return ORBIS_AUDIO3D_ERROR_NOT_SUPPORTED; + } + + const u32 depth = port.parameters.queue_depth; + + if (port.audio_out_handle < 0) { + AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; + ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); + + port.audio_out_handle = + AudioOut::sceAudioOutOpen(0xFF, AudioOut::OrbisAudioOutPort::Audio3d, 0, + port.parameters.granularity, AUDIO3D_SAMPLE_RATE, ext_info); + + if (port.audio_out_handle < 0) + return port.audio_out_handle; + } + + // Function that submits exactly one frame (if available). + auto submit_one_frame = [&](bool& submitted) -> s32 { + AudioData frame; + { + std::scoped_lock lock{port.mutex}; + + if (port.mixed_queue.empty()) { + submitted = false; + return ORBIS_OK; + } + + frame = port.mixed_queue.front(); + port.mixed_queue.pop_front(); + } + + const s32 ret = AudioOut::sceAudioOutOutput(port.audio_out_handle, frame.sample_buffer); + std::free(frame.sample_buffer); + + if (ret < 0) + return ret; + + submitted = true; + return ORBIS_OK; + }; + + // If not full, return immediately. + { + std::scoped_lock lock{port.mutex}; + if (port.mixed_queue.size() < depth) { + return ORBIS_OK; + } + } + + // Submit one frame to free space. + bool submitted = false; + s32 ret = submit_one_frame(submitted); + if (ret < 0) + return ret; + + if (!submitted) + return ORBIS_OK; + + // ASYNC: free exactly one slot and return. + if (blocking == OrbisAudio3dBlocking::ORBIS_AUDIO3D_BLOCKING_ASYNC) { + return ORBIS_OK; + } + + // SYNC: ensure at least one slot is free (drain until size < depth). + while (true) { + { + std::scoped_lock lock{port.mutex}; + if (port.mixed_queue.size() < depth) + break; + } + + bool drained = false; + ret = submit_one_frame(drained); + if (ret < 0) + return ret; + + if (!drained) + break; + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, + const OrbisAudio3dAttributeId attribute_id, + void* attribute, const u64 attribute_size) { + LOG_INFO(Lib_Audio3d, + "called, port_id = {}, attribute_id = {}, attribute = {}, attribute_size = {}", + port_id, static_cast(attribute_id), attribute, attribute_size); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (!attribute) { + LOG_ERROR(Lib_Audio3d, "!attribute"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + // TODO + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dReportUnregisterHandler() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dSetGpuRenderer() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dStrError() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dTerminate() { + LOG_INFO(Lib_Audio3d, "called"); + + if (!state) { + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + std::vector port_ids; + for (const auto& [id, _] : state->ports) { + port_ids.push_back(id); + } + for (const auto id : port_ids) { + sceAudio3dPortClose(id); + } + + state.reset(); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("pZlOm1aF3aA", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutClose); + LIB_FUNCTION("ucEsi62soTo", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutOpen); + LIB_FUNCTION("7NYEzJ9SJbM", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutOutput); + LIB_FUNCTION("HbxYY27lK6E", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutOutputs); + LIB_FUNCTION("9tEwE0GV0qo", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dBedWrite); + LIB_FUNCTION("xH4Q9UILL3o", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dBedWrite2); + LIB_FUNCTION("lvWMW6vEqFU", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dCreateSpeakerArray); + LIB_FUNCTION("8hm6YdoQgwg", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dDeleteSpeakerArray); + LIB_FUNCTION("Im+jOoa5WAI", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetDefaultOpenParameters); + LIB_FUNCTION("kEqqyDkmgdI", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetSpeakerArrayMemorySize); + LIB_FUNCTION("-R1DukFq7Dk", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetSpeakerArrayMixCoefficients); + LIB_FUNCTION("-Re+pCWvwjQ", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetSpeakerArrayMixCoefficients2); + LIB_FUNCTION("UmCvjSmuZIw", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dInitialize); + LIB_FUNCTION("jO2tec4dJ2M", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectReserve); + LIB_FUNCTION("V1FBFpNIAzk", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectSetAttribute); + LIB_FUNCTION("4uyHN9q4ZeU", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectSetAttributes); + LIB_FUNCTION("1HXxo-+1qCw", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectUnreserve); + LIB_FUNCTION("lw0qrdSjZt8", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortAdvance); + LIB_FUNCTION("OyVqOeVNtSk", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortClose); + LIB_FUNCTION("UHFOgVNz0kk", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortCreate); + LIB_FUNCTION("Mw9mRQtWepY", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortDestroy); + LIB_FUNCTION("ZOGrxWLgQzE", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortFlush); + LIB_FUNCTION("uJ0VhGcxCTQ", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortFreeState); + LIB_FUNCTION("9ZA23Ia46Po", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dPortGetAttributesSupported); + LIB_FUNCTION("SEggctIeTcI", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetList); + LIB_FUNCTION("flPcUaXVXcw", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetParameters); + LIB_FUNCTION("YaaDbDwKpFM", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetQueueLevel); + LIB_FUNCTION("CKHlRW2E9dA", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetState); + LIB_FUNCTION("iRX6GJs9tvE", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetStatus); + LIB_FUNCTION("XeDDK0xJWQA", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortOpen); + LIB_FUNCTION("VEVhZ9qd4ZY", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortPush); + LIB_FUNCTION("-pzYDZozm+M", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortQueryDebug); + LIB_FUNCTION("Yq9bfUQ0uJg", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortSetAttribute); + LIB_FUNCTION("QfNXBrKZeI0", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dReportRegisterHandler); + LIB_FUNCTION("psv2gbihC1A", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dReportUnregisterHandler); + LIB_FUNCTION("yEYXcbAGK14", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dSetGpuRenderer); + LIB_FUNCTION("Aacl5qkRU6U", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dStrError); + LIB_FUNCTION("WW1TS2iz5yc", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dTerminate); +} + +} // namespace Libraries::Audio3dOpenAL diff --git a/src/core/libraries/audio3d/audio3d_openal.h b/src/core/libraries/audio3d/audio3d_openal.h new file mode 100644 index 000000000..75be72066 --- /dev/null +++ b/src/core/libraries/audio3d/audio3d_openal.h @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "common/types.h" +#include "core/libraries/audio/audioout.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Audio3dOpenAL { + +constexpr int ORBIS_AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF; + +enum class OrbisAudio3dRate : u32 { + ORBIS_AUDIO3D_RATE_48000 = 0, +}; + +enum class OrbisAudio3dBufferMode : u32 { + ORBIS_AUDIO3D_BUFFER_NO_ADVANCE = 0, + ORBIS_AUDIO3D_BUFFER_ADVANCE_NO_PUSH = 1, + ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2, +}; + +struct OrbisAudio3dOpenParameters { + u64 size_this; + u32 granularity; + OrbisAudio3dRate rate; + u32 max_objects; + u32 queue_depth; + OrbisAudio3dBufferMode buffer_mode; + int : 32; + u32 num_beds; +}; + +enum class OrbisAudio3dFormat : u32 { + ORBIS_AUDIO3D_FORMAT_S16 = 0, + ORBIS_AUDIO3D_FORMAT_FLOAT = 1, +}; + +enum class OrbisAudio3dOutputRoute : u32 { + ORBIS_AUDIO3D_OUTPUT_BOTH = 0, + ORBIS_AUDIO3D_OUTPUT_HMU_ONLY = 1, + ORBIS_AUDIO3D_OUTPUT_TV_ONLY = 2, +}; + +enum class OrbisAudio3dBlocking : u32 { + ORBIS_AUDIO3D_BLOCKING_ASYNC = 0, + ORBIS_AUDIO3D_BLOCKING_SYNC = 1, +}; + +struct OrbisAudio3dPcm { + OrbisAudio3dFormat format; + void* sample_buffer; + u32 num_samples; +}; + +enum class OrbisAudio3dAttributeId : u32 { + ORBIS_AUDIO3D_ATTRIBUTE_PCM = 1, + ORBIS_AUDIO3D_ATTRIBUTE_POSITION = 2, + ORBIS_AUDIO3D_ATTRIBUTE_GAIN = 3, + ORBIS_AUDIO3D_ATTRIBUTE_SPREAD = 4, + ORBIS_AUDIO3D_ATTRIBUTE_PRIORITY = 5, + ORBIS_AUDIO3D_ATTRIBUTE_PASSTHROUGH = 6, + ORBIS_AUDIO3D_ATTRIBUTE_AMBISONICS = 7, + ORBIS_AUDIO3D_ATTRIBUTE_APPLICATION_SPECIFIC = 8, + ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE = 9, + ORBIS_AUDIO3D_ATTRIBUTE_RESTRICTED = 10, + ORBIS_AUDIO3D_ATTRIBUTE_OUTPUT_ROUTE = 11, +}; + +using OrbisAudio3dPortId = u32; +using OrbisAudio3dObjectId = u32; +using OrbisAudio3dAmbisonics = u32; + +struct OrbisAudio3dAttribute { + OrbisAudio3dAttributeId attribute_id; + int : 32; + void* value; + u64 value_size; +}; + +struct AudioData { + u8* sample_buffer; + u32 num_samples; + u32 num_channels{1}; + OrbisAudio3dFormat format{OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}; +}; + +struct ObjectState { + std::deque pcm_queue; + std::unordered_map> persistent_attributes; +}; + +struct Port { + mutable std::recursive_mutex mutex; + OrbisAudio3dOpenParameters parameters{}; + // Opened lazily on the first sceAudio3dPortPush call. + s32 audio_out_handle{-1}; + // Handles explicitly opened by the game via sceAudio3dAudioOutOpen. + std::vector audioout_handles; + // Reserved objects and their state. + std::unordered_map objects; + // Increasing counter for generating unique object IDs within this port. + OrbisAudio3dObjectId next_object_id{0}; + // Bed audio queue. + std::deque bed_queue; + // Mixed stereo frames ready to be consumed by sceAudio3dPortPush. + std::deque mixed_queue; +}; + +struct Audio3dState { + std::unordered_map ports; +}; + +s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(s32 handle); +s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id, + Libraries::UserService::OrbisUserServiceUserId user_id, + s32 type, s32 index, u32 len, u32 freq, + AudioOut::OrbisAudioOutParamExtendedInformation param); +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(s32 handle, void* ptr); +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* param, u32 num); +s32 PS4_SYSV_ABI sceAudio3dBedWrite(OrbisAudio3dPortId port_id, u32 num_channels, + OrbisAudio3dFormat format, void* buffer, u32 num_samples); +s32 PS4_SYSV_ABI sceAudio3dBedWrite2(OrbisAudio3dPortId port_id, u32 num_channels, + OrbisAudio3dFormat format, void* buffer, u32 num_samples, + OrbisAudio3dOutputRoute output_route, bool restricted); +s32 PS4_SYSV_ABI sceAudio3dCreateSpeakerArray(); +s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray(); +s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* params); +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMemorySize(); +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients(); +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients2(); +s32 PS4_SYSV_ABI sceAudio3dInitialize(s64 reserved); +s32 PS4_SYSV_ABI sceAudio3dObjectReserve(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId* object_id); +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttribute(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id, + OrbisAudio3dAttributeId attribute_id, + const void* attribute, u64 attribute_size); +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id, u64 num_attributes, + const OrbisAudio3dAttribute* attribute_array); +s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id); +s32 PS4_SYSV_ABI sceAudio3dPortAdvance(OrbisAudio3dPortId port_id); +s32 PS4_SYSV_ABI sceAudio3dPortClose(OrbisAudio3dPortId port_id); +s32 PS4_SYSV_ABI sceAudio3dPortCreate(); +s32 PS4_SYSV_ABI sceAudio3dPortDestroy(); +s32 PS4_SYSV_ABI sceAudio3dPortFlush(OrbisAudio3dPortId port_id); +s32 PS4_SYSV_ABI sceAudio3dPortFreeState(); +s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported(); +s32 PS4_SYSV_ABI sceAudio3dPortGetList(); +s32 PS4_SYSV_ABI sceAudio3dPortGetParameters(); +s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(OrbisAudio3dPortId port_id, u32* queue_level, + u32* queue_available); +s32 PS4_SYSV_ABI sceAudio3dPortGetState(); +s32 PS4_SYSV_ABI sceAudio3dPortGetStatus(); +s32 PS4_SYSV_ABI sceAudio3dPortOpen(Libraries::UserService::OrbisUserServiceUserId user_id, + const OrbisAudio3dOpenParameters* parameters, + OrbisAudio3dPortId* port_id); +s32 PS4_SYSV_ABI sceAudio3dPortPush(OrbisAudio3dPortId port_id, OrbisAudio3dBlocking blocking); +s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug(); +s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(OrbisAudio3dPortId port_id, + OrbisAudio3dAttributeId attribute_id, void* attribute, + u64 attribute_size); +s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler(); +s32 PS4_SYSV_ABI sceAudio3dReportUnregisterHandler(); +s32 PS4_SYSV_ABI sceAudio3dSetGpuRenderer(); +s32 PS4_SYSV_ABI sceAudio3dStrError(); +s32 PS4_SYSV_ABI sceAudio3dTerminate(); + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Audio3dOpenAL diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index ac35c4b63..6db1ba18d 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -1,12 +1,12 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "core/libraries/ajm/ajm.h" #include "core/libraries/app_content/app_content.h" #include "core/libraries/audio/audioin.h" #include "core/libraries/audio/audioout.h" #include "core/libraries/audio3d/audio3d.h" +#include "core/libraries/audio3d/audio3d_openal.h" #include "core/libraries/avplayer/avplayer.h" #include "core/libraries/camera/camera.h" #include "core/libraries/companion/companion_httpd.h" @@ -127,7 +127,11 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::AvPlayer::RegisterLib(sym); Libraries::Videodec::RegisterLib(sym); Libraries::Videodec2::RegisterLib(sym); - Libraries::Audio3d::RegisterLib(sym); + if (EmulatorSettings.GetAudioBackend() == AudioBackend::OpenAL) { + Libraries::Audio3dOpenAL::RegisterLib(sym); + } else { + Libraries::Audio3d::RegisterLib(sym); + } Libraries::Ime::RegisterLib(sym); Libraries::GameLiveStreaming::RegisterLib(sym); Libraries::SharePlay::RegisterLib(sym); From 880445c2cea5a8c7b571f55e5bf390d011763bf8 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sun, 22 Mar 2026 09:08:44 +0200 Subject: [PATCH 33/44] Initial camera (Part 3 of 0.15.1 branch) (#4156) * using new emulator_settings * the default user is now just player one * transfer install, addon dirs * fix load custom config issue * initial openal backend * linux fix? * camera module updated --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> --- src/core/libraries/camera/camera.cpp | 238 ++++++++++++++++++++++++++- src/core/libraries/camera/camera.h | 117 +++++++++++++ src/sdl_window.cpp | 3 + 3 files changed, 350 insertions(+), 8 deletions(-) diff --git a/src/core/libraries/camera/camera.cpp b/src/core/libraries/camera/camera.cpp index 56ed28f2d..14aad8e84 100644 --- a/src/core/libraries/camera/camera.cpp +++ b/src/core/libraries/camera/camera.cpp @@ -3,17 +3,27 @@ #include "common/elf_info.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/camera/camera.h" #include "core/libraries/camera/camera_error.h" #include "core/libraries/error_codes.h" #include "core/libraries/kernel/process.h" #include "core/libraries/libs.h" +#include + +#include +#include "SDL3/SDL_camera.h" + namespace Libraries::Camera { static bool g_library_opened = false; static s32 g_firmware_version = 0; static s32 g_handles = 0; +static constexpr s32 c_width = 1280, c_height = 800; + +SDL_Camera* sdl_camera = nullptr; +OrbisCameraConfigExtention output_config0, output_config1; s32 PS4_SYSV_ABI sceCameraAccGetData() { LOG_ERROR(Lib_Camera, "(STUBBED) called"); @@ -325,16 +335,126 @@ s32 PS4_SYSV_ABI sceCameraGetExposureGain(s32 handle, OrbisCameraChannel channel return ORBIS_OK; } +static std::vector raw16_buffer1, raw16_buffer2; +static std::vector raw8_buffer1, raw8_buffer2; + +static void ConvertRGBA8888ToRAW16(const u8* src, u16* dst, int width, int height) { + for (int y = 0; y < height; ++y) { + const u8* row = src + y * width * 4; + u16* outRow = dst + y * width; + + for (int x = 0; x < width; ++x) { + const u8* px = row + x * 4; + + u16 b = u16(px[1]) << 4; + u16 g = u16(px[2]) << 4; + u16 r = u16(px[3]) << 4; + + // BGGR Bayer layout + // B G + // G R + bool evenRow = (y & 1) == 0; + bool evenCol = (x & 1) == 0; + + if (evenRow && evenCol) { + outRow[x] = b; + } else if (evenRow && !evenCol) { + outRow[x] = g; + } else if (!evenRow && evenCol) { + outRow[x] = g; + } else { + outRow[x] = r; + } + } + } +} + +static void ConvertRGBA8888ToRAW8(const u8* src, u8* dst, int width, int height) { + for (int y = 0; y < height; ++y) { + const u8* row = src + y * width * 4; + u8* outRow = dst + y * width; + + for (int x = 0; x < width; ++x) { + const u8* px = row + x * 4; + + u8 b = px[1]; + u8 g = px[2]; + u8 r = px[3]; + + // BGGR Bayer layout + // B G + // G R + bool evenRow = (y & 1) == 0; + bool evenCol = (x & 1) == 0; + + if (evenRow && evenCol) { + outRow[x] = b; + } else if (evenRow && !evenCol) { + outRow[x] = g; + } else if (!evenRow && evenCol) { + outRow[x] = g; + } else { + outRow[x] = r; + } + } + } +} + s32 PS4_SYSV_ABI sceCameraGetFrameData(s32 handle, OrbisCameraFrameData* frame_data) { LOG_DEBUG(Lib_Camera, "called"); if (handle < 1 || frame_data == nullptr || frame_data->sizeThis > 584) { return ORBIS_CAMERA_ERROR_PARAM; } - if (!g_library_opened) { + if (!g_library_opened || !sdl_camera) { return ORBIS_CAMERA_ERROR_NOT_OPEN; } + if (EmulatorSettings.GetCameraId() == -1) { + return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + } + Uint64 timestampNS = 0; + static SDL_Surface* frame = nullptr; + if (frame) { // release previous frame, if it exists + SDL_ReleaseCameraFrame(sdl_camera, frame); + } + frame = SDL_AcquireCameraFrame(sdl_camera, ×tampNS); - return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + if (!frame) { + return ORBIS_CAMERA_ERROR_BUSY; + } + + switch (output_config0.format.formatLevel0) { + case ORBIS_CAMERA_FORMAT_YUV422: + frame_data->pFramePointerList[0][0] = frame->pixels; + break; + case ORBIS_CAMERA_FORMAT_RAW16: + ConvertRGBA8888ToRAW16((u8*)frame->pixels, raw16_buffer1.data(), c_width, c_height); + frame_data->pFramePointerList[0][0] = raw16_buffer1.data(); + break; + case ORBIS_CAMERA_FORMAT_RAW8: + ConvertRGBA8888ToRAW8((u8*)frame->pixels, raw8_buffer1.data(), c_width, c_height); + frame_data->pFramePointerList[0][0] = raw8_buffer1.data(); + break; + default: + UNREACHABLE(); + } + switch (output_config1.format.formatLevel0) { + case ORBIS_CAMERA_FORMAT_YUV422: + frame_data->pFramePointerList[1][0] = frame->pixels; + break; + case ORBIS_CAMERA_FORMAT_RAW16: + ConvertRGBA8888ToRAW16((u8*)frame->pixels, raw16_buffer2.data(), c_width, c_height); + frame_data->pFramePointerList[1][0] = raw16_buffer2.data(); + break; + case ORBIS_CAMERA_FORMAT_RAW8: + ConvertRGBA8888ToRAW8((u8*)frame->pixels, raw8_buffer2.data(), c_width, c_height); + frame_data->pFramePointerList[1][0] = raw8_buffer2.data(); + break; + default: + UNREACHABLE(); + } + frame_data->meta.format[0][0] = output_config0.format.formatLevel0; + frame_data->meta.format[1][0] = output_config1.format.formatLevel0; + return ORBIS_OK; } s32 PS4_SYSV_ABI sceCameraGetGamma(s32 handle, OrbisCameraChannel channel, OrbisCameraGamma* gamma, @@ -499,7 +619,7 @@ s32 PS4_SYSV_ABI sceCameraIsAttached(s32 index) { return ORBIS_CAMERA_ERROR_PARAM; } // 0 = disconnected, 1 = connected - return 0; + return EmulatorSettings.GetCameraId() == -1 ? 0 : 1; } s32 PS4_SYSV_ABI sceCameraIsConfigChangeDone() { @@ -516,16 +636,16 @@ s32 PS4_SYSV_ABI sceCameraIsValidFrameData(s32 handle, OrbisCameraFrameData* fra return ORBIS_CAMERA_ERROR_NOT_OPEN; } - return ORBIS_OK; + return 1; // valid } s32 PS4_SYSV_ABI sceCameraOpen(Libraries::UserService::OrbisUserServiceUserId user_id, s32 type, s32 index, OrbisCameraOpenParameter* param) { + LOG_INFO(Lib_Camera, "called"); if (user_id != Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_SYSTEM || type != 0 || index != 0) { return ORBIS_CAMERA_ERROR_PARAM; } - LOG_WARNING(Lib_Camera, "Cameras are not supported yet"); g_library_opened = true; return ++g_handles; @@ -609,15 +729,44 @@ s32 PS4_SYSV_ABI sceCameraSetCalibData() { } s32 PS4_SYSV_ABI sceCameraSetConfig(s32 handle, OrbisCameraConfig* config) { - LOG_DEBUG(Lib_Camera, "called"); + LOG_INFO(Lib_Camera, "called"); if (handle < 1 || config == nullptr || config->sizeThis != sizeof(OrbisCameraConfig)) { return ORBIS_CAMERA_ERROR_PARAM; } if (!g_library_opened) { return ORBIS_CAMERA_ERROR_NOT_OPEN; } + if (EmulatorSettings.GetCameraId() == -1) { + return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + } - return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + switch (config->configType) { + case ORBIS_CAMERA_CONFIG_TYPE1: + case ORBIS_CAMERA_CONFIG_TYPE2: + case ORBIS_CAMERA_CONFIG_TYPE3: + case ORBIS_CAMERA_CONFIG_TYPE4: + output_config0 = camera_config_types[config->configType - 1][0]; + output_config1 = camera_config_types[config->configType - 1][1]; + break; + case ORBIS_CAMERA_CONFIG_TYPE5: + int sdk_ver; + Libraries::Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver); + if (sdk_ver < Common::ElfInfo::FW_45) { + return ORBIS_CAMERA_ERROR_UNKNOWN_CONFIG; + } + output_config0 = camera_config_types[config->configType - 1][0]; + output_config1 = camera_config_types[config->configType - 1][1]; + break; + case ORBIS_CAMERA_CONFIG_EXTENTION: + output_config0 = config->configExtention[0]; + output_config1 = config->configExtention[1]; + break; + default: + LOG_ERROR(Lib_Camera, "Invalid config type {}", std::to_underlying(config->configType)); + return ORBIS_CAMERA_ERROR_PARAM; + } + + return ORBIS_OK; } s32 PS4_SYSV_ABI sceCameraSetConfigInternal(s32 handle, OrbisCameraConfig* config) { @@ -851,7 +1000,7 @@ s32 PS4_SYSV_ABI sceCameraSetWhiteBalance(s32 handle, OrbisCameraChannel channel } s32 PS4_SYSV_ABI sceCameraStart(s32 handle, OrbisCameraStartParameter* param) { - LOG_DEBUG(Lib_Camera, "called"); + LOG_INFO(Lib_Camera, "called"); if (handle < 1 || param == nullptr || param->sizeThis != sizeof(OrbisCameraStartParameter)) { return ORBIS_CAMERA_ERROR_PARAM; } @@ -864,6 +1013,79 @@ s32 PS4_SYSV_ABI sceCameraStart(s32 handle, OrbisCameraStartParameter* param) { return ORBIS_CAMERA_ERROR_FORMAT_UNKNOWN; } + if (param->formatLevel[0] > 1 || param->formatLevel[1] > 1) { + LOG_ERROR(Lib_Camera, "Downscaled image retrieval isn't supported yet!"); + } + + SDL_CameraID* devices = NULL; + int devcount = 0; + devices = SDL_GetCameras(&devcount); + if (devices == NULL) { + LOG_ERROR(Lib_Camera, "Couldn't enumerate camera devices: {}", SDL_GetError()); + return ORBIS_CAMERA_ERROR_FATAL; + } else if (devcount == 0) { + LOG_INFO(Lib_Camera, "No camera devices connected"); + return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + } + raw8_buffer1.resize(c_width * c_height); + raw16_buffer1.resize(c_width * c_height); + raw8_buffer2.resize(c_width * c_height); + raw16_buffer2.resize(c_width * c_height); + SDL_CameraSpec cam_spec{}; + switch (output_config0.format.formatLevel0) { + case ORBIS_CAMERA_FORMAT_YUV422: + cam_spec.format = SDL_PIXELFORMAT_YUY2; + break; + case ORBIS_CAMERA_FORMAT_RAW8: + cam_spec.format = SDL_PIXELFORMAT_RGBA8888; // to be swizzled + break; + case ORBIS_CAMERA_FORMAT_RAW16: + cam_spec.format = SDL_PIXELFORMAT_RGBA8888; // to be swizzled + break; + + default: + LOG_ERROR(Lib_Camera, "Invalid format {}", + std::to_underlying(output_config0.format.formatLevel0)); + break; + } + cam_spec.height = c_height; + cam_spec.width = c_width; + cam_spec.framerate_numerator = 60; + cam_spec.framerate_denominator = 1; + sdl_camera = SDL_OpenCamera(devices[EmulatorSettings.GetCameraId()], &cam_spec); + LOG_INFO(Lib_Camera, "SDL backend in use: {}", SDL_GetCurrentCameraDriver()); + char const* camera_name = SDL_GetCameraName(devices[EmulatorSettings.GetCameraId()]); + if (camera_name) + LOG_INFO(Lib_Camera, "SDL camera name: {}", camera_name); + SDL_CameraSpec spec; + SDL_GetCameraFormat(sdl_camera, &spec); + LOG_INFO(Lib_Camera, "SDL camera format: {:#x}", std::to_underlying(spec.format)); + LOG_INFO(Lib_Camera, "SDL camera framerate: {}", + (float)spec.framerate_numerator / (float)spec.framerate_denominator); + LOG_INFO(Lib_Camera, "SDL camera dimensions: {}x{}", spec.width, spec.height); + + SDL_free(devices); + + // "warm up" the device, as recommended by SDL + u64 timestamp; + SDL_Surface* frame = nullptr; + frame = SDL_AcquireCameraFrame(sdl_camera, ×tamp); + if (!frame) { + for (int i = 0; i < 1000; i++) { + frame = SDL_AcquireCameraFrame(sdl_camera, ×tamp); + if (frame) { + SDL_ReleaseCameraFrame(sdl_camera, frame); + break; + } + std::this_thread::sleep_for(std::chrono::nanoseconds(10)); + } + } + + if (!sdl_camera) { + LOG_ERROR(Lib_Camera, "Failed to open camera: {}", SDL_GetError()); + return ORBIS_CAMERA_ERROR_FATAL; + } + return ORBIS_OK; } diff --git a/src/core/libraries/camera/camera.h b/src/core/libraries/camera/camera.h index 86bb12135..eba7f4720 100644 --- a/src/core/libraries/camera/camera.h +++ b/src/core/libraries/camera/camera.h @@ -102,6 +102,123 @@ struct OrbisCameraConfig { OrbisCameraConfigExtention configExtention[ORBIS_CAMERA_MAX_DEVICE_NUM]; }; +constexpr OrbisCameraConfigExtention camera_config_types[5][ORBIS_CAMERA_MAX_DEVICE_NUM]{ + { + // type 1 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }, + { + // type 2 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }, + { + // type 3 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }, + { + // type 4 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }, + { + // type 5 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }}; + enum OrbisCameraAecAgcTarget { ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_DEF = 0x00, ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_2_0 = 0x20, diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 838fc4db4..0c7374bfb 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -288,6 +288,9 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ if (!SDL_Init(SDL_INIT_VIDEO)) { UNREACHABLE_MSG("Failed to initialize SDL video subsystem: {}", SDL_GetError()); } + if (!SDL_Init(SDL_INIT_CAMERA)) { + UNREACHABLE_MSG("Failed to initialize SDL camera subsystem: {}", SDL_GetError()); + } SDL_InitSubSystem(SDL_INIT_AUDIO); SDL_PropertiesID props = SDL_CreateProperties(); From edd50ab2d0a68391b6057c82aee605735350e375 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sun, 22 Mar 2026 10:26:23 +0200 Subject: [PATCH 34/44] more files for syncing with 0.15.1 branch (#4161) --- src/common/elf_info.h | 6 +++++ src/common/memory_patcher.cpp | 1 - src/core/file_sys/fs.cpp | 1 - src/core/libraries/gnmdriver/gnmdriver.cpp | 9 ++++--- src/core/libraries/gnmdriver/gnmdriver.h | 2 +- src/core/libraries/network/net_resolver.cpp | 4 +-- src/core/libraries/np/np_trophy_error.h | 1 + src/core/libraries/np/trophy_ui.cpp | 5 ---- src/core/libraries/pad/pad_errors.h | 5 +++- .../sysmodule/sysmodule_internal.cpp | 25 ++++++++++--------- .../system_gesture/system_gesture.cpp | 1 - .../vk_pipeline_serialization.cpp | 2 +- .../renderer_vulkan/vk_platform.cpp | 2 +- .../texture_cache/texture_cache.cpp | 8 +++--- 14 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/common/elf_info.h b/src/common/elf_info.h index b84f36ecb..8f79c9e69 100644 --- a/src/common/elf_info.h +++ b/src/common/elf_info.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "assert.h" #include "bit_field.h" @@ -73,6 +74,7 @@ class ElfInfo { std::filesystem::path splash_path{}; std::filesystem::path game_folder{}; + std::vector npCommIds{}; public: static constexpr u32 FW_10 = 0x1000000; @@ -139,6 +141,10 @@ public: [[nodiscard]] const std::filesystem::path& GetGameFolder() const { return game_folder; } + + [[nodiscard]] const std::vector GetNpCommIds() const { + return npCommIds; + } }; } // namespace Common diff --git a/src/common/memory_patcher.cpp b/src/common/memory_patcher.cpp index a7c020246..2517e3f22 100644 --- a/src/common/memory_patcher.cpp +++ b/src/common/memory_patcher.cpp @@ -8,7 +8,6 @@ #include #include #include -#include "common/config.h" #include "common/elf_info.h" #include "common/logging/log.h" #include "common/path_util.h" diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index 96a04ee5e..2fdf8c10b 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/string_util.h" #include "core/file_sys/devices/logger.h" #include "core/file_sys/devices/nop_device.h" diff --git a/src/core/libraries/gnmdriver/gnmdriver.cpp b/src/core/libraries/gnmdriver/gnmdriver.cpp index 1bb034dd0..f8886e3ff 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.cpp +++ b/src/core/libraries/gnmdriver/gnmdriver.cpp @@ -1172,13 +1172,14 @@ bool PS4_SYSV_ABI sceGnmIsUserPaEnabled() { } int PS4_SYSV_ABI sceGnmLogicalCuIndexToPhysicalCuIndex() { - LOG_ERROR(Lib_GnmDriver, "(STUBBED) called"); + LOG_TRACE(Lib_GnmDriver, "called"); + // Not available in retail firmware return ORBIS_OK; } -int PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask() { - LOG_ERROR(Lib_GnmDriver, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask(s64, s32 logical_cu_mask) { + LOG_INFO(Lib_GnmDriver, "called, logical_cu_mask: {}", logical_cu_mask); + return logical_cu_mask; } int PS4_SYSV_ABI sceGnmLogicalTcaUnitToPhysical() { diff --git a/src/core/libraries/gnmdriver/gnmdriver.h b/src/core/libraries/gnmdriver/gnmdriver.h index 4ece58ebd..2208ca05d 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.h +++ b/src/core/libraries/gnmdriver/gnmdriver.h @@ -121,7 +121,7 @@ s32 PS4_SYSV_ABI sceGnmInsertWaitFlipDone(u32* cmdbuf, u32 size, s32 vo_handle, int PS4_SYSV_ABI sceGnmIsCoredumpValid(); bool PS4_SYSV_ABI sceGnmIsUserPaEnabled(); int PS4_SYSV_ABI sceGnmLogicalCuIndexToPhysicalCuIndex(); -int PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask(); +s32 PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask(s64, s32 logical_cu_mask); int PS4_SYSV_ABI sceGnmLogicalTcaUnitToPhysical(); int PS4_SYSV_ABI sceGnmMapComputeQueue(u32 pipe_id, u32 queue_id, VAddr ring_base_addr, u32 ring_size_dw, u32* read_ptr_addr); diff --git a/src/core/libraries/network/net_resolver.cpp b/src/core/libraries/network/net_resolver.cpp index 7eb4c4001..a66f1519b 100644 --- a/src/core/libraries/network/net_resolver.cpp +++ b/src/core/libraries/network/net_resolver.cpp @@ -2,9 +2,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" -#include "common/config.h" #include "common/singleton.h" #include "common/types.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "net_error.h" #include "net_resolver.h" @@ -27,7 +27,7 @@ int Resolver::ResolveAsync(const char* hostname, OrbisNetInAddr* addr, int timeo } void Resolver::Resolve() { - if (!Config::getIsConnectedToNetwork()) { + if (!EmulatorSettings.IsConnectedToNetwork()) { resolution_error = ORBIS_NET_ERROR_RESOLVER_ENODNS; return; } diff --git a/src/core/libraries/np/np_trophy_error.h b/src/core/libraries/np/np_trophy_error.h index 8ac356225..173d7dd59 100644 --- a/src/core/libraries/np/np_trophy_error.h +++ b/src/core/libraries/np/np_trophy_error.h @@ -47,3 +47,4 @@ constexpr int ORBIS_NP_TROPHY_ERROR_INCONSISTENT_TITLE_CONF = 0x80551628; constexpr int ORBIS_NP_TROPHY_ERROR_TITLE_BACKGROUND = 0x80551629; constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISABLED = 0x8055162B; constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISPLAY_BUFFER_NOT_IN_USE = 0x8055162D; +constexpr int ORBIS_NP_TROPHY_ERROR_TITLE_NOT_FOUND = 0x805516C2; diff --git a/src/core/libraries/np/trophy_ui.cpp b/src/core/libraries/np/trophy_ui.cpp index 6ca24b6d5..a4fd21a33 100644 --- a/src/core/libraries/np/trophy_ui.cpp +++ b/src/core/libraries/np/trophy_ui.cpp @@ -7,11 +7,6 @@ #include #include #include - -#ifdef ENABLE_QT_GUI -#include -#endif - #include "common/assert.h" #include "common/path_util.h" #include "common/singleton.h" diff --git a/src/core/libraries/pad/pad_errors.h b/src/core/libraries/pad/pad_errors.h index 182c89219..bb2a2e9ac 100644 --- a/src/core/libraries/pad/pad_errors.h +++ b/src/core/libraries/pad/pad_errors.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -20,3 +20,6 @@ constexpr int ORBIS_PAD_ERROR_INVALID_BUFFER_LENGTH = 0x80920102; constexpr int ORBIS_PAD_ERROR_INVALID_REPORT_LENGTH = 0x80920103; constexpr int ORBIS_PAD_ERROR_INVALID_REPORT_ID = 0x80920104; constexpr int ORBIS_PAD_ERROR_SEND_AGAIN = 0x80920105; + +constexpr s32 ORBIS_DEVICE_SERVICE_ERROR_INVALID_USER = 0x809b0001; +constexpr s32 ORBIS_DEVICE_SERVICE_ERROR_USER_NOT_LOGIN = 0x809b0081; diff --git a/src/core/libraries/sysmodule/sysmodule_internal.cpp b/src/core/libraries/sysmodule/sysmodule_internal.cpp index def410e25..b2853e2fa 100644 --- a/src/core/libraries/sysmodule/sysmodule_internal.cpp +++ b/src/core/libraries/sysmodule/sysmodule_internal.cpp @@ -2,9 +2,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/file_sys/fs.h" #include "core/libraries/disc_map/disc_map.h" #include "core/libraries/font/font.h" @@ -108,7 +108,7 @@ bool validateModuleId(s32 id) { } // Cannot load debug modules on retail hardware. - if (isDebugModule(id) && !Config::isDevKitConsole()) { + if (isDebugModule(id) && !EmulatorSettings.IsDevKit()) { return ORBIS_SYSMODULE_INVALID_ID; } @@ -154,7 +154,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { std::string mod_name = std::string(mod.name); // libSceGnmDriver case - if (index == 0xd && Config::isDevKitConsole()) { + if (index == 0xd && EmulatorSettings.IsDevKit()) { // There are some other checks involved here that I am not familiar with. // Since we're not exactly running libSceGnmDriver LLE, this shouldn't matter too much. mod_name.append("_padebug"); @@ -168,7 +168,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { } // libSceVrTracker case - if (index == 0xb3 && Config::isDevKitConsole()) { + if (index == 0xb3 && EmulatorSettings.IsDevKit()) { mod_name.append("_debug"); } @@ -178,7 +178,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { // PS4 Pro running in enhanced mode mod_name.append("ForNeoMode"); } else if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsNeo) != 0 && - Config::isNeoModeConsole()) { + EmulatorSettings.IsNeo()) { // PS4 Pro running in base mode mod_name.append("ForNeo"); } @@ -188,7 +188,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { // Now we need to check if the requested library is allowed to LLE. // First, we allow all modules from game-specific sys_modules - const auto& sys_module_path = Config::getSysModulesPath(); + const auto& sys_module_path = EmulatorSettings.GetSysModulesDir(); const auto& game_specific_module_path = sys_module_path / game_info->GameSerial() / mod_name; if (std::filesystem::exists(game_specific_module_path)) { @@ -223,7 +223,8 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { {"libSceAudiodec.sprx", nullptr}, {"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont}, {"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt}, - {"libSceFreeTypeOt.sprx", nullptr}}); + {"libSceFreeTypeOt.sprx", nullptr}, + {"libScePadTracker.sprx", nullptr}}); // Iterate through the allowed array const auto it = std::ranges::find_if( @@ -299,7 +300,7 @@ s32 loadModule(s32 id, s32 argc, const void* argv, s32* res_out) { for (s64 i = requested_module.num_to_load - 1; i >= 0; i--) { // Modules flagged as debug modules only load for devkits u32 mod_index = requested_module.to_load[i]; - if ((!Config::isDevKitConsole() && + if ((!EmulatorSettings.IsDevKit() && g_modules_array[mod_index].flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0) { continue; } @@ -361,7 +362,7 @@ s32 unloadModule(s32 id, s32 argc, const void* argv, s32* res_out, bool is_inter OrbisSysmoduleModuleInternal dep_mod = g_modules_array[mod.to_load[i]]; // If this is a debug module and we're not emulating a devkit, skip it. if ((dep_mod.flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0 && - !Config::isDevKitConsole()) { + !EmulatorSettings.IsDevKit()) { continue; } @@ -398,7 +399,7 @@ s32 preloadModulesForLibkernel() { // These are skipped unless this console is a devkit. if ((module_index == 0x12 || module_index == 0x1e || module_index == 0x24 || module_index == 0x26) && - !Config::isDevKitConsole()) { + !EmulatorSettings.IsDevKit()) { continue; } @@ -409,13 +410,13 @@ s32 preloadModulesForLibkernel() { // libSceDbgAssist is skipped on non-testkit consoles. // For now, stub check to non-devkit. - if (module_index == 0x23 && !Config::isDevKitConsole()) { + if (module_index == 0x23 && !EmulatorSettings.IsDevKit()) { continue; } // libSceRazorCpu, skipped for old non-devkit consoles. if (module_index == 0x25 && sdk_ver < Common::ElfInfo::FW_45 && - !Config::isDevKitConsole()) { + !EmulatorSettings.IsDevKit()) { continue; } diff --git a/src/core/libraries/system_gesture/system_gesture.cpp b/src/core/libraries/system_gesture/system_gesture.cpp index 304a11612..a6455e05e 100644 --- a/src/core/libraries/system_gesture/system_gesture.cpp +++ b/src/core/libraries/system_gesture/system_gesture.cpp @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/logging/log.h" #include "common/singleton.h" #include "core/libraries/libs.h" diff --git a/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp b/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp index f36d9c61c..eb31d4994 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/serdes.h" diff --git a/src/video_core/renderer_vulkan/vk_platform.cpp b/src/video_core/renderer_vulkan/vk_platform.cpp index 7f02fcaf1..c77c80223 100644 --- a/src/video_core/renderer_vulkan/vk_platform.cpp +++ b/src/video_core/renderer_vulkan/vk_platform.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later // Include the vulkan platform specific header diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 8163902cc..163712756 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -1,12 +1,12 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/scope_exit.h" +#include "core/emulator_settings.h" #include "core/memory.h" #include "video_core/buffer_cache/buffer_cache.h" #include "video_core/page_manager.h" @@ -641,7 +641,7 @@ ImageView& TextureCache::FindTexture(ImageId image_id, const ImageDesc& desc) { Image& image = slot_images[image_id]; if (desc.type == BindingType::Storage) { image.flags |= ImageFlagBits::GpuModified; - if (Config::readbackLinearImages() && !image.info.props.is_tiled && + if (EmulatorSettings.IsReadbackLinearImagesEnabled() && !image.info.props.is_tiled && image.info.guest_address != 0) { download_images.emplace(image_id); } @@ -653,7 +653,7 @@ ImageView& TextureCache::FindTexture(ImageId image_id, const ImageDesc& desc) { ImageView& TextureCache::FindRenderTarget(ImageId image_id, const ImageDesc& desc) { Image& image = slot_images[image_id]; image.flags |= ImageFlagBits::GpuModified; - if (Config::readbackLinearImages() && !image.info.props.is_tiled) { + if (EmulatorSettings.IsReadbackLinearImagesEnabled() && !image.info.props.is_tiled) { download_images.emplace(image_id); } image.usage.render_target = 1u; From d0a4718fb9bb8de3068f89f346c553b20c00e3af Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Mon, 23 Mar 2026 13:52:33 +0200 Subject: [PATCH 35/44] General Misses (0.15.1 WIP part 5) (#4164) * more from 0.15.1 WIP branch * fixup --- src/core/libraries/kernel/memory.cpp | 33 +++++++++++++++++++ src/core/libraries/np/np_matching2.cpp | 2 +- .../libraries/save_data/save_instance.cpp | 3 +- src/core/libraries/system/systemservice.cpp | 1 - src/core/libraries/system/userservice.cpp | 8 +++++ src/core/libraries/system/userservice.h | 4 ++- src/core/linker.h | 19 +++++------ src/core/user_manager.cpp | 21 ++++++++++++ src/core/user_manager.h | 2 ++ 9 files changed, 79 insertions(+), 14 deletions(-) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 378064e44..5d94735ae 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -318,6 +318,38 @@ s32 PS4_SYSV_ABI sceKernelMapFlexibleMemory(void** addr_in_out, u64 len, s32 pro return sceKernelMapNamedFlexibleMemory(addr_in_out, len, prot, flags, "anon"); } +s32 PS4_SYSV_ABI sceKernelMapNamedSystemFlexibleMemory(void** addr_in_out, u64 len, s32 prot, + s32 flags, const char* name) { + LOG_INFO(Kernel_Vmm, "in_addr = {}, len = {:#x}, prot = {:#x}, flags = {:#x}, name = '{}'", + fmt::ptr(*addr_in_out), len, prot, flags, name); + if (len == 0 || !Common::Is16KBAligned(len)) { + LOG_ERROR(Kernel_Vmm, "len is 0 or not 16kb multiple"); + return ORBIS_KERNEL_ERROR_EINVAL; + } + + if (name == nullptr) { + LOG_ERROR(Kernel_Vmm, "name is invalid!"); + return ORBIS_KERNEL_ERROR_EFAULT; + } + + if (std::strlen(name) >= ORBIS_KERNEL_MAXIMUM_NAME_LENGTH) { + LOG_ERROR(Kernel_Vmm, "name exceeds 32 bytes!"); + return ORBIS_KERNEL_ERROR_ENAMETOOLONG; + } + + VAddr in_addr = reinterpret_cast(*addr_in_out); + if (in_addr == 0) { + in_addr = 0x880000000; + } + const auto mem_prot = static_cast(prot); + const auto map_flags = static_cast(flags); + auto* memory = Core::Memory::Instance(); + const auto ret = memory->MapMemory(addr_in_out, in_addr, len, mem_prot, map_flags, + Core::VMAType::Stack, name); + LOG_INFO(Kernel_Vmm, "out_addr = {}", fmt::ptr(*addr_in_out)); + return ret; +} + s32 PS4_SYSV_ABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, u32* prot) { auto* memory = Core::Memory::Instance(); return memory->QueryProtection(std::bit_cast(addr), start, end, prot); @@ -833,6 +865,7 @@ void RegisterMemory(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("PGhQHd-dzv8", "libkernel", 1, "libkernel", sceKernelMmap); LIB_FUNCTION("cQke9UuBQOk", "libkernel", 1, "libkernel", sceKernelMunmap); LIB_FUNCTION("mL8NDH86iQI", "libkernel", 1, "libkernel", sceKernelMapNamedFlexibleMemory); + LIB_FUNCTION("kc+LEEIYakc", "libkernel", 1, "libkernel", sceKernelMapNamedSystemFlexibleMemory); LIB_FUNCTION("aNz11fnnzi4", "libkernel", 1, "libkernel", sceKernelAvailableFlexibleMemorySize); LIB_FUNCTION("aNz11fnnzi4", "libkernel_avlfmem", 1, "libkernel", sceKernelAvailableFlexibleMemorySize); diff --git a/src/core/libraries/np/np_matching2.cpp b/src/core/libraries/np/np_matching2.cpp index bf9b7b7d0..dcd2a9c23 100644 --- a/src/core/libraries/np/np_matching2.cpp +++ b/src/core/libraries/np/np_matching2.cpp @@ -4,7 +4,7 @@ #include #include -#include "common/config.h" +#include "common/logging/log.h" #include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" diff --git a/src/core/libraries/save_data/save_instance.cpp b/src/core/libraries/save_data/save_instance.cpp index baeec5d2c..4ff682357 100644 --- a/src/core/libraries/save_data/save_instance.cpp +++ b/src/core/libraries/save_data/save_instance.cpp @@ -9,6 +9,7 @@ #include "common/config.h" #include "common/path_util.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/file_sys/fs.h" #include "save_backup.h" #include "save_instance.h" @@ -71,7 +72,7 @@ fs::path SaveInstance::GetParamSFOPath(const fs::path& dir_path) { void SaveInstance::SetupDefaultParamSFO(PSF& param_sfo, std::string dir_name, std::string game_serial) { - int locale = Config::GetLanguage(); + int locale = EmulatorSettings.GetConsoleLanguage(); if (!default_title.contains(locale)) { locale = 1; // default to en_US if not found } diff --git a/src/core/libraries/system/systemservice.cpp b/src/core/libraries/system/systemservice.cpp index f03c7e7cb..0e40c723f 100644 --- a/src/core/libraries/system/systemservice.cpp +++ b/src/core/libraries/system/systemservice.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/singleton.h" #include "core/emulator_settings.h" #include "core/file_sys/fs.h" diff --git a/src/core/libraries/system/userservice.cpp b/src/core/libraries/system/userservice.cpp index 508b1d7e5..b82549c27 100644 --- a/src/core/libraries/system/userservice.cpp +++ b/src/core/libraries/system/userservice.cpp @@ -4,6 +4,7 @@ #include "common/config.h" #include "common/logging/log.h" +#include #include "core/libraries/libs.h" #include "core/libraries/system/userservice.h" #include "core/libraries/system/userservice_error.h" @@ -105,6 +106,13 @@ int PS4_SYSV_ABI sceUserServiceGetDiscPlayerFlag() { return ORBIS_OK; } +std::queue user_service_event_queue = {}; + +void AddUserServiceEvent(const OrbisUserServiceEvent e) { + LOG_DEBUG(Lib_UserService, "Event added to queue: {} {}", (u8)e.event, e.userId); + user_service_event_queue.push(e); +} + s32 PS4_SYSV_ABI sceUserServiceGetEvent(OrbisUserServiceEvent* event) { LOG_TRACE(Lib_UserService, "(DUMMY) called"); // fake a loggin event diff --git a/src/core/libraries/system/userservice.h b/src/core/libraries/system/userservice.h index 30920e002..799bf89ba 100644 --- a/src/core/libraries/system/userservice.h +++ b/src/core/libraries/system/userservice.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later // reference : // https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/master/include/orbis/_types/user.h @@ -57,6 +57,8 @@ struct OrbisUserServiceEvent { OrbisUserServiceUserId userId; }; +void AddUserServiceEvent(const OrbisUserServiceEvent e); + int PS4_SYSV_ABI sceUserServiceInitializeForShellCore(); int PS4_SYSV_ABI sceUserServiceTerminateForShellCore(); int PS4_SYSV_ABI sceUserServiceDestroyUser(); diff --git a/src/core/linker.h b/src/core/linker.h index 3cb59d9ee..895901f08 100644 --- a/src/core/linker.h +++ b/src/core/linker.h @@ -54,17 +54,16 @@ struct EntryParams { }; struct HeapAPI { - PS4_SYSV_ABI void* (*heap_malloc)(size_t); + PS4_SYSV_ABI void* (*heap_malloc)(u64); PS4_SYSV_ABI void (*heap_free)(void*); - PS4_SYSV_ABI void* (*heap_calloc)(size_t, size_t); - PS4_SYSV_ABI void* (*heap_realloc)(void*, size_t); - PS4_SYSV_ABI void* (*heap_memalign)(size_t, size_t); - PS4_SYSV_ABI int (*heap_posix_memalign)(void**, size_t, size_t); - // NOTE: Fields below may be inaccurate - PS4_SYSV_ABI int (*heap_reallocalign)(void); - PS4_SYSV_ABI void (*heap_malloc_stats)(void); - PS4_SYSV_ABI int (*heap_malloc_stats_fast)(void); - PS4_SYSV_ABI size_t (*heap_malloc_usable_size)(void*); + PS4_SYSV_ABI void* (*heap_calloc)(u64, u64); + PS4_SYSV_ABI void* (*heap_realloc)(void*, u64); + PS4_SYSV_ABI void* (*heap_memalign)(u64, u64); + PS4_SYSV_ABI s32 (*heap_posix_memalign)(void**, u64, u64); + PS4_SYSV_ABI s32 (*heap_reallocalign)(void*, u64, u64); + PS4_SYSV_ABI s32 (*heap_malloc_stats)(void*); + PS4_SYSV_ABI s32 (*heap_malloc_stats_fast)(void*); + PS4_SYSV_ABI u64 (*heap_malloc_usable_size)(void*); }; using AppHeapAPI = HeapAPI*; diff --git a/src/core/user_manager.cpp b/src/core/user_manager.cpp index 9d0829fc3..73e881e8e 100644 --- a/src/core/user_manager.cpp +++ b/src/core/user_manager.cpp @@ -172,6 +172,27 @@ LoggedInUsers UserManager::GetLoggedInUsers() const { return logged_in_users; } +using namespace Libraries::UserService; + +void UserManager::LoginUser(User* u, s32 player_index) { + if (!u) { + return; + } + u->logged_in = true; + // u->player_index = player_index; + AddUserServiceEvent({OrbisUserServiceEventType::Login, u->user_id}); + logged_in_users[player_index - 1] = u; +} + +void UserManager::LogoutUser(User* u) { + if (!u) { + return; + } + u->logged_in = false; + AddUserServiceEvent({OrbisUserServiceEventType::Logout, u->user_id}); + logged_in_users[u->player_index - 1] = {}; +} + bool UserManager::Save() const { return UserSettings.Save(); } \ No newline at end of file diff --git a/src/core/user_manager.h b/src/core/user_manager.h index 77f612016..9273ef0cb 100644 --- a/src/core/user_manager.h +++ b/src/core/user_manager.h @@ -42,6 +42,8 @@ public: void SetControllerPort(u32 user_id, int port); std::vector GetValidUsers() const; LoggedInUsers GetLoggedInUsers() const; + void LoginUser(User* u, s32 player_index); + void LogoutUser(User* u); Users& GetUsers() { return m_users; From f450405f3538fc1ded91a8b36e470eee62cfde9c Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Mon, 23 Mar 2026 22:38:05 +0200 Subject: [PATCH 36/44] General Misses (0.15.1 WIP part 6) (#4165) * fixes * fixed a few more config misses * missed include --- src/core/emulator_settings.cpp | 5 ++- src/core/emulator_settings.h | 2 ++ src/core/libraries/kernel/memory.cpp | 33 ------------------- src/core/libraries/np/np_manager.cpp | 3 +- .../sysmodule/sysmodule_internal.cpp | 3 +- src/input/input_handler.cpp | 9 ++--- src/sdl_window.cpp | 5 +-- 7 files changed, 17 insertions(+), 43 deletions(-) diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp index c1c0342ea..8f26485e6 100644 --- a/src/core/emulator_settings.cpp +++ b/src/core/emulator_settings.cpp @@ -93,7 +93,8 @@ void EmulatorSettingsImpl::PrintChangedSummary(const std::vector& c EmulatorSettingsImpl::EmulatorSettingsImpl() = default; EmulatorSettingsImpl::~EmulatorSettingsImpl() { - Save(); + if (m_loaded) + Save(); } std::shared_ptr EmulatorSettingsImpl::GetInstance() { @@ -380,6 +381,7 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { SDL_ShowMessageBox(&msg_box, &result); if (result == 1) { if (TransferSettings()) { + m_loaded = true; Save(); return true; } else { @@ -397,6 +399,7 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { if (GetConfigVersion() != Common::g_scm_rev) { Save(); } + m_loaded = true; return true; } else { // ── Per-game override file ───────────────────────────────── diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index 0490aba77..fab94c6ff 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -465,6 +465,8 @@ private: VulkanSettings m_vulkan{}; ConfigMode m_configMode{ConfigMode::Default}; + bool m_loaded{false}; + static std::shared_ptr s_instance; static std::mutex s_mutex; diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 5d94735ae..378064e44 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -318,38 +318,6 @@ s32 PS4_SYSV_ABI sceKernelMapFlexibleMemory(void** addr_in_out, u64 len, s32 pro return sceKernelMapNamedFlexibleMemory(addr_in_out, len, prot, flags, "anon"); } -s32 PS4_SYSV_ABI sceKernelMapNamedSystemFlexibleMemory(void** addr_in_out, u64 len, s32 prot, - s32 flags, const char* name) { - LOG_INFO(Kernel_Vmm, "in_addr = {}, len = {:#x}, prot = {:#x}, flags = {:#x}, name = '{}'", - fmt::ptr(*addr_in_out), len, prot, flags, name); - if (len == 0 || !Common::Is16KBAligned(len)) { - LOG_ERROR(Kernel_Vmm, "len is 0 or not 16kb multiple"); - return ORBIS_KERNEL_ERROR_EINVAL; - } - - if (name == nullptr) { - LOG_ERROR(Kernel_Vmm, "name is invalid!"); - return ORBIS_KERNEL_ERROR_EFAULT; - } - - if (std::strlen(name) >= ORBIS_KERNEL_MAXIMUM_NAME_LENGTH) { - LOG_ERROR(Kernel_Vmm, "name exceeds 32 bytes!"); - return ORBIS_KERNEL_ERROR_ENAMETOOLONG; - } - - VAddr in_addr = reinterpret_cast(*addr_in_out); - if (in_addr == 0) { - in_addr = 0x880000000; - } - const auto mem_prot = static_cast(prot); - const auto map_flags = static_cast(flags); - auto* memory = Core::Memory::Instance(); - const auto ret = memory->MapMemory(addr_in_out, in_addr, len, mem_prot, map_flags, - Core::VMAType::Stack, name); - LOG_INFO(Kernel_Vmm, "out_addr = {}", fmt::ptr(*addr_in_out)); - return ret; -} - s32 PS4_SYSV_ABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, u32* prot) { auto* memory = Core::Memory::Instance(); return memory->QueryProtection(std::bit_cast(addr), start, end, prot); @@ -865,7 +833,6 @@ void RegisterMemory(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("PGhQHd-dzv8", "libkernel", 1, "libkernel", sceKernelMmap); LIB_FUNCTION("cQke9UuBQOk", "libkernel", 1, "libkernel", sceKernelMunmap); LIB_FUNCTION("mL8NDH86iQI", "libkernel", 1, "libkernel", sceKernelMapNamedFlexibleMemory); - LIB_FUNCTION("kc+LEEIYakc", "libkernel", 1, "libkernel", sceKernelMapNamedSystemFlexibleMemory); LIB_FUNCTION("aNz11fnnzi4", "libkernel", 1, "libkernel", sceKernelAvailableFlexibleMemorySize); LIB_FUNCTION("aNz11fnnzi4", "libkernel_avlfmem", 1, "libkernel", sceKernelAvailableFlexibleMemorySize); diff --git a/src/core/libraries/np/np_manager.cpp b/src/core/libraries/np/np_manager.cpp index 229ae33af..62cad455b 100644 --- a/src/core/libraries/np/np_manager.cpp +++ b/src/core/libraries/np/np_manager.cpp @@ -5,6 +5,7 @@ #include #include +#include #include "common/config.h" #include "common/logging/log.h" #include "core/libraries/error_codes.h" @@ -784,7 +785,7 @@ void DeregisterNpCallback(std::string key) { } void RegisterLib(Core::Loader::SymbolsResolver* sym) { - g_signed_in = Config::getPSNSignedIn(); + g_signed_in = EmulatorSettings.IsPSNSignedIn(); LIB_FUNCTION("GpLQDNKICac", "libSceNpManager", 1, "libSceNpManager", sceNpCreateRequest); LIB_FUNCTION("eiqMCt9UshI", "libSceNpManager", 1, "libSceNpManager", sceNpCreateAsyncRequest); diff --git a/src/core/libraries/sysmodule/sysmodule_internal.cpp b/src/core/libraries/sysmodule/sysmodule_internal.cpp index b2853e2fa..56e130289 100644 --- a/src/core/libraries/sysmodule/sysmodule_internal.cpp +++ b/src/core/libraries/sysmodule/sysmodule_internal.cpp @@ -223,8 +223,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { {"libSceAudiodec.sprx", nullptr}, {"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont}, {"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt}, - {"libSceFreeTypeOt.sprx", nullptr}, - {"libScePadTracker.sprx", nullptr}}); + {"libSceFreeTypeOt.sprx", nullptr}}); // Iterate through the allowed array const auto it = std::ranges::find_if( diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index e44693fbf..fbda6e394 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -23,6 +23,7 @@ #include "common/io_file.h" #include "common/path_util.h" #include "core/devtools/layer.h" +#include "core/emulator_settings.h" #include "core/emulator_state.h" #include "input/controller.h" #include "input/input_mouse.h" @@ -598,13 +599,13 @@ void ControllerOutput::FinalizeUpdate() { PushSDLEvent(SDL_EVENT_RDOC_CAPTURE); break; case HOTKEY_VOLUME_UP: - Config::setVolumeSlider(std::clamp(Config::getVolumeSlider() + 10, 0, 500), - is_game_specific); + EmulatorSettings.SetVolumeSlider( + std::clamp(EmulatorSettings.GetVolumeSlider() + 10, 0, 500)); Overlay::ShowVolume(); break; case HOTKEY_VOLUME_DOWN: - Config::setVolumeSlider(std::clamp(Config::getVolumeSlider() - 10, 0, 500), - is_game_specific); + EmulatorSettings.SetVolumeSlider( + std::clamp(EmulatorSettings.GetVolumeSlider() - 10, 0, 500)); Overlay::ShowVolume(); break; case HOTKEY_QUIT: diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 0c7374bfb..89b65f3dc 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -24,6 +24,7 @@ #ifdef __APPLE__ #include "SDL3/SDL_metal.h" #endif +#include namespace Input { @@ -323,9 +324,9 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ } if (!error) { SDL_SetWindowFullscreenMode( - window, Config::getFullscreenMode() == "Fullscreen" ? displayMode : NULL); + window, EmulatorSettings.GetFullScreenMode() == "Fullscreen" ? displayMode : NULL); } - SDL_SetWindowFullscreen(window, Config::getIsFullscreen()); + SDL_SetWindowFullscreen(window, EmulatorSettings.IsFullScreen()); SDL_SyncWindow(window); SDL_InitSubSystem(SDL_INIT_GAMEPAD); From bb9c223d42ed4ba5e6a1fb7cfb2fe97a27eaaefb Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:40:18 -0500 Subject: [PATCH 37/44] Core: Minor code cleanup (#4166) * Log filters cleanup * Clearer dialog options for config update * Smaller button labels These don't auto-resize, and I don't want to read SDL's docs for something so small. --- src/common/logging/filter.cpp | 5 +---- src/common/logging/types.h | 7 ++----- src/core/emulator_settings.cpp | 38 +++++++++++++++++----------------- src/core/emulator_settings.h | 18 ++++++++-------- src/core/user_settings.cpp | 12 +++++------ 5 files changed, 37 insertions(+), 43 deletions(-) diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index f2597603e..fd48faf72 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -68,6 +68,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(Common) \ SUB(Common, Filesystem) \ SUB(Common, Memory) \ + CLS(KeyManager) \ CLS(Core) \ SUB(Core, Linker) \ SUB(Core, Devices) \ @@ -80,7 +81,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Kernel, Event) \ SUB(Kernel, Sce) \ CLS(Lib) \ - SUB(Lib, LibC) \ SUB(Lib, LibcInternal) \ SUB(Lib, Kernel) \ SUB(Lib, Pad) \ @@ -117,7 +117,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, NpSnsFacebookDialog) \ SUB(Lib, NpPartner) \ SUB(Lib, Screenshot) \ - SUB(Lib, LibCInternal) \ SUB(Lib, AppContent) \ SUB(Lib, Rtc) \ SUB(Lib, Rudp) \ @@ -163,8 +162,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(ImGui) \ CLS(Input) \ CLS(Tty) \ - CLS(KeyManager) \ - CLS(EmuSettings) \ CLS(Loader) // GetClassName is a macro defined by Windows.h, grrr... diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 4c6e53453..2c6edef3b 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -34,6 +34,7 @@ enum class Class : u8 { Common, ///< Library routines Common_Filesystem, ///< Filesystem interface library Common_Memory, ///< Memory mapping and management functions + KeyManager, ///< Key management system Core, ///< LLE emulation core Core_Linker, ///< The module linker Core_Devices, ///< Devices emulation @@ -44,10 +45,9 @@ enum class Class : u8 { Kernel_Fs, ///< The filesystem implementation of the kernel. Kernel_Vmm, ///< The virtual memory implementation of the kernel. Kernel_Event, ///< The event management implementation of the kernel. - Kernel_Sce, ///< The sony specific interfaces provided by the kernel. + Kernel_Sce, ///< The Sony-specific interfaces provided by the kernel. Lib, ///< HLE implementation of system library. Each major library ///< should have its own subclass. - Lib_LibC, ///< The LibC implementation. Lib_LibcInternal, ///< The LibcInternal implementation. Lib_Kernel, ///< The LibKernel implementation. Lib_Pad, ///< The LibScePad implementation. @@ -83,7 +83,6 @@ enum class Class : u8 { Lib_NpProfileDialog, ///< The LibSceNpProfileDialog implementation Lib_NpSnsFacebookDialog, ///< The LibSceNpSnsFacebookDialog implementation Lib_Screenshot, ///< The LibSceScreenshot implementation - Lib_LibCInternal, ///< The LibCInternal implementation. Lib_AppContent, ///< The LibSceAppContent implementation. Lib_Rtc, ///< The LibSceRtc implementation. Lib_Rudp, ///< The LibSceRudp implementation. @@ -131,8 +130,6 @@ enum class Class : u8 { Loader, ///< ROM loader Input, ///< Input emulation Tty, ///< Debug output from emu - KeyManager, ///< Key management system - EmuSettings, /// Emulator settings system Count ///< Total number of logging classes }; diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp index 8f26485e6..f738dc1b5 100644 --- a/src/core/emulator_settings.cpp +++ b/src/core/emulator_settings.cpp @@ -81,12 +81,12 @@ std::optional get_optional(const toml::value& v, const std::string& key) { void EmulatorSettingsImpl::PrintChangedSummary(const std::vector& changed) { if (changed.empty()) { - LOG_DEBUG(EmuSettings, "No game-specific overrides applied"); + LOG_DEBUG(Config, "No game-specific overrides applied"); return; } - LOG_DEBUG(EmuSettings, "Game-specific overrides applied:"); + LOG_DEBUG(Config, "Game-specific overrides applied:"); for (const auto& k : changed) - LOG_DEBUG(EmuSettings, " * {}", k); + LOG_DEBUG(Config, " * {}", k); } // ── Singleton ──────────────────────────────────────────────────────── @@ -212,7 +212,7 @@ void EmulatorSettingsImpl::ClearGameSpecificOverrides() { ClearGroupOverrides(m_audio); ClearGroupOverrides(m_gpu); ClearGroupOverrides(m_vulkan); - LOG_DEBUG(EmuSettings, "All game-specific overrides cleared"); + LOG_DEBUG(Config, "All game-specific overrides cleared"); } void EmulatorSettingsImpl::ResetGameSpecificValue(const std::string& key) { @@ -238,7 +238,7 @@ void EmulatorSettingsImpl::ResetGameSpecificValue(const std::string& key) { return; if (tryGroup(m_vulkan)) return; - LOG_WARNING(EmuSettings, "ResetGameSpecificValue: key '{}' not found", key); + LOG_WARNING(Config, "ResetGameSpecificValue: key '{}' not found", key); } bool EmulatorSettingsImpl::Save(const std::string& serial) { @@ -276,7 +276,7 @@ bool EmulatorSettingsImpl::Save(const std::string& serial) { std::ofstream out(path); if (!out) { - LOG_ERROR(EmuSettings, "Failed to open game config for writing: {}", path.string()); + LOG_ERROR(Config, "Failed to open game config for writing: {}", path.string()); return false; } out << std::setw(2) << j; @@ -317,14 +317,14 @@ bool EmulatorSettingsImpl::Save(const std::string& serial) { std::ofstream out(path); if (!out) { - LOG_ERROR(EmuSettings, "Failed to open config for writing: {}", path.string()); + LOG_ERROR(Config, "Failed to open config for writing: {}", path.string()); return false; } out << std::setw(2) << existing; return !out.fail(); } } catch (const std::exception& e) { - LOG_ERROR(EmuSettings, "Error saving settings: {}", e.what()); + LOG_ERROR(Config, "Error saving settings: {}", e.what()); return false; } } @@ -337,7 +337,7 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { // ── Global config ────────────────────────────────────────── const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); const auto configPath = userDir / "config.json"; - LOG_DEBUG(EmuSettings, "Loading global config from: {}", configPath.string()); + LOG_DEBUG(Config, "Loading global config from: {}", configPath.string()); if (std::ifstream in{configPath}; in.good()) { json gj; @@ -358,13 +358,13 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { mergeGroup(m_gpu, "GPU"); mergeGroup(m_vulkan, "Vulkan"); - LOG_DEBUG(EmuSettings, "Global config loaded successfully"); + LOG_DEBUG(Config, "Global config loaded successfully"); } else { if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml")) { SDL_MessageBoxButtonData btns[2]{ - {0, 0, "No"}, - {0, 1, "Yes"}, + {0, 0, "Defaults"}, + {0, 1, "Update"}, }; SDL_MessageBoxData msg_box{ 0, @@ -392,7 +392,7 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { } } } - LOG_DEBUG(EmuSettings, "Global config not found - using defaults"); + LOG_DEBUG(Config, "Global config not found - using defaults"); SetDefaultValues(); Save(); } @@ -408,16 +408,16 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { // base configuration. const auto gamePath = Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (serial + ".json"); - LOG_DEBUG(EmuSettings, "Applying game config: {}", gamePath.string()); + LOG_DEBUG(Config, "Applying game config: {}", gamePath.string()); if (!std::filesystem::exists(gamePath)) { - LOG_DEBUG(EmuSettings, "No game-specific config found for {}", serial); + LOG_DEBUG(Config, "No game-specific config found for {}", serial); return false; } std::ifstream in(gamePath); if (!in) { - LOG_ERROR(EmuSettings, "Failed to open game config: {}", gamePath.string()); + LOG_ERROR(Config, "Failed to open game config: {}", gamePath.string()); return false; } @@ -448,7 +448,7 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) { return true; } } catch (const std::exception& e) { - LOG_ERROR(EmuSettings, "Error loading settings: {}", e.what()); + LOG_ERROR(Config, "Error loading settings: {}", e.what()); return false; } } @@ -611,7 +611,7 @@ bool EmulatorSettingsImpl::TransferSettings() { } s.install_dirs.value = settings_install_dirs; } catch (const std::exception& e) { - LOG_WARNING(EmuSettings, "Failed to transfer install directories: {}", e.what()); + LOG_WARNING(Config, "Failed to transfer install directories: {}", e.what()); } // Transfer addon install directory @@ -627,7 +627,7 @@ bool EmulatorSettingsImpl::TransferSettings() { } } } catch (const std::exception& e) { - LOG_WARNING(EmuSettings, "Failed to transfer addon install directory: {}", e.what()); + LOG_WARNING(Config, "Failed to transfer addon install directory: {}", e.what()); } } diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index fab94c6ff..ad9cfe227 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -112,26 +112,26 @@ inline OverrideItem make_override(const char* key, Setting Struct::* member) return OverrideItem{ key, [member, key](void* base, const nlohmann::json& entry, std::vector& changed) { - LOG_DEBUG(EmuSettings, "[make_override] Processing key: {}", key); - LOG_DEBUG(EmuSettings, "[make_override] Entry JSON: {}", entry.dump()); + LOG_DEBUG(Config, "[make_override] Processing key: {}", key); + LOG_DEBUG(Config, "[make_override] Entry JSON: {}", entry.dump()); Struct* obj = reinterpret_cast(base); Setting& dst = obj->*member; try { T newValue = entry.get(); - LOG_DEBUG(EmuSettings, "[make_override] Parsed value: {}", newValue); - LOG_DEBUG(EmuSettings, "[make_override] Current value: {}", dst.value); + LOG_DEBUG(Config, "[make_override] Parsed value: {}", newValue); + LOG_DEBUG(Config, "[make_override] Current value: {}", dst.value); if (dst.value != newValue) { std::ostringstream oss; oss << key << " ( " << dst.value << " → " << newValue << " )"; changed.push_back(oss.str()); - LOG_DEBUG(EmuSettings, "[make_override] Recorded change: {}", oss.str()); + LOG_DEBUG(Config, "[make_override] Recorded change: {}", oss.str()); } dst.game_specific_value = newValue; - LOG_DEBUG(EmuSettings, "[make_override] Successfully updated {}", key); + LOG_DEBUG(Config, "[make_override] Successfully updated {}", key); } catch (const std::exception& e) { - LOG_ERROR(EmuSettings, "[make_override] ERROR parsing {}: {}", key, e.what()); - LOG_ERROR(EmuSettings, "[make_override] Entry was: {}", entry.dump()); - LOG_ERROR(EmuSettings, "[make_override] Type name: {}", entry.type_name()); + LOG_ERROR(Config, "[make_override] ERROR parsing {}: {}", key, e.what()); + LOG_ERROR(Config, "[make_override] Entry was: {}", entry.dump()); + LOG_ERROR(Config, "[make_override] Type name: {}", entry.type_name()); } }, diff --git a/src/core/user_settings.cpp b/src/core/user_settings.cpp index a2142dd9a..cf569a68a 100644 --- a/src/core/user_settings.cpp +++ b/src/core/user_settings.cpp @@ -44,13 +44,13 @@ bool UserSettingsImpl::Save() const { std::ofstream out(path); if (!out) { - LOG_ERROR(EmuSettings, "Failed to open user settings for writing: {}", path.string()); + LOG_ERROR(Config, "Failed to open user settings for writing: {}", path.string()); return false; } out << std::setw(2) << j; return !out.fail(); } catch (const std::exception& e) { - LOG_ERROR(EmuSettings, "Error saving user settings: {}", e.what()); + LOG_ERROR(Config, "Error saving user settings: {}", e.what()); return false; } } @@ -59,7 +59,7 @@ bool UserSettingsImpl::Load() { const auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "users.json"; try { if (!std::filesystem::exists(path)) { - LOG_DEBUG(EmuSettings, "User settings file not found: {}", path.string()); + LOG_DEBUG(Config, "User settings file not found: {}", path.string()); // Create default user if no file exists if (m_userManager.GetUsers().user.empty()) { m_userManager.GetUsers() = m_userManager.CreateDefaultUsers(); @@ -70,7 +70,7 @@ bool UserSettingsImpl::Load() { std::ifstream in(path); if (!in) { - LOG_ERROR(EmuSettings, "Failed to open user settings: {}", path.string()); + LOG_ERROR(Config, "Failed to open user settings: {}", path.string()); return false; } @@ -97,10 +97,10 @@ bool UserSettingsImpl::Load() { Save(); } - LOG_DEBUG(EmuSettings, "User settings loaded successfully"); + LOG_DEBUG(Config, "User settings loaded successfully"); return true; } catch (const std::exception& e) { - LOG_ERROR(EmuSettings, "Error loading user settings: {}", e.what()); + LOG_ERROR(Config, "Error loading user settings: {}", e.what()); // Fall back to defaults if (m_userManager.GetUsers().user.empty()) { m_userManager.GetUsers() = m_userManager.CreateDefaultUsers(); From 3cc56bf84ce03dc049baa200a10a449ca84afe2d Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:43:30 -0500 Subject: [PATCH 38/44] More verbose errors for file opening failures (#4173) * Verbose errors for file opening failures These can be pretty helpful, and games don't usually spam them outside loading screens. * oops --- src/core/libraries/kernel/file_system.cpp | 14 +++++++++++++- .../libraries/libc_internal/libc_internal_io.cpp | 6 +++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index 184343801..085e18b9f 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -87,11 +87,13 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { if (!read && !write && !rdwr) { // Start by checking for invalid flags. *__Error() = POSIX_EINVAL; + LOG_ERROR(Kernel_Fs, "Opening path {} failed, invalid flags {:#x}", raw_path, flags); return -1; } if (strlen(raw_path) > 255) { *__Error() = POSIX_ENAMETOOLONG; + LOG_ERROR(Kernel_Fs, "Opening path {} failed, path is too long", raw_path); return -1; } @@ -137,6 +139,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Error if file exists h->DeleteHandle(handle); *__Error() = POSIX_EEXIST; + LOG_ERROR(Kernel_Fs, "Creating {} failed, file already exists", raw_path); return -1; } @@ -145,6 +148,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Can't create files in a read only directory h->DeleteHandle(handle); *__Error() = POSIX_EROFS; + LOG_ERROR(Kernel_Fs, "Creating {} failed, path is read-only", raw_path); return -1; } // Create a file if it doesn't exist @@ -154,6 +158,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // If we're not creating a file, and it doesn't exist, return ENOENT h->DeleteHandle(handle); *__Error() = POSIX_ENOENT; + LOG_ERROR(Kernel_Fs, "Opening path {} failed, file does not exist", raw_path); return -1; } @@ -169,6 +174,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // This will trigger when create & directory is specified, this is expected. h->DeleteHandle(handle); *__Error() = POSIX_ENOTDIR; + LOG_ERROR(Kernel_Fs, "Opening directory {} failed, file is not a directory", raw_path); return -1; } @@ -176,6 +182,8 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Cannot open directories with any type of write access h->DeleteHandle(handle); *__Error() = POSIX_EISDIR; + LOG_ERROR(Kernel_Fs, "Opening directory {} failed, cannot open directories for writing", + raw_path); return -1; } @@ -183,6 +191,8 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Cannot open directories with truncate h->DeleteHandle(handle); *__Error() = POSIX_EISDIR; + LOG_ERROR(Kernel_Fs, "Opening directory {} failed, cannot truncate directories", + raw_path); return -1; } @@ -201,6 +211,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Can't open files with truncate flag in a read only directory h->DeleteHandle(handle); *__Error() = POSIX_EROFS; + LOG_ERROR(Kernel_Fs, "Truncating {} failed, path is read-only", raw_path); return -1; } else if (truncate) { // Open the file as read-write so we can truncate regardless of flags. @@ -219,6 +230,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Can't open files with write/read-write access in a read only directory h->DeleteHandle(handle); *__Error() = POSIX_EROFS; + LOG_ERROR(Kernel_Fs, "Opening {} for writing failed, path is read-only", raw_path); return -1; } else if (write) { if (append) { @@ -244,6 +256,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Open failed in platform-specific code, errno needs to be converted. h->DeleteHandle(handle); SetPosixErrno(e); + LOG_ERROR(Kernel_Fs, "Opening {} failed, error = {}", raw_path, *__Error()); return -1; } @@ -258,7 +271,6 @@ s32 PS4_SYSV_ABI posix_open(const char* filename, s32 flags, u16 mode) { s32 PS4_SYSV_ABI sceKernelOpen(const char* path, s32 flags, /* SceKernelMode*/ u16 mode) { s32 result = open(path, flags, mode); if (result < 0) { - LOG_ERROR(Kernel_Fs, "error = {}", *__Error()); return ErrnoToSceKernelError(*__Error()); } return result; diff --git a/src/core/libraries/libc_internal/libc_internal_io.cpp b/src/core/libraries/libc_internal/libc_internal_io.cpp index 504ba5b48..0bb23eb78 100644 --- a/src/core/libraries/libc_internal/libc_internal_io.cpp +++ b/src/core/libraries/libc_internal/libc_internal_io.cpp @@ -199,7 +199,11 @@ OrbisFILE* PS4_SYSV_ABI internal_fopen(const char* path, const char* mode) { std::scoped_lock lk{g_file_mtx}; LOG_INFO(Lib_LibcInternal, "called, path {}, mode {}", path, mode); OrbisFILE* file = internal__Fofind(); - return internal__Foprep(path, mode, file, -1, 0, 0); + OrbisFILE* ret_file = internal__Foprep(path, mode, file, -1, 0, 0); + if (ret_file == nullptr) { + LOG_ERROR(Lib_LibcInternal, "failed to open file {}", path); + } + return ret_file; } s32 PS4_SYSV_ABI internal_fflush(OrbisFILE* file) { From 650652db4213cfef60f4c2dcdee2942d572ec344 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Thu, 26 Mar 2026 06:13:43 +0800 Subject: [PATCH 39/44] Vblank frequency setting fix + utf 8 encoding for paths (#4174) * vblank frequency setting fix * force utf-8 encoding upon json serialization/deserialization for path strings --- src/core/emulator_settings.cpp | 9 ++++++--- src/core/emulator_settings.h | 15 ++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp index f738dc1b5..79d9a86f8 100644 --- a/src/core/emulator_settings.cpp +++ b/src/core/emulator_settings.cpp @@ -25,10 +25,13 @@ namespace nlohmann { template <> struct adl_serializer { static void to_json(json& j, const std::filesystem::path& p) { - j = p.string(); + const auto u8 = p.u8string(); + j = std::string(reinterpret_cast(u8.data()), u8.size()); } static void from_json(const json& j, std::filesystem::path& p) { - p = j.get(); + const std::string s = j.get(); + p = std::filesystem::path( + std::u8string_view(reinterpret_cast(s.data()), s.size())); } }; } // namespace nlohmann @@ -647,4 +650,4 @@ std::vector EmulatorSettingsImpl::GetAllOverrideableKeys() const { addGroup(m_gpu.GetOverrideableFields()); addGroup(m_vulkan.GetOverrideableFields()); return keys; -} \ No newline at end of file +} diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index ad9cfe227..370fb0ab0 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -594,16 +594,17 @@ public: SETTING_FORWARD_BOOL_READONLY(m_gpu, PatchShaders, patch_shaders) u32 GetVblankFrequency() { - if (m_gpu.vblank_frequency.value < 60) { - m_gpu.vblank_frequency.value = 60; + if (m_gpu.vblank_frequency.value < 30) { + return 30; } - return m_gpu.vblank_frequency.value; + return m_gpu.vblank_frequency.get(); } - void SetVblankFrequency(const u32& v) { - if (v < 60) { - m_gpu.vblank_frequency.value = 60; + void SetVblankFrequency(const u32& v, bool is_specific = false) { + u32 val = v < 30 ? 30 : v; + if (is_specific) { + m_gpu.vblank_frequency.game_specific_value = val; } else { - m_gpu.vblank_frequency.value = v; + m_gpu.vblank_frequency.value = val; } } From 31b2d9ccd77a9ca1bc3fde66f0d214429c9b5811 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 26 Mar 2026 00:31:53 +0200 Subject: [PATCH 40/44] Introducing testing suite using google test (#4170) * initial tests for testing Setting * argg * keep switch off * added ci * fix ci * merging tests to build.yml * fixing linux ttests/? * one more try for ci * more linux fix for ci * more ci fixes * try to fix ctests now * should fix now * trying fixing linux tests * some more tests * more tests (68 tests) and clang format --- .github/workflows/build.yml | 77 +++ CMakeLists.txt | 8 + tests/.clang-format | 259 ++++++++++ tests/CMakeLists.txt | 84 ++++ tests/stubs/log_stub.cpp | 27 + tests/stubs/scm_rev_stub.cpp | 22 + tests/stubs/sdl_stub.cpp | 18 + tests/test_emulator_settings.cpp | 837 +++++++++++++++++++++++++++++++ 8 files changed, 1332 insertions(+) create mode 100644 tests/.clang-format create mode 100644 tests/CMakeLists.txt create mode 100644 tests/stubs/log_stub.cpp create mode 100644 tests/stubs/scm_rev_stub.cpp create mode 100644 tests/stubs/sdl_stub.cpp create mode 100644 tests/test_emulator_settings.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26c8e20fd..96f5e33d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,8 @@ on: - "documents/**" - "**/*.md" + workflow_dispatch: + concurrency: group: ci-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'push' }} @@ -65,6 +67,81 @@ jobs: echo "shorthash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "fullhash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + test: + name: Run C++ Tests on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + include: + - os: windows-latest + compiler_cxx: clang-cl + compiler_c: clang-cl + - os: ubuntu-latest + compiler_cxx: clang++ + compiler_c: clang + - os: macos-latest + compiler_cxx: clang++ + compiler_c: clang + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup CMake + uses: lukka/get-cmake@latest + + - name: Setup Visual Studio shell (Windows only) + if: runner.os == 'Windows' + uses: egor-tensin/vs-shell@v2 + with: + arch: x64 + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y ninja-build libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev libxrandr-dev libxfixes-dev libudev-dev uuid-dev uuid-dev + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew install ninja + + - name: Configure CMake + run: | + cmake -B build -G Ninja \ + -DCMAKE_CXX_COMPILER="${{ matrix.compiler_cxx }}" \ + -DCMAKE_C_COMPILER="${{ matrix.compiler_c }}" \ + -DCMAKE_BUILD_TYPE=Debug \ + -DENABLE_TESTS=ON \ + ${{ runner.os == 'macOS' && '-DCMAKE_OSX_ARCHITECTURES=x86_64' || '' }} + shell: bash + + - name: Create shadPS4 user data directory (Linux) + if: runner.os == 'Linux' + run: mkdir -p ~/.local/share/shadPS4 + + - name: Create shadPS4 user data directory (macOS) + if: runner.os == 'macOS' + run: mkdir -p ~/Library/Application\ Support/shadPS4 + + - name: Create shadPS4 user data directory (Windows) + if: runner.os == 'Windows' + run: mkdir -p "$APPDATA/shadPS4" + shell: bash + + - name: Build all tests + run: cmake --build build + shell: bash + + - name: Run tests with CTest + run: ctest --test-dir build --output-on-failure --progress + shell: bash + windows-sdl: runs-on: windows-2025 needs: get-info diff --git a/CMakeLists.txt b/CMakeLists.txt index 06f8cb6ff..cbf373e57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ endif() option(ENABLE_DISCORD_RPC "Enable the Discord RPC integration" ON) option(ENABLE_UPDATER "Enables the options to updater" ON) +option(ENABLE_TESTS "Build unit tests (requires GTest)" OFF) # First, determine whether to use CMAKE_OSX_ARCHITECTURES or CMAKE_SYSTEM_PROCESSOR. if (APPLE AND CMAKE_OSX_ARCHITECTURES) @@ -1114,6 +1115,8 @@ set(EMULATOR src/emulator.cpp src/sdl_window.cpp ) +if(NOT ENABLE_TESTS) + add_executable(shadps4 ${AUDIO_CORE} ${IMGUI} @@ -1267,3 +1270,8 @@ endif() # Install rules install(TARGETS shadps4 BUNDLE DESTINATION .) + +else() + enable_testing() + add_subdirectory(tests) +endif() \ No newline at end of file diff --git a/tests/.clang-format b/tests/.clang-format new file mode 100644 index 000000000..d69044b0f --- /dev/null +++ b/tests/.clang-format @@ -0,0 +1,259 @@ +# SPDX-FileCopyrightText: 2016 Emmanuel Gil Peyrot +# SPDX-License-Identifier: GPL-2.0-or-later + +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +IncludeCategories: + - Regex: '^\<[^Q][^/.>]*\>' + Priority: -2 + - Regex: '^\<' + Priority: -1 + - Regex: '^\"' + Priority: 0 +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 150 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Never +--- +Language: Java +# BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 100 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +IncludeCategories: + - Regex: '^\<[^Q][^/.>]*\>' + Priority: -2 + - Regex: '^\<' + Priority: -1 + - Regex: '^\"' + Priority: 0 +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 150 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never +--- +Language: ObjC +# BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 100 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +IncludeCategories: + - Regex: '^\<[^Q][^/.>]*\>' + Priority: -2 + - Regex: '^\<' + Priority: -1 + - Regex: '^\"' + Priority: 0 +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 150 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never +... diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 000000000..6b656762a --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +# Find or download Google Test +include(FetchContent) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip +) +FetchContent_MakeAvailable(googletest) + +set(SETTINGS_TEST_SOURCES + # Under test + ${CMAKE_SOURCE_DIR}/src/core/emulator_settings.cpp + ${CMAKE_SOURCE_DIR}/src/core/emulator_state.cpp + + # Minimal common support + ${CMAKE_SOURCE_DIR}/src/common/path_util.cpp + ${CMAKE_SOURCE_DIR}/src/common/assert.cpp + ${CMAKE_SOURCE_DIR}/src/common/error.cpp + ${CMAKE_SOURCE_DIR}/src/common/string_util.cpp + ${CMAKE_SOURCE_DIR}/src/common/logging/filter.cpp + ${CMAKE_SOURCE_DIR}/src/common/logging/text_formatter.cpp + + # Stubs that replace dependencies + stubs/log_stub.cpp + stubs/scm_rev_stub.cpp + stubs/sdl_stub.cpp + + # Tests + test_emulator_settings.cpp +) + +add_executable(shadps4_settings_test ${SETTINGS_TEST_SOURCES}) + +target_include_directories(shadps4_settings_test PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR} +) + +target_link_libraries(shadps4_settings_test PRIVATE + GTest::gtest_main + fmt::fmt + nlohmann_json::nlohmann_json + toml11::toml11 + SDL3::SDL3 +) + +target_compile_features(shadps4_settings_test PRIVATE cxx_std_23) + +target_compile_definitions(shadps4_settings_test PRIVATE BOOST_ASIO_STANDALONE) + +if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR + CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + include(CheckCXXSymbolExists) + check_cxx_symbol_exists(_LIBCPP_VERSION version LIBCPP) + if (LIBCPP) + target_compile_options(shadps4_settings_test PRIVATE -fexperimental-library) + endif() +endif() + +if (WIN32) + target_compile_definitions(shadps4_settings_test PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + NTDDI_VERSION=0x0A000006 + _WIN32_WINNT=0x0A00 + WINVER=0x0A00 + ) + if (MSVC) + target_compile_definitions(shadps4_settings_test PRIVATE + _CRT_SECURE_NO_WARNINGS + _CRT_NONSTDC_NO_DEPRECATE + _SCL_SECURE_NO_WARNINGS + _TIMESPEC_DEFINED + ) + endif() +endif() + +include(GoogleTest) +gtest_discover_tests(shadps4_settings_test + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + PROPERTIES TIMEOUT 60 +) diff --git a/tests/stubs/log_stub.cpp b/tests/stubs/log_stub.cpp new file mode 100644 index 000000000..64d6df644 --- /dev/null +++ b/tests/stubs/log_stub.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include "common/logging/backend.h" +#include "common/logging/filter.h" +#include "common/logging/log.h" + +namespace Common::Log { + + void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, + unsigned int line_num, const char* function, const char* format, + const fmt::format_args& args) { + } + + void Initialize(std::string_view) {} + bool IsActive() { return false; } + void SetGlobalFilter(const Filter&) {} + void SetColorConsoleBackendEnabled(bool) {} + void Start() {} + void Stop() {} + void Denitializer() {} + void SetAppend() {} + +} // namespace Common::Log diff --git a/tests/stubs/scm_rev_stub.cpp b/tests/stubs/scm_rev_stub.cpp new file mode 100644 index 000000000..0f8a5af03 --- /dev/null +++ b/tests/stubs/scm_rev_stub.cpp @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "common/scm_rev.h" + +namespace Common { + + constexpr char g_version[] = "0.0.0 TEST"; + constexpr bool g_is_release = false; + constexpr char g_scm_rev[] = "test_rev_hash"; + constexpr char g_scm_branch[] = "test_branch"; + constexpr char g_scm_desc[] = "test_desc"; + constexpr char g_scm_remote_name[] = "origin"; + constexpr char g_scm_remote_url[] = "https://github.com/test/shadPS4"; + constexpr char g_scm_date[] = "2026-03-23"; + + const std::string GetRemoteNameFromLink() { + return "test"; + } + +} // namespace Common diff --git a/tests/stubs/sdl_stub.cpp b/tests/stubs/sdl_stub.cpp new file mode 100644 index 000000000..859147696 --- /dev/null +++ b/tests/stubs/sdl_stub.cpp @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +extern "C" { + + bool SDL_ShowMessageBox(const SDL_MessageBoxData* /* messageboxdata */, int* buttonid) { + if (buttonid) *buttonid = 0; // "No",skip migration + return true; + } + + bool SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags /* flags */, const char* /* title */, + const char* /* message */, SDL_Window* /* window */) { + return true; + } + +} // extern "C" diff --git a/tests/test_emulator_settings.cpp b/tests/test_emulator_settings.cpp new file mode 100644 index 000000000..1a4a667b3 --- /dev/null +++ b/tests/test_emulator_settings.cpp @@ -0,0 +1,837 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "common/path_util.h" +#include "common/scm_rev.h" +#include "core/emulator_settings.h" +#include "core/emulator_state.h" + +namespace fs = std::filesystem; +using json = nlohmann::json; + +class TempDir { +public: + TempDir() { + auto ns = std::chrono::steady_clock::now().time_since_epoch().count(); + temp_path = fs::temp_directory_path() / ("shadps4_test_" + std::to_string(ns) + "_" + + std::to_string(reinterpret_cast(this))); + fs::create_directories(temp_path); + } + ~TempDir() { + std::error_code ec; + fs::remove_all(temp_path, ec); + } + const fs::path& path() const { + return temp_path; + } + +private: + fs::path temp_path; +}; + +static void WriteJson(const fs::path& p, const json& j) { + std::ofstream out(p); + ASSERT_TRUE(out.is_open()) << "Cannot write: " << p; + out << std::setw(2) << j; +} + +static json ReadJson(const fs::path& p) { + std::ifstream in(p); + EXPECT_TRUE(in.is_open()) << "Cannot read: " << p; + json j; + in >> j; + return j; +} + +class EmulatorSettingsTest : public ::testing::Test { +protected: + void SetUp() override { + temp_dir = std::make_unique(); + const fs::path root = temp_dir->path(); + + using PT = Common::FS::PathType; + const struct { + PT type; + const char* sub; + } dirs[] = { + {PT::UserDir, ""}, + {PT::LogDir, "log"}, + {PT::ScreenshotsDir, "screenshots"}, + {PT::ShaderDir, "shader"}, + {PT::GameDataDir, "data"}, + {PT::TempDataDir, "temp"}, + {PT::SysModuleDir, "sys_modules"}, + {PT::DownloadDir, "download"}, + {PT::CapturesDir, "captures"}, + {PT::CheatsDir, "cheats"}, + {PT::PatchesDir, "patches"}, + {PT::MetaDataDir, "game_data"}, + {PT::CustomTrophy, "custom_trophy"}, + {PT::CustomConfigs, "custom_configs"}, + {PT::CacheDir, "cache"}, + {PT::FontsDir, "fonts"}, + {PT::HomeDir, "home"}, + }; + for (const auto& d : dirs) { + fs::path p = d.sub[0] ? (root / d.sub) : root; + fs::create_directories(p); + Common::FS::SetUserPath(d.type, p); + } + + temp_state = std::make_shared(); + EmulatorState::SetInstance(temp_state); + + temp_settings = std::make_shared(); + EmulatorSettingsImpl::SetInstance(temp_settings); + } + + void TearDown() override { + EmulatorSettingsImpl::SetInstance(nullptr); + EmulatorState::SetInstance(nullptr); + temp_settings.reset(); + temp_state.reset(); + temp_dir.reset(); + } + + fs::path ConfigJson() const { + return temp_dir->path() / "config.json"; + } + fs::path GameConfig(const std::string& serial) const { + return temp_dir->path() / "custom_configs" / (serial + ".json"); + } + + std::unique_ptr temp_dir; + std::shared_ptr temp_settings; + std::shared_ptr temp_state; +}; + +// tests Settting template , default , Global override modes + +TEST(SettingTest, DefaultCtorZeroInitialises) { + Setting s; + EXPECT_EQ(s.value, 0); + EXPECT_EQ(s.default_value, 0); + EXPECT_FALSE(s.game_specific_value.has_value()); +} + +TEST(SettingTest, ValueCtorSetsBothValueAndDefault) { + Setting s{42}; + EXPECT_EQ(s.value, 42); + EXPECT_EQ(s.default_value, 42); +} + +TEST(SettingTest, GetDefaultPrefersGameSpecificOverBase) { + Setting s{10}; + s.value = 20; + s.game_specific_value = 99; + EXPECT_EQ(s.get(ConfigMode::Default), 99); +} + +TEST(SettingTest, GetDefaultFallsBackToBaseWhenNoOverride) { + Setting s{10}; + s.value = 20; + EXPECT_EQ(s.get(ConfigMode::Default), 20); +} + +TEST(SettingTest, GetGlobalIgnoresGameSpecific) { + Setting s{10}; + s.value = 20; + s.game_specific_value = 99; + EXPECT_EQ(s.get(ConfigMode::Global), 20); +} + +TEST(SettingTest, GetCleanAlwaysReturnsFactoryDefault) { + Setting s{10}; + s.value = 20; + s.game_specific_value = 99; + EXPECT_EQ(s.get(ConfigMode::Clean), 10); +} + +TEST(SettingTest, SetWritesToBaseOnly) { + Setting s{0}; + s.game_specific_value = 55; + s.set(77); + EXPECT_EQ(s.value, 77); + EXPECT_EQ(s.game_specific_value.value(), 55); // override untouched +} + +TEST(SettingTest, ResetGameSpecificClearsOverride) { + Setting s{0}; + s.game_specific_value = 55; + s.reset_game_specific(); + EXPECT_FALSE(s.game_specific_value.has_value()); + // base and default must be intact + EXPECT_EQ(s.value, 0); + EXPECT_EQ(s.default_value, 0); +} + +TEST(SettingTest, BoolSettingAllModes) { + Setting s{false}; + s.value = true; + s.game_specific_value = false; + EXPECT_FALSE(s.get(ConfigMode::Default)); + EXPECT_TRUE(s.get(ConfigMode::Global)); + EXPECT_FALSE(s.get(ConfigMode::Clean)); +} + +TEST(SettingTest, StringSettingAllModes) { + Setting s{"shadow"}; + s.value = "rule"; + s.game_specific_value = "override"; + EXPECT_EQ(s.get(ConfigMode::Default), "override"); + EXPECT_EQ(s.get(ConfigMode::Global), "rule"); + EXPECT_EQ(s.get(ConfigMode::Clean), "shadow"); +} + +TEST(SettingTest, NoGameSpecificDefaultAndGlobalAgree) { + Setting s{7}; + s.value = 7; + EXPECT_EQ(s.get(ConfigMode::Default), s.get(ConfigMode::Global)); +} + +// tests for default settings + +TEST_F(EmulatorSettingsTest, SetDefaultValuesResetsAllGroupsToFactory) { + // set random values + temp_settings->SetNeo(true); + temp_settings->SetWindowWidth(3840u); + temp_settings->SetGpuId(2); + temp_settings->SetDebugDump(true); + temp_settings->SetCursorState(HideCursorState::Always); + + temp_settings->SetDefaultValues(); // reset to defaults + // check if values are reset to defaults + EXPECT_FALSE(temp_settings->IsNeo()); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280u); + EXPECT_EQ(temp_settings->GetGpuId(), -1); + EXPECT_FALSE(temp_settings->IsDebugDump()); + EXPECT_EQ(temp_settings->GetCursorState(), static_cast(HideCursorState::Idle)); +} + +TEST_F(EmulatorSettingsTest, SetDefaultValuesClearsGameSpecificOverrides) { + // check that game-specific overrides are cleared by SetDefaultValues + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA00001"), game); + temp_settings->Load("CUSA00001"); + + temp_settings->SetDefaultValues(); + temp_settings->SetConfigMode(ConfigMode::Default); + + EXPECT_FALSE(temp_settings->IsNeo()); // default is false should be loaded instead of override +} + +// configModes tests + +TEST_F(EmulatorSettingsTest, ConfigModeSetAndGetRoundTrips) { + temp_settings->SetConfigMode(ConfigMode::Clean); + EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Clean); + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Global); + temp_settings->SetConfigMode(ConfigMode::Default); + EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Default); +} + +TEST_F(EmulatorSettingsTest, ConfigModeCleanReturnFactoryDefaults) { + temp_settings->SetWindowWidth(3840u); + json game; + game["GPU"]["window_width"] = 2560; + WriteJson(GameConfig("CUSA00001"), game); + temp_settings->Load("CUSA00001"); + + temp_settings->SetConfigMode(ConfigMode::Clean); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); // factory default +} + +TEST_F(EmulatorSettingsTest, ConfigModeGlobalIgnoresGameSpecific) { + temp_settings->SetNeo(false); + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA00001"), game); + temp_settings->Load("CUSA00001"); + + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_FALSE(temp_settings->IsNeo()); +} + +TEST_F(EmulatorSettingsTest, ConfigModeDefaultResolvesGameSpecificWhenPresent) { + temp_settings->SetNeo(false); + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA00001"), game); + temp_settings->Load("CUSA00001"); + + temp_settings->SetConfigMode(ConfigMode::Default); + EXPECT_TRUE(temp_settings->IsNeo()); +} + +// tests for global config.json file + +TEST_F(EmulatorSettingsTest, SaveCreatesConfigJson) { + ASSERT_TRUE(temp_settings->Save()); + EXPECT_TRUE(fs::exists(ConfigJson())); +} + +TEST_F(EmulatorSettingsTest, SaveWritesAllExpectedSections) { + ASSERT_TRUE(temp_settings->Save()); + json j = ReadJson(ConfigJson()); + for (const char* section : {"General", "Debug", "Input", "Audio", "GPU", "Vulkan"}) + EXPECT_TRUE(j.contains(section)) << "Missing section: " << section; +} + +TEST_F(EmulatorSettingsTest, LoadReturnsTrueForExistingFile) { + temp_settings->Save(); + auto fresh = std::make_shared(); + EmulatorSettingsImpl::SetInstance(fresh); + EXPECT_TRUE(fresh->Load()); +} + +TEST_F(EmulatorSettingsTest, RoundTripAllGroups) { + temp_settings->SetNeo(true); + temp_settings->SetDebugDump(true); + temp_settings->SetWindowWidth(1920u); + temp_settings->SetGpuId(1); + temp_settings->SetCursorState(HideCursorState::Always); + temp_settings->SetAudioBackend(AudioBackend::OpenAL); + temp_settings->Save(); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); + EXPECT_TRUE(f->IsNeo()); + EXPECT_TRUE(f->IsDebugDump()); + EXPECT_EQ(f->GetWindowWidth(), 1920u); + EXPECT_EQ(f->GetGpuId(), 1); + EXPECT_EQ(f->GetCursorState(), static_cast(HideCursorState::Always)); + EXPECT_EQ(f->GetAudioBackend(), static_cast(AudioBackend::OpenAL)); +} + +TEST_F(EmulatorSettingsTest, LoadMissingFileCreatesDefaultsOnDisk) { + ASSERT_FALSE(fs::exists(ConfigJson())); + temp_settings->Load(); + EXPECT_TRUE(fs::exists(ConfigJson())); + EXPECT_FALSE(temp_settings->IsNeo()); // defaults +} + +TEST_F(EmulatorSettingsTest, LoadMissingSectionDoesNotZeroOtherSections) { + temp_settings->SetNeo(true); + temp_settings->Save(); + json j = ReadJson(ConfigJson()); + j.erase("GPU"); + WriteJson(ConfigJson(), j); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); + + EXPECT_TRUE(f->IsNeo()); // belongs to General, should be loaded + EXPECT_EQ(f->GetWindowWidth(), 1280); // GPU fell back to default +} + +TEST_F(EmulatorSettingsTest, LoadPreservesUnknownKeysOnResave) { + temp_settings->Save(); + json j = ReadJson(ConfigJson()); + j["General"]["future_feature"] = "preserved"; + WriteJson(ConfigJson(), j); + + // A fresh load + save (triggered by version mismatch) must keep the key + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); + f->Save(); + + json after = ReadJson(ConfigJson()); + EXPECT_EQ(after["General"]["future_feature"], "preserved"); +} + +TEST_F(EmulatorSettingsTest, LoadUnknownTopLevelSectionPreserved) { + temp_settings->Save(); + json j = ReadJson(ConfigJson()); + j["FutureSection"]["key"] = 42; + WriteJson(ConfigJson(), j); + + temp_settings->SetNeo(true); + temp_settings->Save(); // merge path + + json after = ReadJson(ConfigJson()); + EXPECT_TRUE(after.contains("FutureSection")); + EXPECT_EQ(after["FutureSection"]["key"], 42); +} + +TEST_F(EmulatorSettingsTest, LoadCorruptJsonDoesNotCrash) { + { + std::ofstream out(ConfigJson()); + out << "{NOT VALID JSON!!!"; + } + EXPECT_NO_THROW(temp_settings->Load()); +} + +TEST_F(EmulatorSettingsTest, LoadEmptyJsonObjectDoesNotCrash) { + WriteJson(ConfigJson(), json::object()); + EXPECT_NO_THROW(temp_settings->Load()); +} + +// tests for per game config + +TEST_F(EmulatorSettingsTest, SaveSerialCreatesPerGameFile) { + ASSERT_TRUE(temp_settings->Save("CUSA01234")); + EXPECT_TRUE(fs::exists(GameConfig("CUSA01234"))); +} + +TEST_F(EmulatorSettingsTest, LoadSerialReturnsFalseWhenFileAbsent) { + EXPECT_FALSE(temp_settings->Load("CUSA99999")); +} + +TEST_F(EmulatorSettingsTest, LoadSerialAppliesOverrideToGameSpecificValue) { + temp_settings->SetNeo(false); + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA01234"), game); + + ASSERT_TRUE(temp_settings->Load("CUSA01234")); + temp_settings->SetConfigMode(ConfigMode::Default); + EXPECT_TRUE(temp_settings->IsNeo()); +} + +TEST_F(EmulatorSettingsTest, LoadSerialBaseValueUntouched) { + temp_settings->SetWindowWidth(1280); + json game; + game["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); +} + +TEST_F(EmulatorSettingsTest, LoadSerialOverridesMultipleGroups) { + temp_settings->SetNeo(false); + temp_settings->SetWindowWidth(1280u); + temp_settings->SetDebugDump(false); + + json game; + game["General"]["neo_mode"] = true; + game["GPU"]["window_width"] = 3840; + game["Debug"]["debug_dump"] = true; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->SetConfigMode(ConfigMode::Default); + EXPECT_TRUE(temp_settings->IsNeo()); + EXPECT_EQ(temp_settings->GetWindowWidth(), 3840); + EXPECT_TRUE(temp_settings->IsDebugDump()); +} + +TEST_F(EmulatorSettingsTest, LoadSerialUnrecognisedKeyIgnored) { + json game; + game["GPU"]["key_that_does_not_exist"] = 999; + WriteJson(GameConfig("CUSA01234"), game); + EXPECT_NO_THROW(temp_settings->Load("CUSA01234")); +} + +TEST_F(EmulatorSettingsTest, LoadSerialTypeMismatch_DoesNotCrash) { + json game; + game["GPU"]["window_width"] = "not_a_number"; + WriteJson(GameConfig("CUSA01234"), game); + EXPECT_NO_THROW(temp_settings->Load("CUSA01234")); + // base unchanged + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280u); +} + +TEST_F(EmulatorSettingsTest, LoadSerialCorruptFileDoesNotCrash) { + { + std::ofstream out(GameConfig("CUSA01234")); + out << "{{{{totally broken"; + } + EXPECT_NO_THROW(temp_settings->Load("CUSA01234")); +} + +TEST_F(EmulatorSettingsTest, SaveSerialWritesGameSpecificValueWhenOverrideLoaded) { + temp_settings->SetWindowWidth(1280); + json game; + game["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->Save("CUSA01234"); + + json saved = ReadJson(GameConfig("CUSA01234")); + EXPECT_EQ(saved["GPU"]["window_width"].get(), 3840); +} + +TEST_F(EmulatorSettingsTest, SaveSerialWritesBaseValueWhenNoOverrideSet) { + temp_settings->SetWindowWidth(2560); + temp_settings->Save("CUSA01234"); + + json saved = ReadJson(GameConfig("CUSA01234")); + EXPECT_EQ(saved["GPU"]["window_width"].get(), 2560); +} + +TEST_F(EmulatorSettingsTest, MultipleSerialsDoNotInterfere) { + json g1; + g1["General"]["neo_mode"] = true; + g1["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA00001"), g1); + + json g2; + g2["General"]["neo_mode"] = false; + g2["GPU"]["window_width"] = 1920; + WriteJson(GameConfig("CUSA00002"), g2); + + { + auto s = std::make_shared(); + EmulatorSettingsImpl::SetInstance(s); + s->Load(); + s->Load("CUSA00001"); + s->SetConfigMode(ConfigMode::Default); + EXPECT_TRUE(s->IsNeo()); + EXPECT_EQ(s->GetWindowWidth(), 3840); + } + { + auto s = std::make_shared(); + EmulatorSettingsImpl::SetInstance(s); + s->Load(); + s->Load("CUSA00002"); + s->SetConfigMode(ConfigMode::Default); + EXPECT_FALSE(s->IsNeo()); + EXPECT_EQ(s->GetWindowWidth(), 1920); + } +} + +// ClearGameSpecificOverrides tests + +TEST_F(EmulatorSettingsTest, ClearGameSpecificOverridesRemovesAllGroups) { + json game; + game["General"]["neo_mode"] = true; + game["GPU"]["window_width"] = 3840; + game["Debug"]["debug_dump"] = true; + game["Input"]["cursor_state"] = 2; + game["Audio"]["audio_backend"] = 1; + game["Vulkan"]["gpu_id"] = 2; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->ClearGameSpecificOverrides(); + temp_settings->SetConfigMode(ConfigMode::Default); + + EXPECT_FALSE(temp_settings->IsNeo()); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); + EXPECT_FALSE(temp_settings->IsDebugDump()); + EXPECT_EQ(temp_settings->GetCursorState(), static_cast(HideCursorState::Idle)); + EXPECT_EQ(temp_settings->GetGpuId(), -1); +} + +TEST_F(EmulatorSettingsTest, ClearGameSpecificOverridesDoesNotTouchBaseValues) { + temp_settings->SetWindowWidth(1920); + json game; + game["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->ClearGameSpecificOverrides(); + + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1920); +} + +TEST_F(EmulatorSettingsTest, ClearGameSpecificOverrides_NoopWhenNothingLoaded) { + EXPECT_NO_THROW(temp_settings->ClearGameSpecificOverrides()); +} + +// ResetGameSpecificValue tests + +TEST_F(EmulatorSettingsTest, ResetGameSpecificValue_ClearsNamedKey) { + temp_settings->SetWindowWidth(1280); + json game; + game["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->SetConfigMode(ConfigMode::Default); + ASSERT_EQ(temp_settings->GetWindowWidth(), 3840); + + temp_settings->ResetGameSpecificValue("window_width"); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); +} + +TEST_F(EmulatorSettingsTest, ResetGameSpecificValueOnlyAffectsTargetKey) { + json game; + game["GPU"]["window_width"] = 3840; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->ResetGameSpecificValue("window_width"); + temp_settings->SetConfigMode(ConfigMode::Default); + + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); // cleared + EXPECT_TRUE(temp_settings->IsNeo()); // still set +} + +TEST_F(EmulatorSettingsTest, ResetGameSpecificValueUnknownKeyNoOp) { + EXPECT_NO_THROW(temp_settings->ResetGameSpecificValue("does_not_exist")); +} + +// GameInstallDir tests + +TEST_F(EmulatorSettingsTest, AddGameInstallDirAddsEnabled) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + EXPECT_TRUE(temp_settings->AddGameInstallDir(dir)); + ASSERT_EQ(temp_settings->GetGameInstallDirs().size(), 1u); + EXPECT_EQ(temp_settings->GetGameInstallDirs()[0], dir); +} + +TEST_F(EmulatorSettingsTest, AddGameInstallDirRejectsDuplicate) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + temp_settings->AddGameInstallDir(dir); + EXPECT_FALSE(temp_settings->AddGameInstallDir(dir)); + EXPECT_EQ(temp_settings->GetGameInstallDirs().size(), 1u); +} + +TEST_F(EmulatorSettingsTest, RemoveGameInstallDirRemovesEntry) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + temp_settings->AddGameInstallDir(dir); + temp_settings->RemoveGameInstallDir(dir); + EXPECT_TRUE(temp_settings->GetGameInstallDirs().empty()); +} + +TEST_F(EmulatorSettingsTest, RemoveGameInstallDirNoopForMissing) { + EXPECT_NO_THROW(temp_settings->RemoveGameInstallDir("/nonexistent/path")); +} + +TEST_F(EmulatorSettingsTest, SetGameInstallDirEnabledDisablesDir) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + temp_settings->AddGameInstallDir(dir, true); + temp_settings->SetGameInstallDirEnabled(dir, false); + EXPECT_TRUE(temp_settings->GetGameInstallDirs().empty()); +} + +TEST_F(EmulatorSettingsTest, SetGameInstallDirEnabledReEnablesDir) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + temp_settings->AddGameInstallDir(dir, false); + ASSERT_TRUE(temp_settings->GetGameInstallDirs().empty()); + temp_settings->SetGameInstallDirEnabled(dir, true); + EXPECT_EQ(temp_settings->GetGameInstallDirs().size(), 1u); +} + +TEST_F(EmulatorSettingsTest, SetAllGameInstallDirsReplacesExistingList) { + fs::path d1 = temp_dir->path() / "g1"; + fs::path d2 = temp_dir->path() / "g2"; + fs::create_directories(d1); + fs::create_directories(d2); + temp_settings->AddGameInstallDir(d1); + + temp_settings->SetAllGameInstallDirs({{d2, true}}); + ASSERT_EQ(temp_settings->GetGameInstallDirs().size(), 1u); + EXPECT_EQ(temp_settings->GetGameInstallDirs()[0], d2); +} + +TEST_F(EmulatorSettingsTest, GameInstallDirsFullRoundTripWithEnabledFlags) { + fs::path d1 = temp_dir->path() / "g1"; + fs::path d2 = temp_dir->path() / "g2"; + fs::create_directories(d1); + fs::create_directories(d2); + temp_settings->AddGameInstallDir(d1, true); + temp_settings->AddGameInstallDir(d2, false); + temp_settings->Save(); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); + + const auto& all = f->GetAllGameInstallDirs(); + ASSERT_EQ(all.size(), 2u); + EXPECT_EQ(all[0].path, d1); + EXPECT_TRUE(all[0].enabled); + EXPECT_EQ(all[1].path, d2); + EXPECT_FALSE(all[1].enabled); +} + +TEST_F(EmulatorSettingsTest, GetGameInstallDirsEnabledReflectsState) { + fs::path d1 = temp_dir->path() / "g1"; + fs::path d2 = temp_dir->path() / "g2"; + fs::create_directories(d1); + fs::create_directories(d2); + temp_settings->AddGameInstallDir(d1, true); + temp_settings->AddGameInstallDir(d2, false); + + auto enabled = temp_settings->GetGameInstallDirsEnabled(); + ASSERT_EQ(enabled.size(), 2u); + EXPECT_TRUE(enabled[0]); + EXPECT_FALSE(enabled[1]); +} + +// GetAllOverrideableKeys tests + +TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysIsNonEmpty) { + EXPECT_FALSE(temp_settings->GetAllOverrideableKeys().empty()); +} + +TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysContainsRepresentativeKeys) { + auto keys = temp_settings->GetAllOverrideableKeys(); + auto has = [&](const char* k) { return std::find(keys.begin(), keys.end(), k) != keys.end(); }; + // General + EXPECT_TRUE(has("neo_mode")); + EXPECT_TRUE(has("volume_slider")); + // GPU + EXPECT_TRUE(has("window_width")); + EXPECT_TRUE(has("null_gpu")); + EXPECT_TRUE(has("vblank_frequency")); + // Vulkan + EXPECT_TRUE(has("gpu_id")); + EXPECT_TRUE(has("pipeline_cache_enabled")); + // Debug + EXPECT_TRUE(has("debug_dump")); + EXPECT_TRUE(has("log_enabled")); + // Input + EXPECT_TRUE(has("cursor_state")); + // Audio + EXPECT_TRUE(has("audio_backend")); +} + +TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysNoDuplicates) { + auto keys = temp_settings->GetAllOverrideableKeys(); + std::vector sorted = keys; + std::sort(sorted.begin(), sorted.end()); + auto it = std::unique(sorted.begin(), sorted.end()); + EXPECT_EQ(it, sorted.end()) << "Duplicate key found in overrideable keys list"; +} + +// Per-group GetOverrideableFields tests + +TEST_F(EmulatorSettingsTest, GetGeneralOverrideableFieldsNonEmpty) { + EXPECT_FALSE(temp_settings->GetGeneralOverrideableFields().empty()); +} + +TEST_F(EmulatorSettingsTest, GetGPUOverrideableFieldsContainsWindowAndFullscreen) { + auto fields = temp_settings->GetGPUOverrideableFields(); + auto has = [&](const char* k) { + return std::any_of(fields.begin(), fields.end(), + [k](const OverrideItem& f) { return std::string(f.key) == k; }); + }; + EXPECT_TRUE(has("window_width")); + EXPECT_TRUE(has("window_height")); + EXPECT_TRUE(has("full_screen")); + EXPECT_TRUE(has("vblank_frequency")); +} + +TEST_F(EmulatorSettingsTest, GetVulkanOverrideableFieldsContainsGpuId) { + auto fields = temp_settings->GetVulkanOverrideableFields(); + bool found = std::any_of(fields.begin(), fields.end(), + [](const OverrideItem& f) { return std::string(f.key) == "gpu_id"; }); + EXPECT_TRUE(found); +} + +// Path accessors tests +TEST_F(EmulatorSettingsTest, GetHomeDirReturnsCustomWhenSet) { + fs::path dir = temp_dir->path() / "custom_home"; + fs::create_directories(dir); + temp_settings->SetHomeDir(dir); + EXPECT_EQ(temp_settings->GetHomeDir(), dir); +} +TEST_F(EmulatorSettingsTest, GetSysModulesDirFallsBackToPathUtilWhenEmpty) { + // default_value is empty; GetSysModulesDir falls back to GetUserPath(SysModuleDir) + auto result = temp_settings->GetSysModulesDir(); + EXPECT_FALSE(result.empty()); +} +TEST_F(EmulatorSettingsTest, GetFontsDirFallsBackToPathUtilWhenEmpty) { + auto result = temp_settings->GetFontsDir(); + EXPECT_FALSE(result.empty()); +} + +// edge cases tests + +TEST_F(EmulatorSettingsTest, VersionMismatchPreservesSettings) { + temp_settings->SetNeo(true); + temp_settings->SetWindowWidth(2560u); + temp_settings->Save(); + + // Force a stale version string so the mismatch branch fires + json j = ReadJson(ConfigJson()); + j["Debug"]["config_version"] = "old_hash_0000"; + WriteJson(ConfigJson(), j); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); // triggers version-bump Save() internally + + EXPECT_TRUE(f->IsNeo()); + EXPECT_EQ(f->GetWindowWidth(), 2560u); +} + +TEST_F(EmulatorSettingsTest, DoubleGlobalLoadIsIdempotent) { + temp_settings->SetNeo(true); + temp_settings->SetWindowWidth(2560u); + temp_settings->Save(); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(""); // first — loads from disk + f->Load(""); // second — must not reset anything + + EXPECT_TRUE(f->IsNeo()); + EXPECT_EQ(f->GetWindowWidth(), 2560u); +} + +TEST_F(EmulatorSettingsTest, ConfigUsedFlagTrueWhenFileExists) { + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + EXPECT_TRUE(EmulatorState::GetInstance()->IsGameSpecifigConfigUsed()); +} + +TEST_F(EmulatorSettingsTest, ConfigUsedFlagFalseWhenFileAbsent) { + temp_settings->Load("CUSA99999"); + EXPECT_FALSE(EmulatorState::GetInstance()->IsGameSpecifigConfigUsed()); +} + +TEST_F(EmulatorSettingsTest, DestructorNoSaveIfLoadNeverCalled) { + temp_settings->SetNeo(true); + temp_settings->Save(); + auto t0 = fs::last_write_time(ConfigJson()); + + { + // Create and immediately destroy without calling Load() + auto untouched = std::make_shared(); + // destructor fires here + } + + auto t1 = fs::last_write_time(ConfigJson()); + EXPECT_EQ(t0, t1) << "Destructor wrote config.json without a prior Load()"; +} + +TEST_F(EmulatorSettingsTest, DestructorSavesAfterSuccessfulLoad) { + temp_settings->SetNeo(true); + temp_settings->Save(); + + { + auto s = std::make_shared(); + EmulatorSettingsImpl::SetInstance(s); + s->Load(); + s->SetWindowWidth(2560u); // mutate after successful load + // destructor should write this change + } + + auto verify = std::make_shared(); + EmulatorSettingsImpl::SetInstance(verify); + verify->Load(); + EXPECT_EQ(verify->GetWindowWidth(), 2560); +} From e0a86dc8f926df913b8b478b4ba897ddb8d27c79 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 26 Mar 2026 22:33:31 +0200 Subject: [PATCH 41/44] added transfer of sysmodules path and fonts path (#4175) --- src/core/emulator_settings.cpp | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp index 79d9a86f8..066c23af8 100644 --- a/src/core/emulator_settings.cpp +++ b/src/core/emulator_settings.cpp @@ -633,6 +633,41 @@ bool EmulatorSettingsImpl::TransferSettings() { LOG_WARNING(Config, "Failed to transfer addon install directory: {}", e.what()); } } + if (og_data.contains("General")) { + const toml::value& general = og_data.at("General"); + auto& s = m_general; + // Transfer sysmodules install directory + try { + std::string sysmodules_install_dir_str; + if (general.contains("sysModulesPath")) { + const auto& sysmodule_value = general.at("sysModulesPath"); + if (sysmodule_value.is_string()) { + sysmodules_install_dir_str = toml::get(sysmodule_value); + if (!sysmodules_install_dir_str.empty()) { + s.sys_modules_dir.value = std::filesystem::path{sysmodules_install_dir_str}; + } + } + } + } catch (const std::exception& e) { + LOG_WARNING(Config, "Failed to transfer sysmodules install directory: {}", e.what()); + } + + // Transfer font install directory + try { + std::string font_install_dir_str; + if (general.contains("fontsPath")) { + const auto& font_value = general.at("fontsPath"); + if (font_value.is_string()) { + font_install_dir_str = toml::get(font_value); + if (!font_install_dir_str.empty()) { + s.font_dir.value = std::filesystem::path{font_install_dir_str}; + } + } + } + } catch (const std::exception& e) { + LOG_WARNING(Config, "Failed to transfer font install directory: {}", e.what()); + } + } return true; } From 8a1500d7ad31fa0b3da45b5883c3ac4dd456c287 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:37:25 +0100 Subject: [PATCH 42/44] Local multiplayer support (#4169) * multiple controllers v2 * the c language formatter tool * review comments (the easy ones) * c++ copy semantics * correct error return for remotes without the system user id * update pad handle handling logic * controller override colour --- src/core/ipc/ipc.cpp | 7 - src/core/libraries/pad/pad.cpp | 258 ++++++++---- src/core/libraries/pad/pad.h | 4 +- src/core/libraries/system/userservice.cpp | 65 ++- src/emulator.cpp | 4 +- src/emulator.h | 4 +- src/imgui/renderer/imgui_impl_sdl3.cpp | 5 +- src/input/controller.cpp | 294 +++++++++---- src/input/controller.h | 125 ++++-- src/input/input_handler.cpp | 475 +++++++++++++++------- src/input/input_handler.h | 118 +++++- src/input/input_mouse.cpp | 14 +- src/sdl_window.cpp | 335 ++++----------- src/sdl_window.h | 25 +- 14 files changed, 1037 insertions(+), 696 deletions(-) diff --git a/src/core/ipc/ipc.cpp b/src/core/ipc/ipc.cpp index 489c34646..70180d3bf 100644 --- a/src/core/ipc/ipc.cpp +++ b/src/core/ipc/ipc.cpp @@ -211,13 +211,6 @@ void IPC::InputLoop() { } else if (cmd == "RELOAD_INPUTS") { std::string config = next_str(); Input::ParseInputConfig(config); - } else if (cmd == "SET_ACTIVE_CONTROLLER") { - std::string active_controller = next_str(); - GamepadSelect::SetSelectedGamepad(active_controller); - SDL_Event checkGamepad; - SDL_memset(&checkGamepad, 0, sizeof(checkGamepad)); - checkGamepad.type = SDL_EVENT_CHANGE_CONTROLLER; - SDL_PushEvent(&checkGamepad); } else { std::cerr << ";UNKNOWN CMD: " << cmd << std::endl; } diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index b31ed1f0b..f38621191 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -1,20 +1,24 @@ // SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/logging/log.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/libraries/libs.h" #include "core/libraries/pad/pad_errors.h" +#include "core/user_settings.h" #include "input/controller.h" #include "pad.h" namespace Libraries::Pad { using Input::GameController; +using Input::GameControllers; +using namespace Libraries::UserService; static bool g_initialized = false; -static bool g_opened = false; +static std::unordered_map user_id_pad_handle_map{}; +static constexpr s32 tv_remote_handle = 5; int PS4_SYSV_ABI scePadClose(s32 handle) { LOG_ERROR(Lib_Pad, "(STUBBED) called"); @@ -30,8 +34,8 @@ int PS4_SYSV_ABI scePadDeviceClassGetExtendedInformation( s32 handle, OrbisPadDeviceClassExtendedInformation* pExtInfo) { LOG_ERROR(Lib_Pad, "(STUBBED) called"); std::memset(pExtInfo, 0, sizeof(OrbisPadDeviceClassExtendedInformation)); - if (Config::getUseSpecialPad()) { - pExtInfo->deviceClass = (OrbisPadDeviceClass)Config::getSpecialPadClass(); + if (EmulatorSettings.IsUsingSpecialPad()) { + pExtInfo->deviceClass = (OrbisPadDeviceClass)EmulatorSettings.GetSpecialPadClass(); } return ORBIS_OK; } @@ -107,9 +111,9 @@ int PS4_SYSV_ABI scePadGetControllerInformation(s32 handle, OrbisPadControllerIn return ORBIS_OK; } pInfo->connected = true; - if (Config::getUseSpecialPad()) { + if (EmulatorSettings.IsUsingSpecialPad()) { pInfo->connectionType = ORBIS_PAD_PORT_TYPE_SPECIAL; - pInfo->deviceClass = (OrbisPadDeviceClass)Config::getSpecialPadClass(); + pInfo->deviceClass = (OrbisPadDeviceClass)EmulatorSettings.GetSpecialPadClass(); } return ORBIS_OK; } @@ -156,11 +160,16 @@ int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId if (!g_initialized) { return ORBIS_PAD_ERROR_NOT_INITIALIZED; } - if (userId == -1 || !g_opened) { + if (userId == -1) { return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE; } - LOG_DEBUG(Lib_Pad, "(DUMMY) called"); - return 1; + auto it = user_id_pad_handle_map.find(userId); + if (it == user_id_pad_handle_map.end()) { + return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE; + } + s32 pad_handle = it->second; + LOG_DEBUG(Lib_Pad, "called, userid: {}, out pad handle: {}", userId, pad_handle); + return pad_handle; } int PS4_SYSV_ABI scePadGetIdleCount() { @@ -168,8 +177,19 @@ int PS4_SYSV_ABI scePadGetIdleCount() { return ORBIS_OK; } -int PS4_SYSV_ABI scePadGetInfo() { - LOG_ERROR(Lib_Pad, "(STUBBED) called"); +int PS4_SYSV_ABI scePadGetInfo(u32* data) { + LOG_WARNING(Lib_Pad, "(DUMMY) called"); + if (!data) { + return ORBIS_PAD_ERROR_INVALID_ARG; + } + data[0] = 0x1; // index but starting from one? + data[1] = 0x0; // index? + data[2] = 1; // pad handle + data[3] = 0x0101; // ??? + data[4] = 0x0; // ? + data[5] = 0x0; // ? + data[6] = 0x00ff0000; // colour(?) + data[7] = 0x0; // ? return ORBIS_OK; } @@ -254,34 +274,61 @@ int PS4_SYSV_ABI scePadOpen(Libraries::UserService::OrbisUserServiceUserId userI if (!g_initialized) { return ORBIS_PAD_ERROR_NOT_INITIALIZED; } - if (userId == -1) { - return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE; + if (userId < 0) { + return ORBIS_DEVICE_SERVICE_ERROR_INVALID_USER; } - if (Config::getUseSpecialPad()) { + if (userId == ORBIS_USER_SERVICE_USER_ID_SYSTEM) { + if (type == ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) { + LOG_INFO(Lib_Pad, "Opened a TV remote device"); + user_id_pad_handle_map[ORBIS_USER_SERVICE_USER_ID_SYSTEM] = tv_remote_handle; + return tv_remote_handle; + } + return ORBIS_DEVICE_SERVICE_ERROR_INVALID_USER; + } + if (type == ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) { + return ORBIS_PAD_ERROR_INVALID_ARG; + } + if (EmulatorSettings.IsUsingSpecialPad()) { if (type != ORBIS_PAD_PORT_TYPE_SPECIAL) return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED; } else { - if (type != ORBIS_PAD_PORT_TYPE_STANDARD && type != ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) + if (type != ORBIS_PAD_PORT_TYPE_STANDARD) return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED; } - LOG_INFO(Lib_Pad, "(DUMMY) called user_id = {} type = {} index = {}", userId, type, index); - g_opened = true; - scePadResetLightBar(userId); - scePadResetOrientation(userId); - return 1; // dummy + auto u = UserManagement.GetUserByID(userId); + if (!u) { + return ORBIS_DEVICE_SERVICE_ERROR_USER_NOT_LOGIN; + } + s32 pad_handle = u->player_index; + LOG_INFO(Lib_Pad, "called user_id = {} type = {} index = {}, pad_handle = {}", userId, type, + index, pad_handle); + scePadResetLightBar(pad_handle); + scePadResetOrientation(pad_handle); + user_id_pad_handle_map[userId] = pad_handle; + return pad_handle; } int PS4_SYSV_ABI scePadOpenExt(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, s32 index, const OrbisPadOpenExtParam* pParam) { LOG_ERROR(Lib_Pad, "(STUBBED) called"); - if (Config::getUseSpecialPad()) { + if (EmulatorSettings.IsUsingSpecialPad()) { if (type != ORBIS_PAD_PORT_TYPE_SPECIAL) return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED; } else { if (type != ORBIS_PAD_PORT_TYPE_STANDARD && type != ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED; } - return 1; // dummy + auto u = UserManagement.GetUserByID(userId); + if (!u) { + return ORBIS_DEVICE_SERVICE_ERROR_USER_NOT_LOGIN; + } + s32 pad_handle = u->player_index; + LOG_INFO(Lib_Pad, "called user_id = {} type = {} index = {}, pad_handle = {}", userId, type, + index, pad_handle); + scePadResetLightBar(pad_handle); + scePadResetOrientation(pad_handle); + user_id_pad_handle_map[userId] = pad_handle; + return pad_handle; } int PS4_SYSV_ABI scePadOpenExt2() { @@ -294,8 +341,8 @@ int PS4_SYSV_ABI scePadOutputReport() { return ORBIS_OK; } -int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num, bool connected, - u32 connected_count) { +int ProcessStates(s32 handle, OrbisPadData* pData, Input::GameController& controller, + Input::State* states, s32 num, bool connected, u32 connected_count) { if (!connected) { pData[0] = {}; pData[0].orientation = {0.0f, 0.0f, 0.0f, 1.0f}; @@ -319,61 +366,57 @@ int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num pData[i].angularVelocity.z = states[i].angularVelocity.z; pData[i].orientation = {0.0f, 0.0f, 0.0f, 1.0f}; - auto* controller = Common::Singleton::Instance(); - const auto* engine = controller->GetEngine(); - if (engine && handle == 1) { - const auto gyro_poll_rate = engine->GetAccelPollRate(); - if (gyro_poll_rate != 0.0f) { - auto now = std::chrono::steady_clock::now(); - float deltaTime = std::chrono::duration_cast( - now - controller->GetLastUpdate()) - .count() / - 1000000.0f; - controller->SetLastUpdate(now); - Libraries::Pad::OrbisFQuaternion lastOrientation = controller->GetLastOrientation(); - Libraries::Pad::OrbisFQuaternion outputOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; - GameController::CalculateOrientation(pData->acceleration, pData->angularVelocity, - deltaTime, lastOrientation, outputOrientation); - pData[i].orientation = outputOrientation; - controller->SetLastOrientation(outputOrientation); - } + const auto gyro_poll_rate = controller.accel_poll_rate; + if (gyro_poll_rate != 0.0f) { + auto now = std::chrono::steady_clock::now(); + float deltaTime = std::chrono::duration_cast( + now - controller.GetLastUpdate()) + .count() / + 1000000.0f; + controller.SetLastUpdate(now); + Libraries::Pad::OrbisFQuaternion lastOrientation = controller.GetLastOrientation(); + Libraries::Pad::OrbisFQuaternion outputOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; + GameControllers::CalculateOrientation(pData->acceleration, pData->angularVelocity, + deltaTime, lastOrientation, outputOrientation); + pData[i].orientation = outputOrientation; + controller.SetLastOrientation(outputOrientation); } pData[i].touchData.touchNum = (states[i].touchpad[0].state ? 1 : 0) + (states[i].touchpad[1].state ? 1 : 0); if (handle == 1) { - if (controller->GetTouchCount() >= 127) { - controller->SetTouchCount(0); + if (controller.GetTouchCount() >= 127) { + controller.SetTouchCount(0); } - if (controller->GetSecondaryTouchCount() >= 127) { - controller->SetSecondaryTouchCount(0); + if (controller.GetSecondaryTouchCount() >= 127) { + controller.SetSecondaryTouchCount(0); } - if (pData->touchData.touchNum == 1 && controller->GetPreviousTouchNum() == 0) { - controller->SetTouchCount(controller->GetTouchCount() + 1); - controller->SetSecondaryTouchCount(controller->GetTouchCount()); - } else if (pData->touchData.touchNum == 2 && controller->GetPreviousTouchNum() == 1) { - controller->SetSecondaryTouchCount(controller->GetSecondaryTouchCount() + 1); - } else if (pData->touchData.touchNum == 0 && controller->GetPreviousTouchNum() > 0) { - if (controller->GetTouchCount() < controller->GetSecondaryTouchCount()) { - controller->SetTouchCount(controller->GetSecondaryTouchCount()); + if (pData->touchData.touchNum == 1 && controller.GetPreviousTouchNum() == 0) { + controller.SetTouchCount(controller.GetTouchCount() + 1); + controller.SetSecondaryTouchCount(controller.GetTouchCount()); + } else if (pData->touchData.touchNum == 2 && controller.GetPreviousTouchNum() == 1) { + controller.SetSecondaryTouchCount(controller.GetSecondaryTouchCount() + 1); + } else if (pData->touchData.touchNum == 0 && controller.GetPreviousTouchNum() > 0) { + if (controller.GetTouchCount() < controller.GetSecondaryTouchCount()) { + controller.SetTouchCount(controller.GetSecondaryTouchCount()); } else { - if (controller->WasSecondaryTouchReset()) { - controller->SetTouchCount(controller->GetSecondaryTouchCount()); - controller->UnsetSecondaryTouchResetBool(); + if (controller.WasSecondaryTouchReset()) { + controller.SetTouchCount(controller.GetSecondaryTouchCount()); + controller.UnsetSecondaryTouchResetBool(); } } } - controller->SetPreviousTouchNum(pData->touchData.touchNum); + controller.SetPreviousTouchNum(pData->touchData.touchNum); if (pData->touchData.touchNum == 1) { - states[i].touchpad[0].ID = controller->GetTouchCount(); + states[i].touchpad[0].ID = controller.GetTouchCount(); states[i].touchpad[1].ID = 0; } else if (pData->touchData.touchNum == 2) { - states[i].touchpad[0].ID = controller->GetTouchCount(); - states[i].touchpad[1].ID = controller->GetSecondaryTouchCount(); + states[i].touchpad[0].ID = controller.GetTouchCount(); + states[i].touchpad[1].ID = controller.GetSecondaryTouchCount(); } } else { states[i].touchpad[0].ID = 1; @@ -397,16 +440,18 @@ int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num) { LOG_TRACE(Lib_Pad, "called"); - if (handle < 1) { - return ORBIS_PAD_ERROR_INVALID_HANDLE; - } int connected_count = 0; bool connected = false; std::vector states(64); - auto* controller = Common::Singleton::Instance(); - const auto* engine = controller->GetEngine(); - int ret_num = controller->ReadStates(states.data(), num, &connected, &connected_count); - return ProcessStates(handle, pData, states.data(), ret_num, connected, connected_count); + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { + return ORBIS_PAD_ERROR_INVALID_HANDLE; + } + auto& controllers = *Common::Singleton::Instance(); + auto& controller = *controllers[*controller_id]; + int ret_num = controller.ReadStates(states.data(), num, &connected, &connected_count); + return ProcessStates(handle, pData, controller, states.data(), ret_num, connected, + connected_count); } int PS4_SYSV_ABI scePadReadBlasterForTracker() { @@ -430,17 +475,18 @@ int PS4_SYSV_ABI scePadReadHistory() { } int PS4_SYSV_ABI scePadReadState(s32 handle, OrbisPadData* pData) { - LOG_TRACE(Lib_Pad, "called"); - if (handle < 1) { + LOG_TRACE(Lib_Pad, "handle: {}", handle); + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { return ORBIS_PAD_ERROR_INVALID_HANDLE; } - auto* controller = Common::Singleton::Instance(); - const auto* engine = controller->GetEngine(); + auto& controllers = *Common::Singleton::Instance(); + auto& controller = *controllers[*controller_id]; int connected_count = 0; bool connected = false; Input::State state; - controller->ReadState(&state, &connected, &connected_count); - ProcessStates(handle, pData, &state, 1, connected, connected_count); + controller.ReadState(&state, &connected, &connected_count); + ProcessStates(handle, pData, controller, &state, 1, connected, connected_count); return ORBIS_OK; } @@ -450,13 +496,30 @@ int PS4_SYSV_ABI scePadReadStateExt() { } int PS4_SYSV_ABI scePadResetLightBar(s32 handle) { - LOG_INFO(Lib_Pad, "(DUMMY) called"); - if (handle != 1) { + LOG_DEBUG(Lib_Pad, "called, handle: {}", handle); + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { return ORBIS_PAD_ERROR_INVALID_HANDLE; } - auto* controller = Common::Singleton::Instance(); - int* rgb = Config::GetControllerCustomColor(); - controller->SetLightBarRGB(rgb[0], rgb[1], rgb[2]); + auto& controllers = *Common::Singleton::Instance(); + s32 colour_index = UserManagement.GetUserByPlayerIndex(handle)->user_color - 1; + Input::Colour colour{255, 0, 0}; + if (colour_index >= 0 && colour_index <= 3) { + static constexpr Input::Colour colours[4]{ + {0, 0, 255}, // blue + {255, 0, 0}, // red + {0, 255, 0}, // green + {255, 0, 255}, // pink + }; + colour = colours[colour_index]; + } else { + LOG_ERROR(Lib_Pad, "Invalid user colour value {} for controller {}, falling back to blue", + colour_index, handle); + } + if (auto oc = GameControllers::GetControllerCustomColor(*controller_id)) { + colour = *oc; + } + controllers[*controller_id]->SetLightBarRGB(colour.r, colour.g, colour.b); return ORBIS_OK; } @@ -473,14 +536,15 @@ int PS4_SYSV_ABI scePadResetLightBarAllByPortType() { int PS4_SYSV_ABI scePadResetOrientation(s32 handle) { LOG_INFO(Lib_Pad, "scePadResetOrientation called handle = {}", handle); - if (handle != 1) { + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { return ORBIS_PAD_ERROR_INVALID_HANDLE; } - auto* controller = Common::Singleton::Instance(); + auto& controllers = *Common::Singleton::Instance(); Libraries::Pad::OrbisFQuaternion defaultOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; - controller->SetLastOrientation(defaultOrientation); - controller->SetLastUpdate(std::chrono::steady_clock::now()); + controllers[*controller_id]->SetLastOrientation(defaultOrientation); + controllers[*controller_id]->SetLastUpdate(std::chrono::steady_clock::now()); return ORBIS_OK; } @@ -526,7 +590,11 @@ int PS4_SYSV_ABI scePadSetForceIntercepted() { } int PS4_SYSV_ABI scePadSetLightBar(s32 handle, const OrbisPadLightBarParam* pParam) { - if (Config::GetOverrideControllerColor()) { + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { + return ORBIS_PAD_ERROR_INVALID_HANDLE; + } + if (GameControllers::GetControllerCustomColor(*controller_id)) { return ORBIS_OK; } if (pParam != nullptr) { @@ -538,8 +606,8 @@ int PS4_SYSV_ABI scePadSetLightBar(s32 handle, const OrbisPadLightBarParam* pPar return ORBIS_PAD_ERROR_INVALID_LIGHTBAR_SETTING; } - auto* controller = Common::Singleton::Instance(); - controller->SetLightBarRGB(pParam->r, pParam->g, pParam->b); + auto& controllers = *Common::Singleton::Instance(); + controllers[*controller_id]->SetLightBarRGB(pParam->r, pParam->g, pParam->b); return ORBIS_OK; } return ORBIS_PAD_ERROR_INVALID_ARG; @@ -555,8 +623,14 @@ int PS4_SYSV_ABI scePadSetLightBarBlinking() { return ORBIS_OK; } -int PS4_SYSV_ABI scePadSetLightBarForTracker() { - LOG_ERROR(Lib_Pad, "(STUBBED) called"); +int PS4_SYSV_ABI scePadSetLightBarForTracker(s32 handle, const OrbisPadLightBarParam* pParam) { + LOG_INFO(Lib_Pad, "called, r: {} g: {} b: {}", pParam->r, pParam->g, pParam->b); + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { + return ORBIS_PAD_ERROR_INVALID_HANDLE; + } + auto& controllers = *Common::Singleton::Instance(); + controllers[*controller_id]->SetLightBarRGB(pParam->r, pParam->g, pParam->b); return ORBIS_OK; } @@ -603,11 +677,15 @@ int PS4_SYSV_ABI scePadSetUserColor() { } int PS4_SYSV_ABI scePadSetVibration(s32 handle, const OrbisPadVibrationParam* pParam) { + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { + return ORBIS_PAD_ERROR_INVALID_HANDLE; + } if (pParam != nullptr) { LOG_DEBUG(Lib_Pad, "scePadSetVibration called handle = {} data = {} , {}", handle, pParam->smallMotor, pParam->largeMotor); - auto* controller = Common::Singleton::Instance(); - controller->SetVibration(pParam->smallMotor, pParam->largeMotor); + auto& controllers = *Common::Singleton::Instance(); + controllers[*controller_id]->SetVibration(pParam->smallMotor, pParam->largeMotor); return ORBIS_OK; } return ORBIS_PAD_ERROR_INVALID_ARG; diff --git a/src/core/libraries/pad/pad.h b/src/core/libraries/pad/pad.h index 02ceaf3d9..2f4cbcc6a 100644 --- a/src/core/libraries/pad/pad.h +++ b/src/core/libraries/pad/pad.h @@ -280,7 +280,7 @@ int PS4_SYSV_ABI scePadGetFeatureReport(); int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, s32 index); int PS4_SYSV_ABI scePadGetIdleCount(); -int PS4_SYSV_ABI scePadGetInfo(); +int PS4_SYSV_ABI scePadGetInfo(u32* data); int PS4_SYSV_ABI scePadGetInfoByPortType(); int PS4_SYSV_ABI scePadGetLicenseControllerInformation(); int PS4_SYSV_ABI scePadGetMotionSensorPosition(); @@ -324,7 +324,7 @@ int PS4_SYSV_ABI scePadSetForceIntercepted(); int PS4_SYSV_ABI scePadSetLightBar(s32 handle, const OrbisPadLightBarParam* pParam); int PS4_SYSV_ABI scePadSetLightBarBaseBrightness(); int PS4_SYSV_ABI scePadSetLightBarBlinking(); -int PS4_SYSV_ABI scePadSetLightBarForTracker(); +int PS4_SYSV_ABI scePadSetLightBarForTracker(s32 handle, const OrbisPadLightBarParam* pParam); int PS4_SYSV_ABI scePadSetLoginUserNumber(); int PS4_SYSV_ABI scePadSetMotionSensorState(s32 handle, bool bEnable); int PS4_SYSV_ABI scePadSetProcessFocus(); diff --git a/src/core/libraries/system/userservice.cpp b/src/core/libraries/system/userservice.cpp index b82549c27..029868eb4 100644 --- a/src/core/libraries/system/userservice.cpp +++ b/src/core/libraries/system/userservice.cpp @@ -1,13 +1,19 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" +#include + #include "common/logging/log.h" +#include #include +#include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/libraries/libs.h" #include "core/libraries/system/userservice.h" #include "core/libraries/system/userservice_error.h" +#include "core/tls.h" +#include "input/controller.h" namespace Libraries::UserService { @@ -114,14 +120,15 @@ void AddUserServiceEvent(const OrbisUserServiceEvent e) { } s32 PS4_SYSV_ABI sceUserServiceGetEvent(OrbisUserServiceEvent* event) { - LOG_TRACE(Lib_UserService, "(DUMMY) called"); - // fake a loggin event - static bool logged_in = false; + LOG_TRACE(Lib_UserService, "called"); - if (!logged_in) { - logged_in = true; - event->event = OrbisUserServiceEventType::Login; - event->userId = 1; + if (!user_service_event_queue.empty()) { + OrbisUserServiceEvent& temp = user_service_event_queue.front(); + event->event = temp.event; + event->userId = temp.userId; + user_service_event_queue.pop(); + LOG_INFO(Lib_UserService, "Event processed by the game: {} {}", (u8)temp.event, + temp.userId); return ORBIS_OK; } @@ -504,8 +511,7 @@ s32 PS4_SYSV_ABI sceUserServiceGetInitialUser(int* user_id) { LOG_ERROR(Lib_UserService, "user_id is null"); return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT; } - // select first user (TODO add more) - *user_id = 1; + *user_id = UserManagement.GetDefaultUser().user_id; return ORBIS_OK; } @@ -575,20 +581,29 @@ int PS4_SYSV_ABI sceUserServiceGetLoginFlag() { } s32 PS4_SYSV_ABI sceUserServiceGetLoginUserIdList(OrbisUserServiceLoginUserIdList* userIdList) { - LOG_DEBUG(Lib_UserService, "called"); if (userIdList == nullptr) { - LOG_ERROR(Lib_UserService, "user_id is null"); + LOG_ERROR(Lib_UserService, "userIdList is null"); return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT; } - // TODO only first user, do the others as well - userIdList->user_id[0] = 1; - userIdList->user_id[1] = ORBIS_USER_SERVICE_USER_ID_INVALID; - userIdList->user_id[2] = ORBIS_USER_SERVICE_USER_ID_INVALID; - userIdList->user_id[3] = ORBIS_USER_SERVICE_USER_ID_INVALID; + // Initialize all slots to invalid (-1) + for (int i = 0; i < ORBIS_USER_SERVICE_MAX_LOGIN_USERS; i++) { + userIdList->user_id[i] = ORBIS_USER_SERVICE_USER_ID_INVALID; + } + + auto& user_manager = UserManagement; + + auto logged_in_users = user_manager.GetLoggedInUsers(); + + for (int i = 0; i < ORBIS_USER_SERVICE_MAX_LOGIN_USERS; i++) { + s32 id = + logged_in_users[i] ? logged_in_users[i]->user_id : ORBIS_USER_SERVICE_USER_ID_INVALID; + userIdList->user_id[i] = id; + LOG_DEBUG(Lib_UserService, "Slot {}: User ID {} (port {})", i, id, + logged_in_users[i] ? logged_in_users[i]->player_index : -1); + } return ORBIS_OK; } - int PS4_SYSV_ABI sceUserServiceGetMicLevel() { LOG_ERROR(Lib_UserService, "(STUBBED) called"); return ORBIS_OK; @@ -1056,7 +1071,7 @@ s32 PS4_SYSV_ABI sceUserServiceGetUserColor(int user_id, OrbisUserServiceUserCol LOG_ERROR(Lib_UserService, "color is null"); return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT; } - *color = OrbisUserServiceUserColor::Blue; + *color = (OrbisUserServiceUserColor)UserManagement.GetUserByID(user_id)->user_color; return ORBIS_OK; } @@ -1076,12 +1091,18 @@ int PS4_SYSV_ABI sceUserServiceGetUserGroupNum() { } s32 PS4_SYSV_ABI sceUserServiceGetUserName(int user_id, char* user_name, std::size_t size) { - LOG_DEBUG(Lib_UserService, "called user_id = {} ,size = {} ", user_id, size); + LOG_DEBUG(Lib_UserService, "called user_id = {}, size = {} ", user_id, size); if (user_name == nullptr) { LOG_ERROR(Lib_UserService, "user_name is null"); return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT; } - std::string name = Config::getUserName(); + std::string name = "shadPS4"; + auto const* u = UserManagement.GetUserByID(user_id); + if (u != nullptr) { + name = u->user_name; + } else { + LOG_ERROR(Lib_UserService, "No user found"); + } if (size < name.length()) { LOG_ERROR(Lib_UserService, "buffer is too short"); return ORBIS_USER_SERVICE_ERROR_BUFFER_TOO_SHORT; diff --git a/src/emulator.cpp b/src/emulator.cpp index 5206a309d..447c72391 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -289,7 +289,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, // Initialize components memory = Core::Memory::Instance(); - controller = Common::Singleton::Instance(); + controllers = Common::Singleton::Instance(); linker = Common::Singleton::Instance(); // Load renderdoc module @@ -331,7 +331,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, } } window = std::make_unique(EmulatorSettings.GetWindowWidth(), - EmulatorSettings.GetWindowHeight(), controller, + EmulatorSettings.GetWindowHeight(), controllers, window_title); g_window = window.get(); diff --git a/src/emulator.h b/src/emulator.h index f4dd32c20..d350ce16c 100644 --- a/src/emulator.h +++ b/src/emulator.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -43,7 +43,7 @@ private: void LoadSystemModules(const std::string& game_serial); Core::MemoryManager* memory; - Input::GameController* controller; + Input::GameControllers* controllers; Core::Linker* linker; std::unique_ptr window; std::chrono::steady_clock::time_point start_time; diff --git a/src/imgui/renderer/imgui_impl_sdl3.cpp b/src/imgui/renderer/imgui_impl_sdl3.cpp index 388c23b03..679aeb8c0 100644 --- a/src/imgui/renderer/imgui_impl_sdl3.cpp +++ b/src/imgui/renderer/imgui_impl_sdl3.cpp @@ -737,9 +737,8 @@ static void UpdateGamepads() { ImGuiIO& io = ImGui::GetIO(); SdlData* bd = GetBackendData(); - auto controller = Common::Singleton::Instance(); - auto engine = controller->GetEngine(); - SDL_Gamepad* SDLGamepad = engine->m_gamepad; + auto& controllers = *Common::Singleton::Instance(); + SDL_Gamepad* SDLGamepad = controllers[0]->m_sdl_gamepad; // Update list of gamepads to use if (bd->want_update_gamepads_list && bd->gamepad_mode != ImGui_ImplSDL3_GamepadMode_Manual) { if (SDLGamepad) { diff --git a/src/input/controller.cpp b/src/input/controller.cpp index 3606ad5d2..c1ba584e3 100644 --- a/src/input/controller.cpp +++ b/src/input/controller.cpp @@ -1,15 +1,20 @@ // SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include #include -#include "common/config.h" +#include +#include #include "common/logging/log.h" +#include "controller.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/time.h" #include "core/libraries/pad/pad.h" +#include "core/libraries/system/userservice.h" +#include "core/user_settings.h" #include "input/controller.h" -static std::string SelectedGamepad = ""; - namespace Input { using Libraries::Pad::OrbisPadButtonDataOffset; @@ -22,7 +27,15 @@ void State::OnButton(OrbisPadButtonDataOffset button, bool isPressed) { } } -void State::OnAxis(Axis axis, int value) { +void State::OnAxis(Axis axis, int value, bool smooth) { + auto const i = std::to_underlying(axis); + // forcibly finish the previous smoothing task by jumping to the end + axes[i] = axis_smoothing_end_values[i]; + + axis_smoothing_start_times[i] = time; + axis_smoothing_start_values[i] = axes[i]; + axis_smoothing_end_values[i] = value; + axis_smoothing_flags[i] = smooth; const auto toggle = [&](const auto button) { if (value > 0) { buttonsState |= button; @@ -40,7 +53,6 @@ void State::OnAxis(Axis axis, int value) { default: break; } - axes[static_cast(axis)] = value; } void State::OnTouchpad(int touchIndex, bool isDown, float x, float y) { @@ -61,6 +73,22 @@ void State::OnAccel(const float accel[3]) { acceleration.z = accel[2]; } +void State::UpdateAxisSmoothing() { + for (int i = 0; i < std::to_underlying(Axis::AxisMax); i++) { + // if it's not to be smoothed or close enough, just jump to the end + if (!axis_smoothing_flags[i] || std::abs(axes[i] - axis_smoothing_end_values[i]) < 16) { + if (axes[i] != axis_smoothing_end_values[i]) { + axes[i] = axis_smoothing_end_values[i]; + } + continue; + } + auto now = Libraries::Kernel::sceKernelGetProcessTime(); + f32 t = + std::clamp((now - axis_smoothing_start_times[i]) / f32{axis_smoothing_time}, 0.f, 1.f); + axes[i] = s32(axis_smoothing_start_values[i] * (1 - t) + axis_smoothing_end_values[i] * t); + } +} + GameController::GameController() : m_states_queue(64) {} void GameController::ReadState(State* state, bool* isConnected, int* connectedCount) { @@ -88,31 +116,82 @@ int GameController::ReadStates(State* states, int states_num, bool* isConnected, return ret_num; } -void GameController::Button(int id, OrbisPadButtonDataOffset button, bool is_pressed) { +void GameController::Button(OrbisPadButtonDataOffset button, bool is_pressed) { m_state.OnButton(button, is_pressed); PushState(); } -void GameController::Axis(int id, Input::Axis axis, int value) { - m_state.OnAxis(axis, value); +void GameController::Axis(Input::Axis axis, int value, bool smooth) { + m_state.OnAxis(axis, value, smooth); PushState(); } -void GameController::Gyro(int id, const float gyro[3]) { - m_state.OnGyro(gyro); +void GameController::Gyro(int id) { + m_state.OnGyro(gyro_buf); PushState(); } -void GameController::Acceleration(int id, const float acceleration[3]) { - m_state.OnAccel(acceleration); +void GameController::Acceleration(int id) { + m_state.OnAccel(accel_buf); PushState(); } -void GameController::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, - Libraries::Pad::OrbisFVector3& angularVelocity, - float deltaTime, - Libraries::Pad::OrbisFQuaternion& lastOrientation, - Libraries::Pad::OrbisFQuaternion& orientation) { +void GameController::UpdateGyro(const float gyro[3]) { + std::scoped_lock l(m_states_queue_mutex); + std::memcpy(gyro_buf, gyro, sizeof(gyro_buf)); +} + +void GameController::UpdateAcceleration(const float acceleration[3]) { + std::scoped_lock l(m_states_queue_mutex); + std::memcpy(accel_buf, acceleration, sizeof(accel_buf)); +} + +void GameController::UpdateAxisSmoothing() { + m_state.UpdateAxisSmoothing(); +} + +void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) { + colour = {r, g, b}; + if (m_sdl_gamepad != nullptr) { + SDL_SetGamepadLED(m_sdl_gamepad, r, g, b); + } +} + +void GameController::PollLightColour() { + if (m_sdl_gamepad != nullptr) { + SDL_SetGamepadLED(m_sdl_gamepad, colour.r, colour.g, colour.b); + } +} + +bool GameController::SetVibration(u8 smallMotor, u8 largeMotor) { + if (m_sdl_gamepad != nullptr) { + return SDL_RumbleGamepad(m_sdl_gamepad, (smallMotor / 255.0f) * 0xFFFF, + (largeMotor / 255.0f) * 0xFFFF, -1); + } + return true; +} + +void GameController::SetTouchpadState(int touchIndex, bool touchDown, float x, float y) { + if (touchIndex < 2) { + m_state.OnTouchpad(touchIndex, touchDown, x, y); + PushState(); + } +} + +std::array, 4> GameControllers::controller_override_colors{ + std::nullopt, std::nullopt, std::nullopt, std::nullopt}; + +void GameControllers::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, + Libraries::Pad::OrbisFVector3& angularVelocity, + float deltaTime, + Libraries::Pad::OrbisFQuaternion& lastOrientation, + Libraries::Pad::OrbisFQuaternion& orientation) { + // avoid wildly off values coming from elapsed time between two samples + // being too high, such as on the first time the controller is polled + if (deltaTime > 1.0f) { + orientation = lastOrientation; + return; + } Libraries::Pad::OrbisFQuaternion q = lastOrientation; Libraries::Pad::OrbisFQuaternion ω = {angularVelocity.x, angularVelocity.y, angularVelocity.z, 0.0f}; @@ -143,27 +222,100 @@ void GameController::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceler orientation.y, orientation.z, orientation.w); } -void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) { - if (!m_engine) { - return; - } - m_engine->SetLightBarRGB(r, g, b); -} +bool is_first_check = true; -void GameController::SetVibration(u8 smallMotor, u8 largeMotor) { - if (!m_engine) { - return; - } - m_engine->SetVibration(smallMotor, largeMotor); -} +void GameControllers::TryOpenSDLControllers() { + using namespace Libraries::UserService; + int controller_count; + s32 move_count = 0; + SDL_JoystickID* new_joysticks = SDL_GetGamepads(&controller_count); + LOG_INFO(Input, "{} controllers are currently connected", controller_count); -void GameController::SetTouchpadState(int touchIndex, bool touchDown, float x, float y) { - if (touchIndex < 2) { - m_state.OnTouchpad(touchIndex, touchDown, x, y); - PushState(); - } -} + std::unordered_set assigned_ids; + std::array slot_taken{false, false, false, false}; + for (int i = 0; i < 4; i++) { + SDL_Gamepad* pad = controllers[i]->m_sdl_gamepad; + if (pad) { + SDL_JoystickID id = SDL_GetGamepadID(pad); + bool still_connected = false; + ControllerType type = ControllerType::Standard; + for (int j = 0; j < controller_count; j++) { + if (new_joysticks[j] == id) { + still_connected = true; + assigned_ids.insert(id); + slot_taken[i] = true; + break; + } + } + if (!still_connected) { + auto u = UserManagement.GetUserByID(controllers[i]->user_id); + UserManagement.LogoutUser(u); + SDL_CloseGamepad(pad); + controllers[i]->m_sdl_gamepad = nullptr; + controllers[i]->user_id = -1; + slot_taken[i] = false; + } + } + } + + for (int j = 0; j < controller_count; j++) { + SDL_JoystickID id = new_joysticks[j]; + if (assigned_ids.contains(id)) + continue; + + SDL_Gamepad* pad = SDL_OpenGamepad(id); + if (!pad) { + continue; + } + + for (int i = 0; i < 4; i++) { + if (!slot_taken[i]) { + auto u = UserManagement.GetUserByPlayerIndex(i + 1); + if (!u) { + LOG_INFO(Input, "User {} not found", i + 1); + continue; // for now, if you don't specify who Player N is in the config, + // Player N won't be registered at all + } + auto* c = controllers[i]; + c->m_sdl_gamepad = pad; + LOG_INFO(Input, "Gamepad registered for slot {}! Handle: {}", i, + SDL_GetGamepadID(pad)); + c->user_id = u->user_id; + slot_taken[i] = true; + UserManagement.LoginUser(u, i + 1); + if (EmulatorSettings.IsMotionControlsEnabled()) { + if (SDL_SetGamepadSensorEnabled(c->m_sdl_gamepad, SDL_SENSOR_GYRO, true)) { + c->gyro_poll_rate = + SDL_GetGamepadSensorDataRate(c->m_sdl_gamepad, SDL_SENSOR_GYRO); + LOG_INFO(Input, "Gyro initialized, poll rate: {}", c->gyro_poll_rate); + } else { + LOG_ERROR(Input, "Failed to initialize gyro controls for gamepad {}", + c->user_id); + } + if (SDL_SetGamepadSensorEnabled(c->m_sdl_gamepad, SDL_SENSOR_ACCEL, true)) { + c->accel_poll_rate = + SDL_GetGamepadSensorDataRate(c->m_sdl_gamepad, SDL_SENSOR_ACCEL); + LOG_INFO(Input, "Accel initialized, poll rate: {}", c->accel_poll_rate); + } else { + LOG_ERROR(Input, "Failed to initialize accel controls for gamepad {}", + c->user_id); + } + } + break; + } + } + } + if (is_first_check) [[unlikely]] { + is_first_check = false; + if (controller_count - move_count == 0) { + auto u = UserManagement.GetUserByPlayerIndex(1); + controllers[0]->user_id = u->user_id; + UserManagement.LoginUser(u, 1); + } + } + SDL_free(new_joysticks); +} u8 GameController::GetTouchCount() { return m_touch_count; } @@ -215,73 +367,37 @@ void GameController::SetLastUpdate(std::chrono::steady_clock::time_point lastUpd m_last_update = lastUpdate; } -void GameController::SetEngine(std::unique_ptr engine) { - m_engine = std::move(engine); - if (m_engine) { - m_engine->Init(); - } -} - -Engine* GameController::GetEngine() { - return m_engine.get(); -} - void GameController::PushState() { std::lock_guard lg(m_states_queue_mutex); m_state.time = Libraries::Kernel::sceKernelGetProcessTime(); m_states_queue.Push(m_state); } -u32 GameController::Poll() { - if (m_connected) { - PushState(); - } - return 33; -} - -} // namespace Input - -namespace GamepadSelect { - -int GetDefaultGamepad(SDL_JoystickID* gamepadIDs, int gamepadCount) { - char GUIDbuf[33]; - if (Config::getDefaultControllerID() != "") { - for (int i = 0; i < gamepadCount; i++) { - SDL_GUIDToString(SDL_GetGamepadGUIDForID(gamepadIDs[i]), GUIDbuf, 33); - std::string currentGUID = std::string(GUIDbuf); - if (currentGUID == Config::getDefaultControllerID()) { - return i; - } - } - } - return -1; -} - -int GetIndexfromGUID(SDL_JoystickID* gamepadIDs, int gamepadCount, std::string GUID) { - char GUIDbuf[33]; - for (int i = 0; i < gamepadCount; i++) { - SDL_GUIDToString(SDL_GetGamepadGUIDForID(gamepadIDs[i]), GUIDbuf, 33); - std::string currentGUID = std::string(GUIDbuf); - if (currentGUID == GUID) { +u8 GameControllers::GetGamepadIndexFromJoystickId(SDL_JoystickID id) { + auto g = SDL_GetGamepadFromID(id); + ASSERT(g != nullptr); + for (int i = 0; i < 5; i++) { + if (controllers[i]->m_sdl_gamepad == g) { return i; } } + // LOG_TRACE(Input, "Gamepad index: {}", index); return -1; } -std::string GetGUIDString(SDL_JoystickID* gamepadIDs, int index) { - char GUIDbuf[33]; - SDL_GUIDToString(SDL_GetGamepadGUIDForID(gamepadIDs[index]), GUIDbuf, 33); - std::string GUID = std::string(GUIDbuf); - return GUID; +std::optional GameControllers::GetControllerIndexFromUserID(s32 user_id) { + auto const u = UserManagement.GetUserByID(user_id); + if (!u) { + return std::nullopt; + } + return u->player_index - 1; } -std::string GetSelectedGamepad() { - return SelectedGamepad; +std::optional GameControllers::GetControllerIndexFromControllerID(s32 controller_id) { + if (controller_id < 1 || controller_id > 5) { + return std::nullopt; + } + return controller_id - 1; } -void SetSelectedGamepad(std::string GUID) { - SelectedGamepad = GUID; -} - -} // namespace GamepadSelect +} // namespace Input diff --git a/src/input/controller.h b/src/input/controller.h index 6c13fdf99..1c711c488 100644 --- a/src/input/controller.h +++ b/src/input/controller.h @@ -3,18 +3,25 @@ #pragma once -#include -#include #include +#include #include #include - +#include "SDL3/SDL_joystick.h" +#include "common/assert.h" #include "common/types.h" #include "core/libraries/pad/pad.h" +#include "core/libraries/system/userservice.h" + +struct SDL_Gamepad; namespace Input { +enum class ControllerType { + Standard, +}; + enum class Axis { LeftX = 0, LeftY = 1, @@ -33,37 +40,41 @@ struct TouchpadEntry { u16 y{}; }; -class State { +struct Colour { + u8 r, g, b; +}; + +struct State { +private: + template + using AxisArray = std::array; + static constexpr AxisArray axis_defaults{128, 128, 128, 128, 0, 0}; + static constexpr u64 axis_smoothing_time{33000}; + AxisArray axis_smoothing_flags{true}; + AxisArray axis_smoothing_start_times{0}; + AxisArray axis_smoothing_start_values{axis_defaults}; + AxisArray axis_smoothing_end_values{axis_defaults}; + public: void OnButton(Libraries::Pad::OrbisPadButtonDataOffset, bool); - void OnAxis(Axis, int); + void OnAxis(Axis, int, bool smooth = true); void OnTouchpad(int touchIndex, bool isDown, float x, float y); void OnGyro(const float[3]); void OnAccel(const float[3]); + void UpdateAxisSmoothing(); Libraries::Pad::OrbisPadButtonDataOffset buttonsState{}; u64 time = 0; - int axes[static_cast(Axis::AxisMax)] = {128, 128, 128, 128, 0, 0}; + AxisArray axes{axis_defaults}; TouchpadEntry touchpad[2] = {{false, 0, 0}, {false, 0, 0}}; - Libraries::Pad::OrbisFVector3 acceleration = {0.0f, 0.0f, 0.0f}; + Libraries::Pad::OrbisFVector3 acceleration = {0.0f, -9.81f, 0.0f}; Libraries::Pad::OrbisFVector3 angularVelocity = {0.0f, 0.0f, 0.0f}; Libraries::Pad::OrbisFQuaternion orientation = {0.0f, 0.0f, 0.0f, 1.0f}; }; -class Engine { -public: - virtual ~Engine() = default; - virtual void Init() = 0; - virtual void SetLightBarRGB(u8 r, u8 g, u8 b) = 0; - virtual void SetVibration(u8 smallMotor, u8 largeMotor) = 0; - virtual State ReadState() = 0; - virtual float GetAccelPollRate() const = 0; - virtual float GetGyroPollRate() const = 0; - SDL_Gamepad* m_gamepad; -}; - inline int GetAxis(int min, int max, int value) { - return std::clamp((255 * (value - min)) / (max - min), 0, 255); + int v = (255 * (value - min)) / (max - min); + return (v < 0 ? 0 : (v > 255 ? 255 : v)); } template @@ -98,6 +109,8 @@ private: }; class GameController { + friend class GameControllers; + public: GameController(); virtual ~GameController() = default; @@ -105,16 +118,17 @@ public: void ReadState(State* state, bool* isConnected, int* connectedCount); int ReadStates(State* states, int states_num, bool* isConnected, int* connectedCount); - void Button(int id, Libraries::Pad::OrbisPadButtonDataOffset button, bool isPressed); - void Axis(int id, Input::Axis axis, int value); - void Gyro(int id, const float gyro[3]); - void Acceleration(int id, const float acceleration[3]); + void Button(Libraries::Pad::OrbisPadButtonDataOffset button, bool isPressed); + void Axis(Input::Axis axis, int value, bool smooth = true); + void Gyro(int id); + void Acceleration(int id); + void UpdateGyro(const float gyro[3]); + void UpdateAcceleration(const float acceleration[3]); + void UpdateAxisSmoothing(); void SetLightBarRGB(u8 r, u8 g, u8 b); - void SetVibration(u8 smallMotor, u8 largeMotor); + void PollLightColour(); + bool SetVibration(u8 smallMotor, u8 largeMotor); void SetTouchpadState(int touchIndex, bool touchDown, float x, float y); - void SetEngine(std::unique_ptr); - Engine* GetEngine(); - u32 Poll(); u8 GetTouchCount(); void SetTouchCount(u8 touchCount); @@ -129,11 +143,12 @@ public: Libraries::Pad::OrbisFQuaternion GetLastOrientation(); std::chrono::steady_clock::time_point GetLastUpdate(); void SetLastUpdate(std::chrono::steady_clock::time_point lastUpdate); - static void CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, - Libraries::Pad::OrbisFVector3& angularVelocity, - float deltaTime, - Libraries::Pad::OrbisFQuaternion& lastOrientation, - Libraries::Pad::OrbisFQuaternion& orientation); + + float gyro_poll_rate; + float accel_poll_rate; + float gyro_buf[3] = {0.0f, 0.0f, 0.0f}, accel_buf[3] = {0.0f, 9.81f, 0.0f}; + s32 user_id = Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID; + SDL_Gamepad* m_sdl_gamepad = nullptr; private: void PushState(); @@ -146,22 +161,46 @@ private: bool m_was_secondary_reset = false; std::chrono::steady_clock::time_point m_last_update = {}; Libraries::Pad::OrbisFQuaternion m_orientation = {0.0f, 0.0f, 0.0f, 1.0f}; + Colour colour; State m_state; std::mutex m_states_queue_mutex; RingBufferQueue m_states_queue; +}; - std::unique_ptr m_engine = nullptr; +class GameControllers { + std::array controllers; + + static std::array, 4> controller_override_colors; + +public: + GameControllers() + : controllers({new GameController(), new GameController(), new GameController(), + new GameController(), new GameController()}) {}; + virtual ~GameControllers() = default; + GameController* operator[](const size_t& i) const { + if (i > 4) { + UNREACHABLE_MSG("Index {} is out of bounds for GameControllers!", i); + } + return controllers[i]; + } + void TryOpenSDLControllers(); + u8 GetGamepadIndexFromJoystickId(SDL_JoystickID id); + static std::optional GetControllerIndexFromUserID(s32 user_id); + static std::optional GetControllerIndexFromControllerID(s32 controller_id); + + static void CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, + Libraries::Pad::OrbisFVector3& angularVelocity, + float deltaTime, + Libraries::Pad::OrbisFQuaternion& lastOrientation, + Libraries::Pad::OrbisFQuaternion& orientation); + static void SetControllerCustomColor(s32 i, u8 r, u8 g, u8 b) { + controller_override_colors[i] = {r, g, b}; + } + static std::optional GetControllerCustomColor(s32 i) { + return controller_override_colors[i]; + } }; } // namespace Input - -namespace GamepadSelect { - -int GetIndexfromGUID(SDL_JoystickID* gamepadIDs, int gamepadCount, std::string GUID); -std::string GetGUIDString(SDL_JoystickID* gamepadIDs, int index); -std::string GetSelectedGamepad(); -void SetSelectedGamepad(std::string GUID); - -} // namespace GamepadSelect diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index fbda6e394..cf258235d 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -18,10 +18,10 @@ #include "SDL3/SDL_events.h" #include "SDL3/SDL_timer.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/io_file.h" #include "common/path_util.h" +#include "common/singleton.h" #include "core/devtools/layer.h" #include "core/emulator_settings.h" #include "core/emulator_state.h" @@ -43,78 +43,185 @@ What structs are needed? InputBinding(key1, key2, key3) ControllerOutput(button, axis) - we only need a const array of these, and one of the attr-s is always 0 BindingConnection(inputBinding (member), controllerOutput (ref to the array element)) - -Things to always test before pushing like a dumbass: -Button outputs -Axis outputs -Input hierarchy -Multi key inputs -Mouse to joystick -Key toggle -Joystick halfmode - -Don't be an idiot and test only the changed part expecting everything else to not be broken */ +constexpr std::string_view GetDefaultGlobalConfig() { + return R"(# Anything put here will be loaded for all games, +# alongside the game's config or default.ini depending on your preference. +)"; +} + +constexpr std::string_view GetDefaultInputConfig() { + return R"(#Feeling lost? Check out the Help section! + +# Keyboard bindings + +triangle = kp8 +circle = kp6 +cross = kp2 +square = kp4 +# Alternatives for users without a keypad +triangle = c +circle = b +cross = n +square = v + +l1 = q +r1 = u +l2 = e +r2 = o +l3 = x +r3 = m + +options = enter +touchpad_center = space + +pad_up = up +pad_down = down +pad_left = left +pad_right = right + +axis_left_x_minus = a +axis_left_x_plus = d +axis_left_y_minus = w +axis_left_y_plus = s + +axis_right_x_minus = j +axis_right_x_plus = l +axis_right_y_minus = i +axis_right_y_plus = k + +# Controller bindings + +triangle = triangle +cross = cross +square = square +circle = circle + +l1 = l1 +l2 = l2 +l3 = l3 +r1 = r1 +r2 = r2 +r3 = r3 + +options = options +touchpad_center = back + +pad_up = pad_up +pad_down = pad_down +pad_left = pad_left +pad_right = pad_right + +axis_left_x = axis_left_x +axis_left_y = axis_left_y +axis_right_x = axis_right_x +axis_right_y = axis_right_y + +# Range of deadzones: 1 (almost none) to 127 (max) +analog_deadzone = leftjoystick, 2, 127 +analog_deadzone = rightjoystick, 2, 127 + +override_controller_color = false, 0, 0, 255 +)"; +} +std::filesystem::path GetInputConfigFile(const std::string& game_id) { + // Read configuration file of the game, and if it doesn't exist, generate it from default + // If that doesn't exist either, generate that from getDefaultConfig() and try again + // If even the folder is missing, we start with that. + + const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "input_config"; + const auto config_file = config_dir / (game_id + ".ini"); + const auto default_config_file = config_dir / "default.ini"; + + // Ensure the config directory exists + if (!std::filesystem::exists(config_dir)) { + std::filesystem::create_directories(config_dir); + } + + // Check if the default config exists + if (!std::filesystem::exists(default_config_file)) { + // If the default config is also missing, create it from getDefaultConfig() + const auto default_config = GetDefaultInputConfig(); + std::ofstream default_config_stream(default_config_file); + if (default_config_stream) { + default_config_stream << default_config; + } + } + + // if empty, we only need to execute the function up until this point + if (game_id.empty()) { + return default_config_file; + } + + // Create global config if it doesn't exist yet + if (game_id == "global" && !std::filesystem::exists(config_file)) { + if (!std::filesystem::exists(config_file)) { + const auto global_config = GetDefaultGlobalConfig(); + std::ofstream global_config_stream(config_file); + if (global_config_stream) { + global_config_stream << global_config; + } + } + } + if (game_id == "global") { + std::map default_bindings_to_add = { + {"hotkey_renderdoc_capture", "f12"}, + {"hotkey_fullscreen", "f11"}, + {"hotkey_show_fps", "f10"}, + {"hotkey_pause", "f9"}, + {"hotkey_reload_inputs", "f8"}, + {"hotkey_toggle_mouse_to_joystick", "f7"}, + {"hotkey_toggle_mouse_to_gyro", "f6"}, + {"hotkey_add_virtual_user", "f5"}, + {"hotkey_remove_virtual_user", "f4"}, + {"hotkey_toggle_mouse_to_touchpad", "delete"}, + {"hotkey_quit", "lctrl, lshift, end"}, + {"hotkey_volume_up", "kpplus"}, + {"hotkey_volume_down", "kpminus"}, + }; + std::ifstream global_in(config_file); + std::string line; + while (std::getline(global_in, line)) { + line.erase(std::remove_if(line.begin(), line.end(), + [](unsigned char c) { return std::isspace(c); }), + line.end()); + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) { + continue; + } + std::string output_string = line.substr(0, equal_pos); + default_bindings_to_add.erase(output_string); + } + global_in.close(); + std::ofstream global_out(config_file, std::ios::app); + for (auto const& b : default_bindings_to_add) { + global_out << b.first << " = " << b.second << "\n"; + } + } + + // If game-specific config doesn't exist, create it from the default config + if (!std::filesystem::exists(config_file)) { + std::filesystem::copy(default_config_file, config_file); + } + return config_file; +} + bool leftjoystick_halfmode = false, rightjoystick_halfmode = false; -std::pair leftjoystick_deadzone, rightjoystick_deadzone, lefttrigger_deadzone, - righttrigger_deadzone; +std::array, 4> leftjoystick_deadzone, rightjoystick_deadzone, + lefttrigger_deadzone, righttrigger_deadzone; std::list> pressed_keys; std::list toggled_keys; static std::vector connections; -auto output_array = std::array{ - // Important: these have to be the first, or else they will update in the wrong order - ControllerOutput(LEFTJOYSTICK_HALFMODE), - ControllerOutput(RIGHTJOYSTICK_HALFMODE), - ControllerOutput(KEY_TOGGLE), - ControllerOutput(MOUSE_GYRO_ROLL_MODE), +GameControllers ControllerOutput::controllers = + *Common::Singleton::Instance(); - // Button mappings - ControllerOutput(SDL_GAMEPAD_BUTTON_NORTH), // Triangle - ControllerOutput(SDL_GAMEPAD_BUTTON_EAST), // Circle - ControllerOutput(SDL_GAMEPAD_BUTTON_SOUTH), // Cross - ControllerOutput(SDL_GAMEPAD_BUTTON_WEST), // Square - ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), // L1 - ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_STICK), // L3 - ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), // R1 - ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_STICK), // R3 - ControllerOutput(SDL_GAMEPAD_BUTTON_START), // Options - ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT), // TouchPad - ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER), // TouchPad - ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT), // TouchPad - ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_UP), // Up - ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_DOWN), // Down - ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_LEFT), // Left - ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), // Right - - // Axis mappings - // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX, false), - // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY, false), - // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX, false), - // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY, false), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY), - - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER), - - ControllerOutput(HOTKEY_FULLSCREEN), - ControllerOutput(HOTKEY_PAUSE), - ControllerOutput(HOTKEY_SIMPLE_FPS), - ControllerOutput(HOTKEY_QUIT), - ControllerOutput(HOTKEY_RELOAD_INPUTS), - ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_JOYSTICK), - ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_GYRO), - ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD), - ControllerOutput(HOTKEY_RENDERDOC), - ControllerOutput(HOTKEY_VOLUME_UP), - ControllerOutput(HOTKEY_VOLUME_DOWN), - - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID), +std::array output_arrays = { + ControllerAllOutputs(0), ControllerAllOutputs(1), ControllerAllOutputs(2), + ControllerAllOutputs(3), ControllerAllOutputs(4), ControllerAllOutputs(5), + ControllerAllOutputs(6), ControllerAllOutputs(7), ControllerAllOutputs(8), }; void ControllerOutput::LinkJoystickAxes() { @@ -158,6 +265,8 @@ static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) { return OPBDO::TouchPad; case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: return OPBDO::L1; + case SDL_GAMEPAD_BUTTON_MISC1: // Move + return OPBDO::L1; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return OPBDO::R1; case SDL_GAMEPAD_BUTTON_LEFT_STICK: @@ -223,10 +332,19 @@ InputBinding GetBindingFromString(std::string& line) { return InputBinding(keys[0], keys[1], keys[2]); } +std::optional parseInt(const std::string& s) { + try { + return std::stoi(s); + } catch (...) { + return std::nullopt; + } +}; + void ParseInputConfig(const std::string game_id = "") { - std::string game_id_or_default = Config::GetUseUnifiedInputConfig() ? "default" : game_id; - const auto config_file = Config::GetInputConfigFile(game_id_or_default); - const auto global_config_file = Config::GetInputConfigFile("global"); + std::string game_id_or_default = + EmulatorSettings.IsUseUnifiedInputConfig() ? "default" : game_id; + const auto config_file = GetInputConfigFile(game_id_or_default); + const auto global_config_file = GetInputConfigFile("global"); // we reset these here so in case the user fucks up or doesn't include some of these, // we can fall back to default @@ -235,13 +353,14 @@ void ParseInputConfig(const std::string game_id = "") { float mouse_speed = 1; float mouse_speed_offset = 0.125; - leftjoystick_deadzone = {1, 127}; - rightjoystick_deadzone = {1, 127}; - lefttrigger_deadzone = {1, 127}; - righttrigger_deadzone = {1, 127}; + // me when I'm in a type deduction tournament and my opponent is clang + constexpr std::array, 4> default_deadzone = { + std::pair{1, 127}, {1, 127}, {1, 127}, {1, 127}}; - Config::SetOverrideControllerColor(false); - Config::SetControllerCustomColor(0, 0, 255); + leftjoystick_deadzone = default_deadzone; + rightjoystick_deadzone = default_deadzone; + lefttrigger_deadzone = default_deadzone; + righttrigger_deadzone = default_deadzone; int lineCount = 0; @@ -278,21 +397,37 @@ void ParseInputConfig(const std::string game_id = "") { std::string output_string = line.substr(0, equal_pos); std::string input_string = line.substr(equal_pos + 1); - // Remove trailing semicolon from input_string - if (!input_string.empty() && input_string[input_string.length() - 1] == ';' && - input_string != ";") { - line = line.substr(0, line.length() - 1); + s8 input_gamepad_id = -1, output_gamepad_id = -1; // -1 means it's not specified + + // input gamepad id is only for controllers, it's discarded otherwise + std::size_t input_colon_pos = input_string.find(':'); + if (input_colon_pos != std::string::npos) { + auto temp = parseInt(input_string.substr(input_colon_pos + 1)); + if (!temp) { + LOG_WARNING(Input, "Invalid gamepad ID value at line {}: \"{}\"", lineCount, line); + } else { + input_gamepad_id = *temp; + } + input_string = input_string.substr(0, input_colon_pos); + } + + // if not provided, assume it's for all gamepads, if the input is a controller and that also + // doesn't have an ID, and for the first otherwise + std::size_t output_colon_pos = output_string.find(':'); + if (output_colon_pos != std::string::npos) { + auto temp = parseInt(output_string.substr(output_colon_pos + 1)); + if (!temp) { + LOG_WARNING(Input, "Invalid gamepad ID value at line {}: \"{}\"", lineCount, line); + } else { + output_gamepad_id = *temp; + } + output_string = output_string.substr(0, output_colon_pos); } std::size_t comma_pos = input_string.find(','); - auto parseInt = [](const std::string& s) -> std::optional { - try { - return std::stoi(s); - } catch (...) { - return std::nullopt; - } - }; + // todo make override_controller_color and analog_deadzone be controller specific + // instead of global if (output_string == "mouse_to_joystick") { if (input_string == "left") { SetMouseToJoystick(1); @@ -315,7 +450,7 @@ void ParseInputConfig(const std::string game_id = "") { return; } ControllerOutput* toggle_out = - &*std::ranges::find(output_array, ControllerOutput(KEY_TOGGLE)); + &*std::ranges::find(output_arrays[0].data, ControllerOutput(KEY_TOGGLE)); BindingConnection toggle_connection = BindingConnection( InputBinding(toggle_keys.keys[0]), toggle_out, 0, toggle_keys.keys[1]); connections.insert(connections.end(), toggle_connection); @@ -356,15 +491,17 @@ void ParseInputConfig(const std::string game_id = "") { std::pair deadzone = {*inner_deadzone, *outer_deadzone}; - static std::unordered_map&> deadzone_map = { - {"leftjoystick", leftjoystick_deadzone}, - {"rightjoystick", rightjoystick_deadzone}, - {"l2", lefttrigger_deadzone}, - {"r2", righttrigger_deadzone}, - }; + static std::unordered_map, 4>&> + deadzone_map = { + {"leftjoystick", leftjoystick_deadzone}, + {"rightjoystick", rightjoystick_deadzone}, + {"l2", lefttrigger_deadzone}, + {"r2", righttrigger_deadzone}, + }; + output_gamepad_id = output_gamepad_id == -1 ? 1 : output_gamepad_id; if (auto it = deadzone_map.find(device); it != deadzone_map.end()) { - it->second = deadzone; + it->second[output_gamepad_id - 1] = deadzone; LOG_DEBUG(Input, "Parsed deadzone: {} {} {}", device, inner_deadzone_str, outer_deadzone_str); } else { @@ -390,10 +527,12 @@ void ParseInputConfig(const std::string game_id = "") { lineCount, line); return; } - Config::SetOverrideControllerColor(enable == "true"); - Config::SetControllerCustomColor(*r, *g, *b); - LOG_DEBUG(Input, "Parsed color settings: {} {} {} {}", - enable == "true" ? "override" : "no override", *r, *b, *g); + output_gamepad_id = output_gamepad_id == -1 ? 1 : output_gamepad_id; + if (enable == "true") { + GameControllers::SetControllerCustomColor(output_gamepad_id - 1, *r, *g, *b); + } + LOG_DEBUG(Input, "Parsed color settings: {} {} - {} {} {}", + enable == "true" ? "override" : "no override", output_gamepad_id, *r, *b, *g); return; } @@ -410,31 +549,46 @@ void ParseInputConfig(const std::string game_id = "") { auto axis_it = string_to_axis_map.find(output_string); if (button_it != string_to_cbutton_map.end()) { connection = BindingConnection( - binding, &*std::ranges::find(output_array, ControllerOutput(button_it->second))); - connections.insert(connections.end(), connection); + binding, + &*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data, + ControllerOutput(button_it->second))); } else if (hotkey_it != string_to_hotkey_map.end()) { connection = BindingConnection( - binding, &*std::ranges::find(output_array, ControllerOutput(hotkey_it->second))); - connections.insert(connections.end(), connection); + binding, + &*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data, + ControllerOutput(hotkey_it->second))); } else if (axis_it != string_to_axis_map.end()) { int value_to_set = binding.keys[2].type == InputType::Axis ? 0 : axis_it->second.value; connection = BindingConnection( binding, - &*std::ranges::find(output_array, ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, - axis_it->second.axis, - axis_it->second.value >= 0)), + &*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data, + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, + axis_it->second.axis, + axis_it->second.value >= 0)), value_to_set); - connections.insert(connections.end(), connection); } else { LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.", lineCount, line); return; } + // if the input binding contains a controller input, and gamepad ID + // isn't specified for either inputs or output (both are -1), then multiply the binding and + // add it to all 4 controllers + if (connection.HasGamepadInput() && input_gamepad_id == -1 && output_gamepad_id == -1) { + for (int i = 0; i < output_arrays.size(); i++) { + BindingConnection copy = connection.CopyWithChangedGamepadId(i + 1); + copy.output = &*std::ranges::find(output_arrays[i].data, *connection.output); + connections.push_back(copy); + } + } else { + connections.push_back(connection); + } LOG_DEBUG(Input, "Succesfully parsed line {}", lineCount); }; while (std::getline(global_config_stream, line)) { ProcessLine(); } + lineCount = 0; while (std::getline(config_stream, line)) { ProcessLine(); } @@ -446,6 +600,16 @@ void ParseInputConfig(const std::string game_id = "") { LOG_DEBUG(Input, "Done parsing the input config!"); } +BindingConnection BindingConnection::CopyWithChangedGamepadId(u8 gamepad) { + BindingConnection copy = *this; + for (auto& key : copy.binding.keys) { + if (key.type == InputType::Controller || key.type == InputType::Axis) { + key.gamepad_id = gamepad; + } + } + return copy; +} + u32 GetMouseWheelEvent(const SDL_Event& event) { if (event.type != SDL_EVENT_MOUSE_WHEEL && event.type != SDL_EVENT_MOUSE_WHEEL_OFF) { LOG_WARNING(Input, "Something went wrong with wheel input parsing!"); @@ -464,6 +628,7 @@ u32 GetMouseWheelEvent(const SDL_Event& event) { } InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) { + u8 gamepad = 1; switch (e.type) { case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: @@ -478,21 +643,17 @@ InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) { e.type == SDL_EVENT_MOUSE_WHEEL, 0); case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: - return InputEvent(InputType::Controller, static_cast(e.gbutton.button), e.gbutton.down, - 0); // clang made me do it + gamepad = ControllerOutput::controllers.GetGamepadIndexFromJoystickId(e.gbutton.which) + 1; + return InputEvent({InputType::Controller, (u32)e.gbutton.button, gamepad}, e.gbutton.down, + 0); case SDL_EVENT_GAMEPAD_AXIS_MOTION: - return InputEvent(InputType::Axis, static_cast(e.gaxis.axis), true, - e.gaxis.value / 256); // this too + gamepad = ControllerOutput::controllers.GetGamepadIndexFromJoystickId(e.gaxis.which) + 1; + return InputEvent({InputType::Axis, (u32)e.gaxis.axis, gamepad}, true, e.gaxis.value / 256); default: return InputEvent(); } } -GameController* ControllerOutput::controller = nullptr; -void ControllerOutput::SetControllerOutputController(GameController* c) { - ControllerOutput::controller = c; -} - void ToggleKeyInList(InputID input) { if (input.type == InputType::Axis) { LOG_ERROR(Input, "Toggling analog inputs is not supported!"); @@ -538,7 +699,7 @@ void ControllerOutput::AddUpdate(InputEvent event) { *new_param = (event.active ? event.axis_value : 0) + *new_param; } } -void ControllerOutput::FinalizeUpdate() { +void ControllerOutput::FinalizeUpdate(u8 gamepad_index) { auto PushSDLEvent = [&](u32 event_type) { if (new_button_state) { SDL_Event e; @@ -553,20 +714,24 @@ void ControllerOutput::FinalizeUpdate() { } old_button_state = new_button_state; old_param = *new_param; - bool is_game_specific = EmulatorState::GetInstance()->IsGameSpecifigConfigUsed(); + GameController* controller; + if (gamepad_index < 5) + controller = controllers[gamepad_index]; + else + UNREACHABLE(); if (button != SDL_GAMEPAD_BUTTON_INVALID) { switch (button) { case SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT: controller->SetTouchpadState(0, new_button_state, 0.25f, 0.5f); - controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(SDLGamepadToOrbisButton(button), new_button_state); break; case SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER: controller->SetTouchpadState(0, new_button_state, 0.50f, 0.5f); - controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(SDLGamepadToOrbisButton(button), new_button_state); break; case SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT: controller->SetTouchpadState(0, new_button_state, 0.75f, 0.5f); - controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(SDLGamepadToOrbisButton(button), new_button_state); break; case LEFTJOYSTICK_HALFMODE: leftjoystick_halfmode = new_button_state; @@ -598,6 +763,12 @@ void ControllerOutput::FinalizeUpdate() { case HOTKEY_RENDERDOC: PushSDLEvent(SDL_EVENT_RDOC_CAPTURE); break; + case HOTKEY_ADD_VIRTUAL_USER: + PushSDLEvent(SDL_EVENT_ADD_VIRTUAL_USER); + break; + case HOTKEY_REMOVE_VIRTUAL_USER: + PushSDLEvent(SDL_EVENT_REMOVE_VIRTUAL_USER); + break; case HOTKEY_VOLUME_UP: EmulatorSettings.SetVolumeSlider( std::clamp(EmulatorSettings.GetVolumeSlider() + 10, 0, 500)); @@ -618,7 +789,7 @@ void ControllerOutput::FinalizeUpdate() { SetMouseGyroRollMode(new_button_state); break; default: // is a normal key (hopefully) - controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(SDLGamepadToOrbisButton(button), new_button_state); break; } } else if (axis != SDL_GAMEPAD_AXIS_INVALID && positive_axis) { @@ -638,28 +809,28 @@ void ControllerOutput::FinalizeUpdate() { switch (c_axis) { case Axis::LeftX: case Axis::LeftY: - ApplyDeadzone(new_param, leftjoystick_deadzone); + ApplyDeadzone(new_param, leftjoystick_deadzone[gamepad_index]); multiplier = leftjoystick_halfmode ? 0.5 : 1.0; break; case Axis::RightX: case Axis::RightY: - ApplyDeadzone(new_param, rightjoystick_deadzone); + ApplyDeadzone(new_param, rightjoystick_deadzone[gamepad_index]); multiplier = rightjoystick_halfmode ? 0.5 : 1.0; break; case Axis::TriggerLeft: - ApplyDeadzone(new_param, lefttrigger_deadzone); - controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param)); - controller->Button(0, OrbisPadButtonDataOffset::L2, *new_param > 0x20); + ApplyDeadzone(new_param, lefttrigger_deadzone[gamepad_index]); + controller->Axis(c_axis, GetAxis(0x0, 0x7f, *new_param)); + controller->Button(OrbisPadButtonDataOffset::L2, *new_param > 0x20); return; case Axis::TriggerRight: - ApplyDeadzone(new_param, righttrigger_deadzone); - controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param)); - controller->Button(0, OrbisPadButtonDataOffset::R2, *new_param > 0x20); + ApplyDeadzone(new_param, righttrigger_deadzone[gamepad_index]); + controller->Axis(c_axis, GetAxis(0x0, 0x7f, *new_param)); + controller->Button(OrbisPadButtonDataOffset::R2, *new_param > 0x20); return; default: break; } - controller->Axis(0, c_axis, GetAxis(-0x80, 0x7f, *new_param * multiplier)); + controller->Axis(c_axis, GetAxis(-0x80, 0x7f, *new_param * multiplier)); } } @@ -674,11 +845,9 @@ bool UpdatePressedKeys(InputEvent event) { if (input.type == InputType::Axis) { // analog input, it gets added when it first sends an event, // and from there, it only changes the parameter - auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input, - [](const std::pair& e, InputID i) { - return std::tie(e.first.input.type, e.first.input.sdl_id) < - std::tie(i.type, i.sdl_id); - }); + auto it = std::lower_bound( + pressed_keys.begin(), pressed_keys.end(), input, + [](const std::pair& e, InputID i) { return e.first.input < i; }); if (it == pressed_keys.end() || it->first.input != input) { pressed_keys.insert(it, {event, false}); LOG_DEBUG(Input, "Added axis {} to the input list", event.input.sdl_id); @@ -791,25 +960,33 @@ InputEvent BindingConnection::ProcessBinding() { } void ActivateOutputsFromInputs() { - // Reset values and flags - for (auto& it : pressed_keys) { - it.second = false; - } - for (auto& it : output_array) { - it.ResetUpdate(); - } - // Check for input blockers - ApplyMouseInputBlockers(); + // todo find a better solution + for (int i = 0; i < output_arrays.size(); i++) { - // Iterate over all inputs, and update their respecive outputs accordingly - for (auto& it : connections) { - it.output->AddUpdate(it.ProcessBinding()); - } + // Reset values and flags + for (auto& it : pressed_keys) { + it.second = false; + } + for (auto& it : output_arrays[i].data) { + it.ResetUpdate(); + } - // Update all outputs - for (auto& it : output_array) { - it.FinalizeUpdate(); + // Check for input blockers + ApplyMouseInputBlockers(); + + // Iterate over all inputs, and update their respecive outputs accordingly + for (auto& it : connections) { + // only update this when it's the correct pass + if (it.output->gamepad_id == i) { + it.output->AddUpdate(it.ProcessBinding()); + } + } + + // Update all outputs + for (auto& it : output_arrays[i].data) { + it.FinalizeUpdate(i); + } } } diff --git a/src/input/input_handler.h b/src/input/input_handler.h index 844870b5d..22ae0f4e0 100644 --- a/src/input/input_handler.h +++ b/src/input/input_handler.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "SDL3/SDL_events.h" #include "SDL3/SDL_timer.h" @@ -35,9 +36,11 @@ #define SDL_EVENT_MOUSE_TO_JOYSTICK SDL_EVENT_USER + 6 #define SDL_EVENT_MOUSE_TO_GYRO SDL_EVENT_USER + 7 #define SDL_EVENT_MOUSE_TO_TOUCHPAD SDL_EVENT_USER + 8 -#define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 9 -#define SDL_EVENT_QUIT_DIALOG SDL_EVENT_USER + 10 -#define SDL_EVENT_MOUSE_WHEEL_OFF SDL_EVENT_USER + 11 +#define SDL_EVENT_QUIT_DIALOG SDL_EVENT_USER + 9 +#define SDL_EVENT_MOUSE_WHEEL_OFF SDL_EVENT_USER + 10 +#define SDL_EVENT_ADD_VIRTUAL_USER SDL_EVENT_USER + 11 +#define SDL_EVENT_REMOVE_VIRTUAL_USER SDL_EVENT_USER + 12 +#define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 13 #define LEFTJOYSTICK_HALFMODE 0x00010000 #define RIGHTJOYSTICK_HALFMODE 0x00020000 @@ -57,6 +60,8 @@ #define HOTKEY_RENDERDOC 0xf0000009 #define HOTKEY_VOLUME_UP 0xf000000a #define HOTKEY_VOLUME_DOWN 0xf000000b +#define HOTKEY_ADD_VIRTUAL_USER 0xf000000c +#define HOTKEY_REMOVE_VIRTUAL_USER 0xf000000d #define SDL_UNMAPPED UINT32_MAX - 1 @@ -77,21 +82,24 @@ class InputID { public: InputType type; u32 sdl_id; - InputID(InputType d = InputType::Count, u32 i = SDL_UNMAPPED) : type(d), sdl_id(i) {} + u8 gamepad_id; + InputID(InputType d = InputType::Count, u32 i = (u32)-1, u8 g = 1) + : type(d), sdl_id(i), gamepad_id(g) {} bool operator==(const InputID& o) const { - return type == o.type && sdl_id == o.sdl_id; + return type == o.type && sdl_id == o.sdl_id && gamepad_id == o.gamepad_id; } bool operator!=(const InputID& o) const { - return type != o.type || sdl_id != o.sdl_id; + return type != o.type || sdl_id != o.sdl_id || gamepad_id != o.gamepad_id; } - bool operator<=(const InputID& o) const { - return type <= o.type && sdl_id <= o.sdl_id; + auto operator<=>(const InputID& o) const { + return std::tie(gamepad_id, type, sdl_id, gamepad_id) <=> + std::tie(o.gamepad_id, o.type, o.sdl_id, o.gamepad_id); } bool IsValid() const { return *this != InputID(); } std::string ToString() { - return fmt::format("({}: {:x})", input_type_names[static_cast(type)], sdl_id); + return fmt::format("({}. {}: {:x})", gamepad_id, input_type_names[(u8)type], sdl_id); } }; @@ -149,6 +157,8 @@ const std::map string_to_hotkey_map = { {"hotkey_toggle_mouse_to_gyro", HOTKEY_TOGGLE_MOUSE_TO_GYRO}, {"hotkey_toggle_mouse_to_touchpad", HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD}, {"hotkey_renderdoc_capture", HOTKEY_RENDERDOC}, + {"hotkey_add_virtual_user", HOTKEY_ADD_VIRTUAL_USER}, + {"hotkey_remove_virtual_user", HOTKEY_REMOVE_VIRTUAL_USER}, {"hotkey_volume_up", HOTKEY_VOLUME_UP}, {"hotkey_volume_down", HOTKEY_VOLUME_DOWN}, }; @@ -401,7 +411,7 @@ public: inline bool IsEmpty() { return !(keys[0].IsValid() || keys[1].IsValid() || keys[2].IsValid()); } - std::string ToString() { // todo add device type + std::string ToString() { switch (KeyCount()) { case 1: return fmt::format("({})", keys[0].ToString()); @@ -420,14 +430,14 @@ public: }; class ControllerOutput { - static GameController* controller; - public: - static void SetControllerOutputController(GameController* c); + static GameControllers controllers; + static void GetGetGamepadIndexFromSDLJoystickID(const SDL_JoystickID id) {} static void LinkJoystickAxes(); u32 button; u32 axis; + u8 gamepad_id; // these are only used as s8, // but I added some padding to avoid overflow if it's activated by multiple inputs // axis_plus and axis_minus pairs share a common new_param, the other outputs have their own @@ -441,6 +451,7 @@ public: new_param = new s16(0); old_param = 0; positive_axis = p; + gamepad_id = 0; } ControllerOutput(const ControllerOutput& o) : button(o.button), axis(o.axis) { new_param = new s16(*o.new_param); @@ -466,7 +477,7 @@ public: void ResetUpdate(); void AddUpdate(InputEvent event); - void FinalizeUpdate(); + void FinalizeUpdate(u8 gamepad_index); }; class BindingConnection { public: @@ -481,6 +492,13 @@ public: output = out; toggle = t; } + BindingConnection& operator=(const BindingConnection& o) { + binding = o.binding; + output = o.output; + axis_param = o.axis_param; + toggle = o.toggle; + return *this; + } bool operator<(const BindingConnection& other) const { // a button is a higher priority than an axis, as buttons can influence axes // (e.g. joystick_halfmode) @@ -494,9 +512,81 @@ public: } return false; } + bool HasGamepadInput() { + for (auto& key : binding.keys) { + if (key.type == InputType::Controller || key.type == InputType::Axis) { + return true; + } + } + return false; + } + BindingConnection CopyWithChangedGamepadId(u8 gamepad); InputEvent ProcessBinding(); }; +class ControllerAllOutputs { +public: + static constexpr u64 output_count = 39; + std::array data = { + // Important: these have to be the first, or else they will update in the wrong order + ControllerOutput(LEFTJOYSTICK_HALFMODE), + ControllerOutput(RIGHTJOYSTICK_HALFMODE), + ControllerOutput(KEY_TOGGLE), + ControllerOutput(MOUSE_GYRO_ROLL_MODE), + + // Button mappings + ControllerOutput(SDL_GAMEPAD_BUTTON_NORTH), // Triangle + ControllerOutput(SDL_GAMEPAD_BUTTON_EAST), // Circle + ControllerOutput(SDL_GAMEPAD_BUTTON_SOUTH), // Cross + ControllerOutput(SDL_GAMEPAD_BUTTON_WEST), // Square + ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), // L1 + ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_STICK), // L3 + ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), // R1 + ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_STICK), // R3 + ControllerOutput(SDL_GAMEPAD_BUTTON_START), // Options + ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT), // TouchPad + ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER), // TouchPad + ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT), // TouchPad + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_UP), // Up + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_DOWN), // Down + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_LEFT), // Left + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), // Right + + // Axis mappings + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX, false), + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY, false), + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX, false), + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY, false), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY), + + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER), + + ControllerOutput(HOTKEY_FULLSCREEN), + ControllerOutput(HOTKEY_PAUSE), + ControllerOutput(HOTKEY_SIMPLE_FPS), + ControllerOutput(HOTKEY_QUIT), + ControllerOutput(HOTKEY_RELOAD_INPUTS), + ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_JOYSTICK), + ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_GYRO), + ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD), + ControllerOutput(HOTKEY_RENDERDOC), + ControllerOutput(HOTKEY_ADD_VIRTUAL_USER), + ControllerOutput(HOTKEY_REMOVE_VIRTUAL_USER), + ControllerOutput(HOTKEY_VOLUME_UP), + ControllerOutput(HOTKEY_VOLUME_DOWN), + + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID), + }; + ControllerAllOutputs(u8 g) { + for (int i = 0; i < output_count; i++) { + data[i].gamepad_id = g; + } + } +}; + // Updates the list of pressed keys with the given input. // Returns whether the list was updated or not. bool UpdatePressedKeys(InputEvent event); diff --git a/src/input/input_mouse.cpp b/src/input/input_mouse.cpp index 19daab3d6..f90c20484 100644 --- a/src/input/input_mouse.cpp +++ b/src/input/input_mouse.cpp @@ -77,11 +77,11 @@ void EmulateJoystick(GameController* controller, u32 interval) { float a_x = cos(angle) * output_speed, a_y = sin(angle) * output_speed; if (d_x != 0 || d_y != 0) { - controller->Axis(0, axis_x, GetAxis(-0x80, 0x7f, a_x)); - controller->Axis(0, axis_y, GetAxis(-0x80, 0x7f, a_y)); + controller->Axis(axis_x, GetAxis(-0x80, 0x7f, a_x), false); + controller->Axis(axis_y, GetAxis(-0x80, 0x7f, a_y), false); } else { - controller->Axis(0, axis_x, GetAxis(-0x80, 0x7f, 0)); - controller->Axis(0, axis_y, GetAxis(-0x80, 0x7f, 0)); + controller->Axis(axis_x, GetAxis(-0x80, 0x7f, 0), false); + controller->Axis(axis_y, GetAxis(-0x80, 0x7f, 0), false); } } @@ -89,13 +89,13 @@ constexpr float constant_down_accel[3] = {0.0f, 9.81f, 0.0f}; void EmulateGyro(GameController* controller, u32 interval) { float d_x = 0, d_y = 0; SDL_GetRelativeMouseState(&d_x, &d_y); - controller->Acceleration(1, constant_down_accel); + controller->UpdateAcceleration(constant_down_accel); float gyro_from_mouse[3] = {-d_y / 100, -d_x / 100, 0.0f}; if (mouse_gyro_roll_mode) { gyro_from_mouse[1] = 0.0f; gyro_from_mouse[2] = -d_x / 100; } - controller->Gyro(1, gyro_from_mouse); + controller->UpdateGyro(gyro_from_mouse); } void EmulateTouchpad(GameController* controller, u32 interval) { @@ -104,7 +104,7 @@ void EmulateTouchpad(GameController* controller, u32 interval) { controller->SetTouchpadState(0, (mouse_buttons & SDL_BUTTON_LMASK) != 0, std::clamp(x / g_window->GetWidth(), 0.0f, 1.0f), std::clamp(y / g_window->GetHeight(), 0.0f, 1.0f)); - controller->Button(0, Libraries::Pad::OrbisPadButtonDataOffset::TouchPad, + controller->Button(Libraries::Pad::OrbisPadButtonDataOffset::TouchPad, (mouse_buttons & SDL_BUTTON_RMASK) != 0); } diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 89b65f3dc..766a336c2 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -8,12 +8,14 @@ #include "SDL3/SDL_timer.h" #include "SDL3/SDL_video.h" #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "core/debug_state.h" #include "core/devtools/layer.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/time.h" #include "core/libraries/pad/pad.h" +#include "core/libraries/system/userservice.h" +#include "core/user_settings.h" #include "imgui/renderer/imgui_core.h" #include "input/controller.h" #include "input/input_handler.h" @@ -26,9 +28,9 @@ #endif #include -namespace Input { +namespace Frontend { -using Libraries::Pad::OrbisPadButtonDataOffset; +using namespace Libraries::Pad; static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) { using OPBDO = OrbisPadButtonDataOffset; @@ -69,220 +71,24 @@ static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) { } } -static SDL_GamepadAxis InputAxisToSDL(Axis axis) { - switch (axis) { - case Axis::LeftX: - return SDL_GAMEPAD_AXIS_LEFTX; - case Axis::LeftY: - return SDL_GAMEPAD_AXIS_LEFTY; - case Axis::RightX: - return SDL_GAMEPAD_AXIS_RIGHTX; - case Axis::RightY: - return SDL_GAMEPAD_AXIS_RIGHTY; - case Axis::TriggerLeft: - return SDL_GAMEPAD_AXIS_LEFT_TRIGGER; - case Axis::TriggerRight: - return SDL_GAMEPAD_AXIS_RIGHT_TRIGGER; - default: - UNREACHABLE(); - } -} - -SDLInputEngine::~SDLInputEngine() { - if (m_gamepad) { - SDL_CloseGamepad(m_gamepad); - } -} - -void SDLInputEngine::Init() { - if (m_gamepad) { - SDL_CloseGamepad(m_gamepad); - m_gamepad = nullptr; - } - - int gamepad_count; - SDL_JoystickID* gamepads = SDL_GetGamepads(&gamepad_count); - if (!gamepads) { - LOG_ERROR(Input, "Cannot get gamepad list: {}", SDL_GetError()); - return; - } - if (gamepad_count == 0) { - LOG_INFO(Input, "No gamepad found!"); - SDL_free(gamepads); - return; - } - - int selectedIndex = GamepadSelect::GetIndexfromGUID(gamepads, gamepad_count, - GamepadSelect::GetSelectedGamepad()); - int defaultIndex = - GamepadSelect::GetIndexfromGUID(gamepads, gamepad_count, Config::getDefaultControllerID()); - - // If user selects a gamepad in the GUI, use that, otherwise try the default - if (!m_gamepad) { - if (selectedIndex != -1) { - m_gamepad = SDL_OpenGamepad(gamepads[selectedIndex]); - LOG_INFO(Input, "Opening gamepad selected in GUI."); - } else if (defaultIndex != -1) { - m_gamepad = SDL_OpenGamepad(gamepads[defaultIndex]); - LOG_INFO(Input, "Opening default gamepad."); - } else { - m_gamepad = SDL_OpenGamepad(gamepads[0]); - LOG_INFO(Input, "Got {} gamepads. Opening the first one.", gamepad_count); - } - } - - if (!m_gamepad) { - if (!m_gamepad) { - LOG_ERROR(Input, "Failed to open gamepad: {}", SDL_GetError()); - SDL_free(gamepads); - return; - } - } - - SDL_Joystick* joystick = SDL_GetGamepadJoystick(m_gamepad); - Uint16 vendor = SDL_GetJoystickVendor(joystick); - Uint16 product = SDL_GetJoystickProduct(joystick); - - bool isDualSense = (vendor == 0x054C && product == 0x0CE6); - - LOG_INFO(Input, "Gamepad Vendor: {:04X}, Product: {:04X}", vendor, product); - if (isDualSense) { - LOG_INFO(Input, "Detected DualSense Controller"); - } - - if (Config::getIsMotionControlsEnabled()) { - if (SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_GYRO, true)) { - m_gyro_poll_rate = SDL_GetGamepadSensorDataRate(m_gamepad, SDL_SENSOR_GYRO); - LOG_INFO(Input, "Gyro initialized, poll rate: {}", m_gyro_poll_rate); - } else { - LOG_ERROR(Input, "Failed to initialize gyro controls for gamepad, error: {}", - SDL_GetError()); - SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_GYRO, false); - } - if (SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_ACCEL, true)) { - m_accel_poll_rate = SDL_GetGamepadSensorDataRate(m_gamepad, SDL_SENSOR_ACCEL); - LOG_INFO(Input, "Accel initialized, poll rate: {}", m_accel_poll_rate); - } else { - LOG_ERROR(Input, "Failed to initialize accel controls for gamepad, error: {}", - SDL_GetError()); - SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_ACCEL, false); - } - } - - SDL_free(gamepads); - - int* rgb = Config::GetControllerCustomColor(); - - if (isDualSense) { - if (SDL_SetJoystickLED(joystick, rgb[0], rgb[1], rgb[2]) == 0) { - LOG_INFO(Input, "Set DualSense LED to R:{} G:{} B:{}", rgb[0], rgb[1], rgb[2]); - } else { - LOG_ERROR(Input, "Failed to set DualSense LED: {}", SDL_GetError()); - } - } else { - SetLightBarRGB(rgb[0], rgb[1], rgb[2]); - } -} - -void SDLInputEngine::SetLightBarRGB(u8 r, u8 g, u8 b) { - if (m_gamepad) { - SDL_SetGamepadLED(m_gamepad, r, g, b); - } -} - -void SDLInputEngine::SetVibration(u8 smallMotor, u8 largeMotor) { - if (m_gamepad) { - const auto low_freq = (smallMotor / 255.0f) * 0xFFFF; - const auto high_freq = (largeMotor / 255.0f) * 0xFFFF; - SDL_RumbleGamepad(m_gamepad, low_freq, high_freq, -1); - } -} - -State SDLInputEngine::ReadState() { - State state{}; - state.time = Libraries::Kernel::sceKernelGetProcessTime(); - - // Buttons - for (u8 i = 0; i < SDL_GAMEPAD_BUTTON_COUNT; ++i) { - auto orbisButton = SDLGamepadToOrbisButton(i); - if (orbisButton == OrbisPadButtonDataOffset::None) { - continue; - } - state.OnButton(orbisButton, SDL_GetGamepadButton(m_gamepad, (SDL_GamepadButton)i)); - } - - // Axes - for (int i = 0; i < static_cast(Axis::AxisMax); ++i) { - const auto axis = static_cast(i); - const auto value = SDL_GetGamepadAxis(m_gamepad, InputAxisToSDL(axis)); - switch (axis) { - case Axis::TriggerLeft: - case Axis::TriggerRight: - state.OnAxis(axis, GetAxis(0, 0x8000, value)); - break; - default: - state.OnAxis(axis, GetAxis(-0x8000, 0x8000, value)); - break; - } - } - - // Touchpad - if (SDL_GetNumGamepadTouchpads(m_gamepad) > 0) { - for (int finger = 0; finger < 2; ++finger) { - bool down; - float x, y; - if (SDL_GetGamepadTouchpadFinger(m_gamepad, 0, finger, &down, &x, &y, NULL)) { - state.OnTouchpad(finger, down, x, y); - } - } - } - - // Gyro - if (SDL_GamepadHasSensor(m_gamepad, SDL_SENSOR_GYRO)) { - float gyro[3]; - if (SDL_GetGamepadSensorData(m_gamepad, SDL_SENSOR_GYRO, gyro, 3)) { - state.OnGyro(gyro); - } - } - - // Accel - if (SDL_GamepadHasSensor(m_gamepad, SDL_SENSOR_ACCEL)) { - float accel[3]; - if (SDL_GetGamepadSensorData(m_gamepad, SDL_SENSOR_ACCEL, accel, 3)) { - state.OnAccel(accel); - } - } - - return state; -} - -float SDLInputEngine::GetGyroPollRate() const { - return m_gyro_poll_rate; -} - -float SDLInputEngine::GetAccelPollRate() const { - return m_accel_poll_rate; -} - -} // namespace Input - -namespace Frontend { - -using namespace Libraries::Pad; - -std::mutex motion_control_mutex; -float gyro_buf[3] = {0.0f, 0.0f, 0.0f}, accel_buf[3] = {0.0f, 9.81f, 0.0f}; -static Uint32 SDLCALL PollGyroAndAccel(void* userdata, SDL_TimerID timer_id, Uint32 interval) { +static Uint32 SDLCALL PollController(void* userdata, SDL_TimerID timer_id, Uint32 interval) { auto* controller = reinterpret_cast(userdata); - std::scoped_lock l{motion_control_mutex}; - controller->Gyro(0, gyro_buf); - controller->Acceleration(0, accel_buf); - return 4; + controller->UpdateAxisSmoothing(); + controller->Gyro(0); + controller->Acceleration(0); + return interval; } -WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_, +static Uint32 SDLCALL PollControllerLightColour(void* userdata, SDL_TimerID timer_id, + Uint32 interval) { + auto* controller = reinterpret_cast(userdata); + controller->PollLightColour(); + return interval; +} + +WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controllers_, std::string_view window_title) - : width{width_}, height{height_}, controller{controller_} { + : width{width_}, height{height_}, controllers{*controllers_} { if (!SDL_SetHint(SDL_HINT_APP_NAME, "shadPS4")) { UNREACHABLE_MSG("Failed to set SDL window hint: {}", SDL_GetError()); } @@ -330,7 +136,6 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ SDL_SyncWindow(window); SDL_InitSubSystem(SDL_INIT_GAMEPAD); - controller->SetEngine(std::make_unique()); #if defined(SDL_PLATFORM_WIN32) window_info.type = WindowSystemType::Windows; @@ -355,11 +160,11 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(window)); #endif // input handler init-s - Input::ControllerOutput::SetControllerOutputController(controller); Input::ControllerOutput::LinkJoystickAxes(); Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); + controllers.TryOpenSDLControllers(); - if (Config::getBackgroundControllerInput()) { + if (EmulatorSettings.IsBackgroundControllerInput()) { SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); } } @@ -399,37 +204,16 @@ void WindowSDL::WaitEvent() { break; case SDL_EVENT_GAMEPAD_ADDED: case SDL_EVENT_GAMEPAD_REMOVED: - controller->SetEngine(std::make_unique()); - break; - case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: - case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: - case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: - controller->SetTouchpadState(event.gtouchpad.finger, - event.type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP, event.gtouchpad.x, - event.gtouchpad.y); + controllers.TryOpenSDLControllers(); break; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: case SDL_EVENT_GAMEPAD_AXIS_MOTION: - OnGamepadEvent(&event); - break; - // i really would have appreciated ANY KIND OF DOCUMENTATION ON THIS - // AND IT DOESN'T EVEN USE PROPER ENUMS + case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: - switch ((SDL_SensorType)event.gsensor.sensor) { - case SDL_SENSOR_GYRO: { - std::scoped_lock l{motion_control_mutex}; - memcpy(gyro_buf, event.gsensor.data, sizeof(gyro_buf)); - break; - } - case SDL_SENSOR_ACCEL: { - std::scoped_lock l{motion_control_mutex}; - memcpy(accel_buf, event.gsensor.data, sizeof(accel_buf)); - break; - } - default: - break; - } + OnGamepadEvent(&event); break; case SDL_EVENT_QUIT: is_open = false; @@ -455,7 +239,7 @@ void WindowSDL::WaitEvent() { } break; case SDL_EVENT_CHANGE_CONTROLLER: - controller->GetEngine()->Init(); + UNREACHABLE_MSG("todo"); break; case SDL_EVENT_TOGGLE_SIMPLE_FPS: Overlay::ToggleSimpleFps(); @@ -476,6 +260,29 @@ void WindowSDL::WaitEvent() { Input::ToggleMouseModeTo(Input::MouseMode::Touchpad)); SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), false); break; + case SDL_EVENT_ADD_VIRTUAL_USER: + for (int i = 0; i < 4; i++) { + if (controllers[i]->user_id == -1) { + auto u = UserManagement.GetUserByPlayerIndex(i + 1); + if (!u) { + break; + } + controllers[i]->user_id = u->user_id; + UserManagement.LoginUser(u, i + 1); + break; + } + } + break; + case SDL_EVENT_REMOVE_VIRTUAL_USER: + LOG_INFO(Input, "Remove user"); + for (int i = 3; i >= 0; i--) { + if (controllers[i]->user_id != -1) { + UserManagement.LogoutUser(UserManagement.GetUserByID(controllers[i]->user_id)); + controllers[i]->user_id = -1; + break; + } + } + break; case SDL_EVENT_RDOC_CAPTURE: VideoCore::TriggerCapture(); break; @@ -485,8 +292,10 @@ void WindowSDL::WaitEvent() { } void WindowSDL::InitTimers() { - SDL_AddTimer(4, &PollGyroAndAccel, controller); - SDL_AddTimer(33, Input::MousePolling, (void*)controller); + for (int i = 0; i < 4; ++i) { + SDL_AddTimer(4, &PollController, controllers[i]); + } + SDL_AddTimer(33, Input::MousePolling, (void*)controllers[0]); } void WindowSDL::RequestKeyboard() { @@ -554,10 +363,44 @@ void WindowSDL::OnGamepadEvent(const SDL_Event* event) { // as it would break the entire touchpad handling // You can still bind other things to it though if (event->gbutton.button == SDL_GAMEPAD_BUTTON_TOUCHPAD) { - controller->Button(0, OrbisPadButtonDataOffset::TouchPad, input_down); + controllers[controllers.GetGamepadIndexFromJoystickId(event->gbutton.which)]->Button( + OrbisPadButtonDataOffset::TouchPad, input_down); return; } + u8 gamepad; + + switch (event->type) { + case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: + switch ((SDL_SensorType)event->gsensor.sensor) { + case SDL_SENSOR_GYRO: + gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which); + if (gamepad < 5) { + controllers[gamepad]->UpdateGyro(event->gsensor.data); + } + break; + case SDL_SENSOR_ACCEL: + gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which); + if (gamepad < 5) { + controllers[gamepad]->UpdateAcceleration(event->gsensor.data); + } + break; + default: + break; + } + return; + case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: + controllers[controllers.GetGamepadIndexFromJoystickId(event->gtouchpad.which)] + ->SetTouchpadState(event->gtouchpad.finger, + event->type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP, event->gtouchpad.x, + event->gtouchpad.y); + return; + default: + break; + } + // add/remove it from the list bool inputs_changed = Input::UpdatePressedKeys(input_event); diff --git a/src/sdl_window.h b/src/sdl_window.h index 3a4341de5..4fc750bbc 100644 --- a/src/sdl_window.h +++ b/src/sdl_window.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -14,23 +14,8 @@ struct SDL_Gamepad; union SDL_Event; namespace Input { - -class SDLInputEngine : public Engine { -public: - ~SDLInputEngine() override; - void Init() override; - void SetLightBarRGB(u8 r, u8 g, u8 b) override; - void SetVibration(u8 smallMotor, u8 largeMotor) override; - float GetGyroPollRate() const override; - float GetAccelPollRate() const override; - State ReadState() override; - -private: - float m_gyro_poll_rate = 0.0f; - float m_accel_poll_rate = 0.0f; -}; - -} // namespace Input +class GameController; +} namespace Frontend { @@ -62,7 +47,7 @@ class WindowSDL { int keyboard_grab = 0; public: - explicit WindowSDL(s32 width, s32 height, Input::GameController* controller, + explicit WindowSDL(s32 width, s32 height, Input::GameControllers* controllers, std::string_view window_title); ~WindowSDL(); @@ -100,7 +85,7 @@ private: private: s32 width; s32 height; - Input::GameController* controller; + Input::GameControllers controllers{}; WindowSystemInfo window_info{}; SDL_Window* window{}; bool is_shown{}; From 5b60b73e9a6fa1a1b6eac5487085e35109114965 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 27 Mar 2026 17:58:54 +0200 Subject: [PATCH 43/44] added new trophies and saves dirs (#4177) --- src/core/file_format/trp.cpp | 150 ++-- src/core/file_format/trp.h | 8 +- src/core/libraries/np/np_manager.cpp | 12 +- src/core/libraries/np/np_trophy.cpp | 771 ++++++++++++------ src/core/libraries/np/np_trophy.h | 2 - .../libraries/save_data/save_instance.cpp | 12 +- src/core/libraries/save_data/savedata.cpp | 12 +- src/emulator.cpp | 53 +- 8 files changed, 635 insertions(+), 385 deletions(-) diff --git a/src/core/file_format/trp.cpp b/src/core/file_format/trp.cpp index f0a258c12..6269fc6c7 100644 --- a/src/core/file_format/trp.cpp +++ b/src/core/file_format/trp.cpp @@ -5,7 +5,6 @@ #include "common/key_manager.h" #include "common/logging/log.h" #include "common/path_util.h" -#include "core/file_format/npbind.h" #include "core/file_format/trp.h" static void DecryptEFSM(std::span trophyKey, std::span NPcommID, @@ -43,8 +42,10 @@ static void hexToBytes(const char* hex, unsigned char* dst) { } } -bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string titleId) { - std::filesystem::path gameSysDir = trophyPath / "sce_sys/trophy/"; +bool TRP::Extract(const std::filesystem::path& trophyPath, int index, std::string npCommId, + const std::filesystem::path& outputPath) { + std::filesystem::path gameSysDir = + trophyPath / "sce_sys/trophy/" / std::format("trophy{:02d}.trp", index); if (!std::filesystem::exists(gameSysDir)) { LOG_WARNING(Common_Filesystem, "Game trophy directory doesn't exist"); return false; @@ -61,117 +62,82 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit std::array user_key{}; std::copy(user_key_vec.begin(), user_key_vec.end(), user_key.begin()); - // Load npbind.dat using the new class - std::filesystem::path npbindPath = trophyPath / "sce_sys/npbind.dat"; - NPBindFile npbind; - if (!npbind.Load(npbindPath.string())) { - LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file"); - } - - auto npCommIds = npbind.GetNpCommIds(); - if (npCommIds.empty()) { - LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat"); - } - bool success = true; int trpFileIndex = 0; try { - // Process each TRP file in the trophy directory - for (const auto& it : std::filesystem::directory_iterator(gameSysDir)) { - if (!it.is_regular_file() || it.path().extension() != ".trp") { - continue; // Skip non-TRP files - } + const auto& it = gameSysDir; + if (it.extension() != ".trp") { + return false; + } + Common::FS::IOFile file(it, Common::FS::FileAccessMode::Read); + if (!file.IsOpen()) { + LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.string()); + return false; + } - // Get NPCommID for this TRP file (if available) - std::string npCommId; - if (trpFileIndex < static_cast(npCommIds.size())) { - npCommId = npCommIds[trpFileIndex]; - LOG_DEBUG(Common_Filesystem, "Using NPCommID: {} for {}", npCommId, - it.path().filename().string()); - } else { - LOG_WARNING(Common_Filesystem, "No NPCommID found for TRP file index {}", - trpFileIndex); - } + TrpHeader header; + if (!file.Read(header)) { + LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}", it.string()); + return false; + } - Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read); - if (!file.IsOpen()) { - LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.path().string()); + if (header.magic != TRP_MAGIC) { + LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.string()); + return false; + } + + s64 seekPos = sizeof(TrpHeader); + // Create output directories + if (!std::filesystem::create_directories(outputPath / "Icons") || + !std::filesystem::create_directories(outputPath / "Xml")) { + LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", npCommId); + return false; + } + + // Process each entry in the TRP file + for (int i = 0; i < header.entry_num; i++) { + if (!file.Seek(seekPos)) { + LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset"); success = false; - continue; + break; } + seekPos += static_cast(header.entry_size); - TrpHeader header; - if (!file.Read(header)) { - LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}", - it.path().string()); + TrpEntry entry; + if (!file.Read(entry)) { + LOG_ERROR(Common_Filesystem, "Failed to read TRP entry"); success = false; - continue; + break; } - if (header.magic != TRP_MAGIC) { - LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.path().string()); - success = false; - continue; - } + std::string_view name(entry.entry_name); - s64 seekPos = sizeof(TrpHeader); - std::filesystem::path trpFilesPath( - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId / - "TrophyFiles" / it.path().stem()); - - // Create output directories - if (!std::filesystem::create_directories(trpFilesPath / "Icons") || - !std::filesystem::create_directories(trpFilesPath / "Xml")) { - LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", titleId); - success = false; - continue; - } - - // Process each entry in the TRP file - for (int i = 0; i < header.entry_num; i++) { - if (!file.Seek(seekPos)) { - LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset"); + if (entry.flag == ENTRY_FLAG_PNG) { + if (!ProcessPngEntry(file, entry, outputPath, name)) { success = false; - break; + // Continue with next entry } - seekPos += static_cast(header.entry_size); - - TrpEntry entry; - if (!file.Read(entry)) { - LOG_ERROR(Common_Filesystem, "Failed to read TRP entry"); - success = false; - break; - } - - std::string_view name(entry.entry_name); - - if (entry.flag == ENTRY_FLAG_PNG) { - if (!ProcessPngEntry(file, entry, trpFilesPath, name)) { + } else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) { + // Check if we have a valid NPCommID for decryption + if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') { + if (!ProcessEncryptedXmlEntry(file, entry, outputPath, name, user_key, + npCommId)) { success = false; // Continue with next entry } - } else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) { - // Check if we have a valid NPCommID for decryption - if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') { - if (!ProcessEncryptedXmlEntry(file, entry, trpFilesPath, name, user_key, - npCommId)) { - success = false; - // Continue with next entry - } - } else { - LOG_WARNING(Common_Filesystem, - "Skipping encrypted XML entry - invalid NPCommID"); - // Skip this entry but continue - } } else { - LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}", - static_cast(entry.flag), name); + LOG_WARNING(Common_Filesystem, + "Skipping encrypted XML entry - invalid NPCommID"); + // Skip this entry but continue } + } else { + LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}", + static_cast(entry.flag), name); } - trpFileIndex++; } + } catch (const std::filesystem::filesystem_error& e) { LOG_CRITICAL(Common_Filesystem, "Filesystem error during trophy extraction: {}", e.what()); return false; @@ -182,7 +148,7 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit if (success) { LOG_INFO(Common_Filesystem, "Successfully extracted {} trophy files for {}", trpFileIndex, - titleId); + npCommId); } return success; diff --git a/src/core/file_format/trp.h b/src/core/file_format/trp.h index 2b52a4d57..df0ea6eaf 100644 --- a/src/core/file_format/trp.h +++ b/src/core/file_format/trp.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -36,7 +36,8 @@ class TRP { public: TRP(); ~TRP(); - bool Extract(const std::filesystem::path& trophyPath, const std::string titleId); + bool Extract(const std::filesystem::path& trophyPath, int index, std::string npCommId, + const std::filesystem::path& outputPath); private: bool ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry, @@ -45,9 +46,6 @@ private: const std::filesystem::path& outputPath, std::string_view name, const std::array& user_key, const std::string& npCommId); - std::vector NPcommID = std::vector(12); - std::array np_comm_id{}; std::array esfmIv{}; - std::filesystem::path trpFilesPath; static constexpr int iv_len = 16; }; diff --git a/src/core/libraries/np/np_manager.cpp b/src/core/libraries/np/np_manager.cpp index 62cad455b..0ffbb682a 100644 --- a/src/core/libraries/np/np_manager.cpp +++ b/src/core/libraries/np/np_manager.cpp @@ -1,13 +1,13 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include -#include -#include "common/config.h" +#include #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" @@ -632,7 +632,8 @@ s32 PS4_SYSV_ABI sceNpGetNpId(Libraries::UserService::OrbisUserServiceUserId use return ORBIS_NP_ERROR_SIGNED_OUT; } memset(np_id, 0, sizeof(OrbisNpId)); - strncpy(np_id->handle.data, Config::getUserName().c_str(), sizeof(np_id->handle.data)); + strncpy(np_id->handle.data, UserManagement.GetDefaultUser().user_name.c_str(), + sizeof(np_id->handle.data)); return ORBIS_OK; } @@ -646,7 +647,8 @@ s32 PS4_SYSV_ABI sceNpGetOnlineId(Libraries::UserService::OrbisUserServiceUserId return ORBIS_NP_ERROR_SIGNED_OUT; } memset(online_id, 0, sizeof(OrbisNpOnlineId)); - strncpy(online_id->data, Config::getUserName().c_str(), sizeof(online_id->data)); + strncpy(online_id->data, UserManagement.GetDefaultUser().user_name.c_str(), + sizeof(online_id->data)); return ORBIS_OK; } diff --git a/src/core/libraries/np/np_trophy.cpp b/src/core/libraries/np/np_trophy.cpp index 976d614c0..287d8d295 100644 --- a/src/core/libraries/np/np_trophy.cpp +++ b/src/core/libraries/np/np_trophy.cpp @@ -1,22 +1,118 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include +#include "common/elf_info.h" #include "common/logging/log.h" #include "common/path_util.h" #include "common/slot_vector.h" +#include "core/emulator_settings.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" #include "core/libraries/np/np_trophy.h" #include "core/libraries/np/np_trophy_error.h" #include "core/libraries/np/trophy_ui.h" +#include "core/libraries/system/userservice.h" #include "core/memory.h" namespace Libraries::Np::NpTrophy { -std::string game_serial; +// PS4 system language IDs map directly to TROP00.XML .. TROP30.XML. +// Index = OrbisSystemServiceParamId language value reported by the system. +// clang-format off +static constexpr std::array s_language_xml_names = { + "TROP_00.XML", // 00 Japanese + "TROP_01.XML", // 01 English (US) + "TROP_02.XML", // 02 French + "TROP_03.XML", // 03 Spanish (ES) + "TROP_04.XML", // 04 German + "TROP_05.XML", // 05 Italian + "TROP_06.XML", // 06 Dutch + "TROP_07.XML", // 07 Portuguese (PT) + "TROP_08.XML", // 08 Russian + "TROP_09.XML", // 09 Korean + "TROP_10.XML", // 10 Traditional Chinese + "TROP_11.XML", // 11 Simplified Chinese + "TROP_12.XML", // 12 Finnish + "TROP_13.XML", // 13 Swedish + "TROP_14.XML", // 14 Danish + "TROP_15.XML", // 15 Norwegian + "TROP_16.XML", // 16 Polish + "TROP_17.XML", // 17 Portuguese (BR) + "TROP_18.XML", // 18 English (GB) + "TROP_19.XML", // 19 Turkish + "TROP_20.XML", // 20 Spanish (LA) + "TROP_21.XML", // 21 Arabic + "TROP_22.XML", // 22 French (CA) + "TROP_23.XML", // 23 Czech + "TROP_24.XML", // 24 Hungarian + "TROP_25.XML", // 25 Greek + "TROP_26.XML", // 26 Romanian + "TROP_27.XML", // 27 Thai + "TROP_28.XML", // 28 Vietnamese + "TROP_29.XML", // 29 Indonesian + "TROP_30.XML", // 30 Unkrainian +}; +// clang-format on + +// Returns the best available trophy XML path for the current system language. +// Resolution order: +// 1. TROP_XX.XML for the active system language (e.g. TROP01.XML for English) +// 2. TROP.XML (master / language-neutral fallback) +static std::filesystem::path GetTrophyXmlPath(const std::filesystem::path& xml_dir, + int system_language) { + // Try the exact language file first. + if (system_language >= 0 && system_language < static_cast(s_language_xml_names.size())) { + auto lang_path = xml_dir / s_language_xml_names[system_language]; + if (std::filesystem::exists(lang_path)) { + return lang_path; + } + } + // Final fallback: master TROP.XML (always present). + return xml_dir / "TROP.XML"; +} + +static void ApplyUnlockToXmlFile(const std::filesystem::path& xml_path, OrbisNpTrophyId trophyId, + u64 trophyTimestamp, bool unlock_platinum, + OrbisNpTrophyId platinumId, u64 platinumTimestamp) { + pugi::xml_document doc; + if (!doc.load_file(xml_path.native().c_str())) { + LOG_WARNING(Lib_NpTrophy, "ApplyUnlock: failed to load {}", xml_path.string()); + return; + } + + auto trophyconf = doc.child("trophyconf"); + for (pugi::xml_node& node : trophyconf.children()) { + if (std::string_view(node.name()) != "trophy") { + continue; + } + int id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + + auto set_unlock = [&](u64 ts) { + if (node.attribute("unlockstate").empty()) { + node.append_attribute("unlockstate") = "true"; + } else { + node.attribute("unlockstate").set_value("true"); + } + const auto ts_str = std::to_string(ts); + if (node.attribute("timestamp").empty()) { + node.append_attribute("timestamp") = ts_str.c_str(); + } else { + node.attribute("timestamp").set_value(ts_str.c_str()); + } + }; + + if (id == trophyId) { + set_unlock(trophyTimestamp); + } else if (unlock_platinum && id == platinumId) { + set_unlock(platinumTimestamp); + } + } + + doc.save_file(xml_path.native().c_str()); +} static constexpr auto MaxTrophyHandles = 4u; static constexpr auto MaxTrophyContexts = 8u; @@ -30,6 +126,11 @@ struct ContextKeyHash { struct TrophyContext { u32 context_id; + bool registered = false; + std::filesystem::path trophy_xml_path; // resolved once at CreateContext + std::filesystem::path xml_dir; // .../Xml/ + std::filesystem::path xml_save_file; // The actual file for tracking progress per-user. + std::filesystem::path icons_dir; // .../Icons/ }; static Common::SlotVector trophy_handles{}; static Common::SlotVector trophy_contexts{}; @@ -94,66 +195,10 @@ OrbisNpTrophyGrade GetTrophyGradeFromChar(char trophyType) { } } -int PS4_SYSV_ABI sceNpTrophyAbortHandle(OrbisNpTrophyHandle handle) { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyCaptureScreenshot() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyDetails() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyFlagArray() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupArray() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupDetails() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfo() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, Libraries::UserService::OrbisUserServiceUserId user_id, uint32_t service_label, u64 options) { - ASSERT(options == 0ull); - if (!context) { + if (!context || options != 0ull) { return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; } @@ -169,7 +214,20 @@ s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, const auto ctx_id = trophy_contexts.insert(user_id, service_label); *context = ctx_id.index + 1; - contexts_internal[key].context_id = *context; + + auto& ctx = contexts_internal[key]; + ctx.context_id = *context; + + // Resolve and cache all paths once so callers never recompute them. + const std::string np_comm_id = Common::ElfInfo::Instance().GetNpCommIds()[service_label]; + const auto trophy_base = + Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "trophy" / np_comm_id; + ctx.xml_save_file = Common::FS::GetUserPath(Common::FS::PathType::HomeDir) / + std::to_string(user_id) / "trophy" / (np_comm_id + ".xml"); + ctx.xml_dir = trophy_base / "Xml"; + ctx.icons_dir = trophy_base / "Icons"; + ctx.trophy_xml_path = GetTrophyXmlPath(ctx.xml_dir, EmulatorSettings.GetConsoleLanguage()); + LOG_INFO(Lib_NpTrophy, "New context = {}, user_id = {} service label = {}", *context, user_id, service_label); @@ -206,6 +264,10 @@ int PS4_SYSV_ABI sceNpTrophyDestroyContext(OrbisNpTrophyContext context) { return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } + if (!trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + ContextKey contextkey = trophy_contexts[contextId]; trophy_contexts.erase(contextId); contexts_internal.erase(contextkey); @@ -251,12 +313,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGameIcon(OrbisNpTrophyContext context, OrbisNpTro return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto icon_file = trophy_dir / trophy_folder / "Icons" / "ICON0.PNG"; + const auto& ctx = contexts_internal[contextkey]; + + auto icon_file = ctx.icons_dir / "ICON0.PNG"; Common::FS::IOFile icon(icon_file, Common::FS::FileAccessMode::Read); if (!icon.IsOpen()) { @@ -304,12 +364,11 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.trophy_xml_path; + const auto& trophy_save_file = ctx.xml_save_file; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -336,7 +395,18 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro if (node_name == "group") game_info.num_groups++; + } + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); if (node_name == "trophy") { bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); std::string_view current_trophy_grade = node.attribute("ttype").value(); @@ -368,8 +438,9 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro data->unlocked_silver = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER]; data->unlocked_bronze = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE]; - // maybe this should be 1 instead of 100? - data->progress_percentage = 100; + data->progress_percentage = (game_info.num_trophies > 0) + ? (game_info.unlocked_trophies * 100u) / game_info.num_trophies + : 0; return ORBIS_OK; } @@ -411,12 +482,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.trophy_xml_path; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -450,7 +519,18 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr details->group_id = groupId; data->group_id = groupId; + } + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); if (node_name == "trophy") { bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); std::string_view current_trophy_grade = node.attribute("ttype").value(); @@ -484,15 +564,84 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr data->unlocked_silver = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER]; data->unlocked_bronze = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE]; - // maybe this should be 1 instead of 100? - data->progress_percentage = 100; + data->progress_percentage = + (group_info.num_trophies > 0) + ? (group_info.unlocked_trophies * 100u) / group_info.num_trophies + : 0; return ORBIS_OK; } int PS4_SYSV_ABI sceNpTrophyGetTrophyIcon(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle, OrbisNpTrophyId trophyId, void* buffer, u64* size) { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + if (size == nullptr) + return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; + + if (trophyId < 0 || trophyId >= ORBIS_NP_TROPHY_NUM_MAX) + return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; + + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + + if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + + Common::SlotId contextId; + contextId.index = context - 1; + if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size() || + !trophy_handles.is_allocated({static_cast(handle_index)})) { + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + } + + ContextKey contextkey = trophy_contexts[contextId]; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + + // Check that the trophy is unlocked and icons are only available for earned trophies. + pugi::xml_document doc; + if (!doc.load_file(ctx.xml_save_file.native().c_str())) { + LOG_ERROR(Lib_NpTrophy, "Failed to open trophy XML: {}", ctx.xml_save_file.string()); + return ORBIS_NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; + } + + bool unlocked = false; + bool found = false; + for (const pugi::xml_node& node : doc.child("trophyconf").children()) { + if (std::string_view(node.name()) != "trophy") + continue; + if (node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID) == trophyId) { + found = true; + unlocked = node.attribute("unlockstate").as_bool(); + break; + } + } + + if (!found) + return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; + + if (!unlocked) + return ORBIS_NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED; + + const std::string icon_name = fmt::format("TROP{:03d}.PNG", trophyId); + const auto icon_path = ctx.icons_dir / icon_name; + + Common::FS::IOFile icon(icon_path, Common::FS::FileAccessMode::Read); + if (!icon.IsOpen()) { + LOG_ERROR(Lib_NpTrophy, "Failed to open trophy icon: {}", icon_path.string()); + return ORBIS_NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; + } + + if (buffer != nullptr) { + ReadFile(icon, buffer, *size); + } else { + *size = icon.GetSize(); + } return ORBIS_OK; } @@ -507,7 +656,7 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; - if (trophyId >= 127) + if (trophyId >= ORBIS_NP_TROPHY_NUM_MAX) return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; if (details == nullptr || data == nullptr) @@ -522,12 +671,10 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.trophy_xml_path; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -545,12 +692,34 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT if (node_name == "trophy") { int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); if (current_trophy_id == trophyId) { - bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); - std::string_view current_trophy_grade = node.attribute("ttype").value(); std::string_view current_trophy_name = node.child("name").text().as_string(); std::string_view current_trophy_description = node.child("detail").text().as_string(); + strncpy(details->name, current_trophy_name.data(), ORBIS_NP_TROPHY_NAME_MAX_SIZE); + strncpy(details->description, current_trophy_description.data(), + ORBIS_NP_TROPHY_DESCR_MAX_SIZE); + } + } + } + + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); + + if (node_name == "trophy") { + int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + if (current_trophy_id == trophyId) { + bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); + std::string_view current_trophy_grade = node.attribute("ttype").value(); + uint64_t current_trophy_timestamp = node.attribute("timestamp").as_ullong(); int current_trophy_groupid = node.attribute("gid").as_int(-1); bool current_trophy_hidden = node.attribute("hidden").as_bool(); @@ -560,10 +729,6 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT details->group_id = current_trophy_groupid; details->hidden = current_trophy_hidden; - strncpy(details->name, current_trophy_name.data(), ORBIS_NP_TROPHY_NAME_MAX_SIZE); - strncpy(details->description, current_trophy_description.data(), - ORBIS_NP_TROPHY_DESCR_MAX_SIZE); - data->trophy_id = trophyId; data->unlocked = current_trophy_unlockstate; data->timestamp.tick = current_trophy_timestamp; @@ -579,29 +744,34 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context, OrbisNpTrophyFlagArray* flags, u32* count) { LOG_INFO(Lib_NpTrophy, "called"); + if (flags == nullptr || count == nullptr) + return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; - if (flags == nullptr || count == nullptr) - return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; - - ORBIS_NP_TROPHY_FLAG_ZERO(flags); - Common::SlotId contextId; contextId.index = context - 1; - if (contextId.index >= trophy_contexts.size()) { + if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) { return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } - ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size() || + !trophy_handles.is_allocated({static_cast(handle_index)})) { + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + } + + ContextKey contextkey = trophy_contexts[contextId]; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.xml_save_file; + + ORBIS_NP_TROPHY_FLAG_ZERO(flags); pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -622,10 +792,9 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context, if (node_name == "trophy") { num_trophies++; - } - - if (current_trophy_unlockstate) { - ORBIS_NP_TROPHY_FLAG_SET(current_trophy_id, flags); + if (current_trophy_unlockstate) { + ORBIS_NP_TROPHY_FLAG_SET(current_trophy_id, flags); + } } } @@ -633,6 +802,200 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context, return ORBIS_OK; } +int PS4_SYSV_ABI sceNpTrophyRegisterContext(OrbisNpTrophyContext context, + OrbisNpTrophyHandle handle, uint64_t options) { + if (options != 0ull) + return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; + + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + + if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + + Common::SlotId contextId; + contextId.index = context - 1; + if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size() || + !trophy_handles.is_allocated({static_cast(handle_index)})) { + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + } + + ContextKey contextkey = trophy_contexts[contextId]; + auto& ctx = contexts_internal[contextkey]; + + if (ctx.registered) + return ORBIS_NP_TROPHY_ERROR_ALREADY_REGISTERED; + + if (!std::filesystem::exists(ctx.trophy_xml_path)) + return ORBIS_NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED; + + ctx.registered = true; + LOG_INFO(Lib_NpTrophy, "Context {} registered", context); + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle, + OrbisNpTrophyId trophyId, OrbisNpTrophyId* platinumId) { + LOG_INFO(Lib_NpTrophy, "Unlocking trophy id {}", trophyId); + + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + + if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + + if (trophyId >= ORBIS_NP_TROPHY_NUM_MAX) + return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; + + if (platinumId == nullptr) + return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; + + Common::SlotId contextId; + contextId.index = context - 1; + if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size() || + !trophy_handles.is_allocated({static_cast(handle_index)})) { + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + } + + ContextKey contextkey = trophy_contexts[contextId]; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& xml_dir = ctx.xml_dir; + const auto& trophy_file = ctx.trophy_xml_path; + + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", save_result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); + if (std::string_view(node.name()) != "trophy") + continue; + + int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); + + if (current_trophy_id == trophyId) { + if (current_trophy_unlockstate) { + LOG_INFO(Lib_NpTrophy, "Trophy already unlocked"); + return ORBIS_NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; + } + } + } + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); + + if (!result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse trophy xml : {}", result.description()); + return ORBIS_NP_TROPHY_ERROR_TITLE_NOT_FOUND; + } + + *platinumId = ORBIS_NP_TROPHY_INVALID_TROPHY_ID; + + int num_trophies = 0; + int num_trophies_unlocked = 0; + pugi::xml_node platinum_node; + + // Outputs filled during the scan. + bool trophy_found = false; + const char* trophy_name = ""; + std::string_view trophy_type; + std::filesystem::path trophy_icon_path; + + auto trophyconf = doc.child("trophyconf"); + + for (pugi::xml_node& node : trophyconf.children()) { + if (std::string_view(node.name()) != "trophy") + continue; + + int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); + std::string_view current_trophy_type = node.attribute("ttype").value(); + + if (current_trophy_type == "P") { + platinum_node = node; + if (trophyId == current_trophy_id) { + return ORBIS_NP_TROPHY_ERROR_PLATINUM_CANNOT_UNLOCK; + } + } + + if (node.attribute("pid").as_int(-1) != ORBIS_NP_TROPHY_INVALID_TROPHY_ID) { + num_trophies++; + if (current_trophy_unlockstate) { + num_trophies_unlocked++; + } + } + + if (current_trophy_id == trophyId) { + trophy_found = true; + trophy_name = node.child("name").text().as_string(); + trophy_type = current_trophy_type; + + const std::string icon_file = fmt::format("TROP{:03d}.PNG", current_trophy_id); + trophy_icon_path = ctx.icons_dir / icon_file; + } + } + + if (!trophy_found) + return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; + + // Capture timestamps once so every file gets the exact same value. + const auto now_secs = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + const u64 trophy_timestamp = static_cast(now_secs); + + // Decide platinum. + bool unlock_platinum = false; + OrbisNpTrophyId platinum_id = ORBIS_NP_TROPHY_INVALID_TROPHY_ID; + u64 platinum_timestamp = 0; + const char* platinum_name = ""; + std::filesystem::path platinum_icon_path; + + if (!platinum_node.attribute("unlockstate").as_bool()) { + if ((num_trophies - 1) == num_trophies_unlocked) { + unlock_platinum = true; + platinum_id = platinum_node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + platinum_timestamp = trophy_timestamp; // same second is fine + platinum_name = platinum_node.child("name").text().as_string(); + + const std::string plat_icon_file = fmt::format("TROP{:03d}.PNG", platinum_id); + platinum_icon_path = ctx.icons_dir / plat_icon_file; + + *platinumId = platinum_id; + } + } + + // Queue UI notifications (only once, using the primary XML's strings). + AddTrophyToQueue(trophy_icon_path, trophy_name, trophy_type); + if (unlock_platinum) { + AddTrophyToQueue(platinum_icon_path, platinum_name, "P"); + } + + ApplyUnlockToXmlFile(ctx.xml_save_file, trophyId, trophy_timestamp, unlock_platinum, + platinum_id, platinum_timestamp); + LOG_INFO(Lib_NpTrophy, "Trophy {} successfully saved.", trophyId); + + return ORBIS_OK; +} + int PS4_SYSV_ABI sceNpTrophyGroupArrayGetNum() { LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); return ORBIS_OK; @@ -698,19 +1061,6 @@ int PS4_SYSV_ABI sceNpTrophyNumInfoGetTotal() { return ORBIS_OK; } -int PS4_SYSV_ABI sceNpTrophyRegisterContext(OrbisNpTrophyContext context, - OrbisNpTrophyHandle handle, uint64_t options) { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - - if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) - return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; - - if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) - return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; - - return ORBIS_OK; -} - int PS4_SYSV_ABI sceNpTrophySetInfoGetTrophyFlagArray() { LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); return ORBIS_OK; @@ -942,147 +1292,58 @@ int PS4_SYSV_ABI sceNpTrophySystemSetDbgParamInt() { return ORBIS_OK; } -int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle, - OrbisNpTrophyId trophyId, OrbisNpTrophyId* platinumId) { - LOG_INFO(Lib_NpTrophy, "Unlocking trophy id {}", trophyId); +int PS4_SYSV_ABI sceNpTrophyAbortHandle(OrbisNpTrophyHandle handle) { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) - return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; +int PS4_SYSV_ABI sceNpTrophyCaptureScreenshot() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) - return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyDetails() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (trophyId >= 127) - return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyFlagArray() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (platinumId == nullptr) - return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupArray() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - Common::SlotId contextId; - contextId.index = context - 1; - if (contextId.index >= trophy_contexts.size()) { - return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; - } - ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupDetails() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfo() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (!result) { - LOG_ERROR(Lib_NpTrophy, "Failed to parse trophy xml : {}", result.description()); - return ORBIS_OK; - } +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - *platinumId = ORBIS_NP_TROPHY_INVALID_TROPHY_ID; - - int num_trophies = 0; - int num_trophies_unlocked = 0; - pugi::xml_node platinum_node; - - auto trophyconf = doc.child("trophyconf"); - - for (pugi::xml_node& node : trophyconf.children()) { - int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); - bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); - const char* current_trophy_name = node.child("name").text().as_string(); - std::string_view current_trophy_description = node.child("detail").text().as_string(); - std::string_view current_trophy_type = node.attribute("ttype").value(); - - if (current_trophy_type == "P") { - platinum_node = node; - if (trophyId == current_trophy_id) { - return ORBIS_NP_TROPHY_ERROR_PLATINUM_CANNOT_UNLOCK; - } - } - - if (std::string_view(node.name()) == "trophy") { - if (node.attribute("pid").as_int(-1) != ORBIS_NP_TROPHY_INVALID_TROPHY_ID) { - num_trophies++; - if (current_trophy_unlockstate) { - num_trophies_unlocked++; - } - } - - if (current_trophy_id == trophyId) { - if (current_trophy_unlockstate) { - LOG_INFO(Lib_NpTrophy, "Trophy already unlocked"); - return ORBIS_NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; - } else { - if (node.attribute("unlockstate").empty()) { - node.append_attribute("unlockstate") = "true"; - } else { - node.attribute("unlockstate").set_value("true"); - } - - auto trophyTimestamp = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - - if (node.attribute("timestamp").empty()) { - node.append_attribute("timestamp") = - std::to_string(trophyTimestamp).c_str(); - } else { - node.attribute("timestamp") - .set_value(std::to_string(trophyTimestamp).c_str()); - } - - std::string trophy_icon_file = "TROP"; - trophy_icon_file.append(node.attribute("id").value()); - trophy_icon_file.append(".PNG"); - - std::filesystem::path current_icon_path = - trophy_dir / trophy_folder / "Icons" / trophy_icon_file; - - AddTrophyToQueue(current_icon_path, current_trophy_name, current_trophy_type); - } - } - } - } - - if (!platinum_node.attribute("unlockstate").as_bool()) { - if ((num_trophies - 1) == num_trophies_unlocked) { - if (platinum_node.attribute("unlockstate").empty()) { - platinum_node.append_attribute("unlockstate") = "true"; - } else { - platinum_node.attribute("unlockstate").set_value("true"); - } - - auto trophyTimestamp = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - - if (platinum_node.attribute("timestamp").empty()) { - platinum_node.append_attribute("timestamp") = - std::to_string(trophyTimestamp).c_str(); - } else { - platinum_node.attribute("timestamp") - .set_value(std::to_string(trophyTimestamp).c_str()); - } - - int platinum_trophy_id = - platinum_node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); - const char* platinum_trophy_name = platinum_node.child("name").text().as_string(); - - std::string platinum_icon_file = "TROP"; - platinum_icon_file.append(platinum_node.attribute("id").value()); - platinum_icon_file.append(".PNG"); - - std::filesystem::path platinum_icon_path = - trophy_dir / trophy_folder / "Icons" / platinum_icon_file; - - *platinumId = platinum_trophy_id; - AddTrophyToQueue(platinum_icon_path, platinum_trophy_name, "P"); - } - } - - doc.save_file((trophy_dir / trophy_folder / "Xml" / "TROP.XML").native().c_str()); +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} +int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); return ORBIS_OK; } diff --git a/src/core/libraries/np/np_trophy.h b/src/core/libraries/np/np_trophy.h index ab187ae13..590e58c0d 100644 --- a/src/core/libraries/np/np_trophy.h +++ b/src/core/libraries/np/np_trophy.h @@ -13,8 +13,6 @@ class SymbolsResolver; namespace Libraries::Np::NpTrophy { -extern std::string game_serial; - constexpr int ORBIS_NP_TROPHY_FLAG_SETSIZE = 128; constexpr int ORBIS_NP_TROPHY_FLAG_BITS_SHIFT = 5; diff --git a/src/core/libraries/save_data/save_instance.cpp b/src/core/libraries/save_data/save_instance.cpp index 4ff682357..463baa50b 100644 --- a/src/core/libraries/save_data/save_instance.cpp +++ b/src/core/libraries/save_data/save_instance.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -6,7 +6,6 @@ #include #include "common/assert.h" -#include "common/config.h" #include "common/path_util.h" #include "common/singleton.h" #include "core/emulator_settings.h" @@ -49,12 +48,13 @@ namespace Libraries::SaveData { fs::path SaveInstance::MakeTitleSavePath(Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view game_serial) { - return Config::GetSaveDataPath() / std::to_string(user_id) / game_serial; + return EmulatorSettings.GetHomeDir() / std::to_string(user_id) / "savedata" / game_serial; } -fs::path SaveInstance::MakeDirSavePath(Libraries::UserService::OrbisUserServiceUserId user_id, - std::string_view game_serial, std::string_view dir_name) { - return Config::GetSaveDataPath() / std::to_string(user_id) / game_serial / dir_name; +fs::path SaveInstance::MakeDirSavePath(OrbisUserServiceUserId user_id, std::string_view game_serial, + std::string_view dir_name) { + return EmulatorSettings.GetHomeDir() / std::to_string(user_id) / "savedata" / game_serial / + dir_name; } uint64_t SaveInstance::GetMaxBlockFromSFO(const PSF& psf) { diff --git a/src/core/libraries/save_data/savedata.cpp b/src/core/libraries/save_data/savedata.cpp index 48b086457..70c66e4cd 100644 --- a/src/core/libraries/save_data/savedata.cpp +++ b/src/core/libraries/save_data/savedata.cpp @@ -1,6 +1,7 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -8,13 +9,13 @@ #include #include "common/assert.h" -#include "common/config.h" #include "common/cstring.h" #include "common/elf_info.h" #include "common/enum.h" #include "common/logging/log.h" #include "common/path_util.h" #include "common/string_util.h" +#include "core/emulator_settings.h" #include "core/file_format/psf.h" #include "core/file_sys/fs.h" #include "core/libraries/error_codes.h" @@ -441,7 +442,8 @@ static Error saveDataMount(const OrbisSaveDataMount2* mount_info, LOG_INFO(Lib_SaveData, "called with invalid block size"); } - const auto root_save = Config::GetSaveDataPath(); + const auto root_save = + EmulatorSettings.GetHomeDir() / std::to_string(mount_info->userId) / "savedata"; fs::create_directories(root_save); const auto available = fs::space(root_save).available; @@ -489,7 +491,9 @@ static Error Umount(const OrbisSaveDataMountPoint* mountPoint, bool call_backup return Error::PARAMETER; } LOG_DEBUG(Lib_SaveData, "Umount mountPoint:{}", mountPoint->data.to_view()); - const std::string_view mount_point_str{mountPoint->data}; + + std::string mount_point_str = mountPoint->data.to_string(); + for (auto& instance : g_mount_slots) { if (instance.has_value()) { const auto& slot_name = instance->GetMountPoint(); diff --git a/src/emulator.cpp b/src/emulator.cpp index 447c72391..034d30ed8 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -28,6 +28,7 @@ #include "common/singleton.h" #include "core/debugger.h" #include "core/devtools/widget/module_list.h" +#include "core/emulator_settings.h" #include "core/emulator_state.h" #include "core/file_format/psf.h" #include "core/file_format/trp.h" @@ -38,6 +39,7 @@ #include "core/libraries/save_data/save_backup.h" #include "core/linker.h" #include "core/memory.h" +#include "core/user_settings.h" #include "emulator.h" #include "video_core/cache_storage.h" #include "video_core/renderdoc.h" @@ -50,6 +52,7 @@ #include #include #endif +#include Frontend::WindowSDL* g_window = nullptr; @@ -196,14 +199,20 @@ void Emulator::Run(std::filesystem::path file, std::vector args, } game_info.game_folder = game_folder; + std::filesystem::path npbindPath = game_folder / "sce_sys/npbind.dat"; + NPBindFile npbind; + if (!npbind.Load(npbindPath.string())) { + LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file"); + } else { + auto npCommIds = npbind.GetNpCommIds(); + if (npCommIds.empty()) { + LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat"); + } else { + game_info.npCommIds = std::move(npCommIds); + } + } EmulatorSettings.Load(id); - if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / - (id + ".json"))) { - EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(true); - } else { - EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(false); - } // Initialize logging as soon as possible if (!id.empty() && EmulatorSettings.IsSeparateLoggingEnabled()) { @@ -224,9 +233,8 @@ void Emulator::Run(std::filesystem::path file, std::vector args, LOG_INFO(Loader, "Description {}", Common::g_scm_desc); LOG_INFO(Loader, "Remote {}", Common::g_scm_remote_url); - const bool has_game_config = std::filesystem::exists( - Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".json")); - LOG_INFO(Config, "Game-specific config exists: {}", has_game_config); + LOG_INFO(Config, "Game-specific config used: {}", + EmulatorState::GetInstance()->IsGameSpecifigConfigUsed()); LOG_INFO(Config, "General LogType: {}", EmulatorSettings.GetLogType()); LOG_INFO(Config, "General isIdenticalLogGrouped: {}", EmulatorSettings.IsIdenticalLogGrouped()); @@ -298,15 +306,28 @@ void Emulator::Run(std::filesystem::path file, std::vector args, // Initialize patcher and trophies if (!id.empty()) { MemoryPatcher::g_game_serial = id; - Libraries::Np::NpTrophy::game_serial = id; - const auto trophyDir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / id / "TrophyFiles"; - if (!std::filesystem::exists(trophyDir)) { - TRP trp; - if (!trp.Extract(game_folder, id)) { - LOG_ERROR(Loader, "Couldn't extract trophies"); + int index = 0; + for (std::string npCommId : game_info.npCommIds) { + const auto trophyDir = + Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "trophy" / npCommId; + if (!std::filesystem::exists(trophyDir)) { + TRP trp; + if (!trp.Extract(game_folder, index, npCommId, trophyDir)) { + LOG_ERROR(Loader, "Couldn't extract trophies"); + } } + for (User user : UserSettings.GetUserManager().GetValidUsers()) { + auto const user_trophy_file = + Common::FS::GetUserPath(Common::FS::PathType::HomeDir) / + std::to_string(user.user_id) / "trophy" / (npCommId + ".xml"); + if (!std::filesystem::exists(user_trophy_file)) { + std::error_code discard; + std::filesystem::copy_file(trophyDir / "Xml" / "TROPCONF.XML", user_trophy_file, + discard); + } + } + index++; } } From df32a2076b27a84660115f1b3872c89ac928be56 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:20:26 +0100 Subject: [PATCH 44/44] readd missing line (#4180) Co-authored-by: dsprogrammingprojects --- src/input/input_handler.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/input/input_handler.h b/src/input/input_handler.h index 22ae0f4e0..ee286aea9 100644 --- a/src/input/input_handler.h +++ b/src/input/input_handler.h @@ -526,7 +526,7 @@ public: class ControllerAllOutputs { public: - static constexpr u64 output_count = 39; + static constexpr u64 output_count = 40; std::array data = { // Important: these have to be the first, or else they will update in the wrong order ControllerOutput(LEFTJOYSTICK_HALFMODE), @@ -563,6 +563,7 @@ public: ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY), ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER), ControllerOutput(HOTKEY_FULLSCREEN), ControllerOutput(HOTKEY_PAUSE),