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 01/27] 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 02/27] 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 03/27] 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), From 96411a17cb08e44598084eb89db2322de0fc8924 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:08:02 +0800 Subject: [PATCH 04/27] Imgui: translations (#4124) * WIP: imgui translations * fallback to original strings if tables are incomplete * reorder things a bit * construct tables as consts * Update imgui_translations.h --- CMakeLists.txt | 1 + src/imgui/imgui_translations.h | 184 +++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 src/imgui/imgui_translations.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cbf373e57..6bb22db33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1091,6 +1091,7 @@ set(IMGUI src/imgui/imgui_config.h src/imgui/imgui_layer.h src/imgui/imgui_std.h src/imgui/imgui_texture.h + src/imgui/imgui_translations.h src/imgui/renderer/imgui_core.cpp src/imgui/renderer/imgui_core.h src/imgui/renderer/imgui_impl_sdl3.cpp diff --git a/src/imgui/imgui_translations.h b/src/imgui/imgui_translations.h new file mode 100644 index 000000000..83f60f187 --- /dev/null +++ b/src/imgui/imgui_translations.h @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "core/emulator_settings.h" + +///////////// ImGui Translation Tables + +// disable clang line limits for ease of translation +// clang-format off + +const std::map JapaneseMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map FrenchMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map FrenchCanadaMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map SpanishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map SpanishLatinAmericanMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map GermanMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ItalianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map DutchMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map PortugesePtMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map PortugeseBrMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map RussianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map KoreanMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ChineseTraditionalMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ChineseSimplifiedMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map FinnishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map SwedishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map DanishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map NorwegianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map PolishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map TurkishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ArabicMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map CzechMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map HungarianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map GreekMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map RomanianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ThaiMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map VietnameseMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map IndonesianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map UkranianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +// clang-format on + +///////////// End ImGui Translation Tables + +const std::map> langMap = { + {0, JapaneseMap}, + // {1, EnglishUsMap}, - not used + {2, FrenchMap}, + {3, SpanishMap}, + {4, GermanMap}, + {5, ItalianMap}, + {6, DutchMap}, + {7, PortugesePtMap}, + {8, RussianMap}, + {9, KoreanMap}, + {10, ChineseTraditionalMap}, + {11, ChineseSimplifiedMap}, + {12, FinnishMap}, + {13, SwedishMap}, + {14, DanishMap}, + {15, NorwegianMap}, + {16, PolishMap}, + {17, PortugeseBrMap}, + // {18, "English (UK)"}, - not used + {19, TurkishMap}, + {20, SpanishLatinAmericanMap}, + {21, ArabicMap}, + {22, FrenchCanadaMap}, + {23, CzechMap}, + {24, HungarianMap}, + {25, GreekMap}, + {26, RomanianMap}, + {27, ThaiMap}, + {28, VietnameseMap}, + {29, IndonesianMap}, + {30, UkranianMap}, +}; + +namespace ImguiTranslate { + +std::string tr(std::string input) { + // since we're coding in English + if (EmulatorSettings.GetConsoleLanguage() == 1 || EmulatorSettings.GetConsoleLanguage() == 18) + return input; + + const std::map translationTable = + langMap.at(EmulatorSettings.GetConsoleLanguage()); + + if (!translationTable.contains(input)) { + return input; + } + + return translationTable.at(input); +} + +} // namespace ImguiTranslate From 1018660ad70453b94fdb8ef58872de4e160d68a3 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:34:30 +0100 Subject: [PATCH 05/27] Make sure user trophy folder exists before creating files in it (#4186) * Make sure user trophy folder exists before creating files in it * ??? --- src/emulator.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/emulator.cpp b/src/emulator.cpp index 034d30ed8..7e62680f6 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -322,6 +322,8 @@ void Emulator::Run(std::filesystem::path file, std::vector args, Common::FS::GetUserPath(Common::FS::PathType::HomeDir) / std::to_string(user.user_id) / "trophy" / (npCommId + ".xml"); if (!std::filesystem::exists(user_trophy_file)) { + auto temp = user_trophy_file.parent_path(); + std::filesystem::create_directories(temp); std::error_code discard; std::filesystem::copy_file(trophyDir / "Xml" / "TROPCONF.XML", user_trophy_file, discard); From 1012f84bf99700f3153f7d7d943eacd732becc06 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Sun, 29 Mar 2026 14:58:48 +0800 Subject: [PATCH 06/27] use cpp file for function (#4187) --- CMakeLists.txt | 3 +- src/imgui/imgui_translations.cpp | 58 ++++++++++++++++++++++++++++++++ src/imgui/imgui_translations.h | 56 +++--------------------------- 3 files changed, 64 insertions(+), 53 deletions(-) create mode 100644 src/imgui/imgui_translations.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bb22db33..7e6349ff9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1091,6 +1091,7 @@ set(IMGUI src/imgui/imgui_config.h src/imgui/imgui_layer.h src/imgui/imgui_std.h src/imgui/imgui_texture.h + src/imgui/imgui_translations.cpp src/imgui/imgui_translations.h src/imgui/renderer/imgui_core.cpp src/imgui/renderer/imgui_core.h @@ -1275,4 +1276,4 @@ install(TARGETS shadps4 BUNDLE DESTINATION .) else() enable_testing() add_subdirectory(tests) -endif() \ No newline at end of file +endif() diff --git a/src/imgui/imgui_translations.cpp b/src/imgui/imgui_translations.cpp new file mode 100644 index 000000000..608f980c3 --- /dev/null +++ b/src/imgui/imgui_translations.cpp @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/emulator_settings.h" +#include "imgui_translations.h" + +namespace ImguiTranslate { + +const std::map> langMap = { + {0, JapaneseMap}, + // {1, EnglishUsMap}, - not used + {2, FrenchMap}, + {3, SpanishMap}, + {4, GermanMap}, + {5, ItalianMap}, + {6, DutchMap}, + {7, PortugesePtMap}, + {8, RussianMap}, + {9, KoreanMap}, + {10, ChineseTraditionalMap}, + {11, ChineseSimplifiedMap}, + {12, FinnishMap}, + {13, SwedishMap}, + {14, DanishMap}, + {15, NorwegianMap}, + {16, PolishMap}, + {17, PortugeseBrMap}, + // {18, "English (UK)"}, - not used + {19, TurkishMap}, + {20, SpanishLatinAmericanMap}, + {21, ArabicMap}, + {22, FrenchCanadaMap}, + {23, CzechMap}, + {24, HungarianMap}, + {25, GreekMap}, + {26, RomanianMap}, + {27, ThaiMap}, + {28, VietnameseMap}, + {29, IndonesianMap}, + {30, UkranianMap}, +}; + +std::string tr(std::string input) { + // since we're coding in English + if (EmulatorSettings.GetConsoleLanguage() == 1 || EmulatorSettings.GetConsoleLanguage() == 18) + return input; + + const std::map translationTable = + langMap.at(EmulatorSettings.GetConsoleLanguage()); + + if (!translationTable.contains(input)) { + return input; + } + + return translationTable.at(input); +} + +} // namespace ImguiTranslate diff --git a/src/imgui/imgui_translations.h b/src/imgui/imgui_translations.h index 83f60f187..d6844b759 100644 --- a/src/imgui/imgui_translations.h +++ b/src/imgui/imgui_translations.h @@ -2,8 +2,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include -#include "core/emulator_settings.h" +namespace ImguiTranslate { + +std::string tr(std::string input); ///////////// ImGui Translation Tables @@ -130,55 +133,4 @@ const std::map UkranianMap = { ///////////// End ImGui Translation Tables -const std::map> langMap = { - {0, JapaneseMap}, - // {1, EnglishUsMap}, - not used - {2, FrenchMap}, - {3, SpanishMap}, - {4, GermanMap}, - {5, ItalianMap}, - {6, DutchMap}, - {7, PortugesePtMap}, - {8, RussianMap}, - {9, KoreanMap}, - {10, ChineseTraditionalMap}, - {11, ChineseSimplifiedMap}, - {12, FinnishMap}, - {13, SwedishMap}, - {14, DanishMap}, - {15, NorwegianMap}, - {16, PolishMap}, - {17, PortugeseBrMap}, - // {18, "English (UK)"}, - not used - {19, TurkishMap}, - {20, SpanishLatinAmericanMap}, - {21, ArabicMap}, - {22, FrenchCanadaMap}, - {23, CzechMap}, - {24, HungarianMap}, - {25, GreekMap}, - {26, RomanianMap}, - {27, ThaiMap}, - {28, VietnameseMap}, - {29, IndonesianMap}, - {30, UkranianMap}, -}; - -namespace ImguiTranslate { - -std::string tr(std::string input) { - // since we're coding in English - if (EmulatorSettings.GetConsoleLanguage() == 1 || EmulatorSettings.GetConsoleLanguage() == 18) - return input; - - const std::map translationTable = - langMap.at(EmulatorSettings.GetConsoleLanguage()); - - if (!translationTable.contains(input)) { - return input; - } - - return translationTable.at(input); -} - } // namespace ImguiTranslate From 2334981be80f8513acb352ce8c442077609e3beb Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:21:21 +0800 Subject: [PATCH 07/27] respect emulator settings home directory for trophies (#4188) --- src/core/libraries/np/np_trophy.cpp | 4 ++-- src/emulator.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/libraries/np/np_trophy.cpp b/src/core/libraries/np/np_trophy.cpp index 287d8d295..449ee775a 100644 --- a/src/core/libraries/np/np_trophy.cpp +++ b/src/core/libraries/np/np_trophy.cpp @@ -222,8 +222,8 @@ s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, 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_save_file = + EmulatorSettings.GetHomeDir() / 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()); diff --git a/src/emulator.cpp b/src/emulator.cpp index 7e62680f6..a9496f5f0 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -318,9 +318,9 @@ void Emulator::Run(std::filesystem::path file, std::vector args, } } 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"); + auto const user_trophy_file = EmulatorSettings.GetHomeDir() / + std::to_string(user.user_id) / "trophy" / + (npCommId + ".xml"); if (!std::filesystem::exists(user_trophy_file)) { auto temp = user_trophy_file.parent_path(); std::filesystem::create_directories(temp); From a87abee8e30f6b060f1dc70d137e7cc497d58b12 Mon Sep 17 00:00:00 2001 From: Ploo <239304139+xinitrcn1@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:44:29 +0000 Subject: [PATCH 08/27] WIP: port: Add x64 FreeBSD (#3927) * port: Add x64 FreeBSD * clang formaa * fix epoll stuffs * date-tz for fbsd, force submodule zydis * fix filesystem hang + date-tz * fix * fix freebsd SIGBUS * madvise() ifdef * signal fix + camera fix * proper %gs tls for once * better tls? + clang format --------- Co-authored-by: lizzie --- CMakeLists.txt | 13 +++- externals/CMakeLists.txt | 24 ++++--- src/common/adaptive_mutex.h | 2 +- src/common/signal_context.cpp | 31 ++++++--- src/core/address_space.cpp | 17 ++++- src/core/debugger.cpp | 8 ++- src/core/file_sys/fs.cpp | 64 ++++++++++--------- .../libraries/kernel/threads/exception.cpp | 24 ++++++- src/core/libraries/kernel/threads/exception.h | 2 +- src/core/libraries/kernel/time.cpp | 4 +- src/core/libraries/network/net.cpp | 13 +++- src/core/libraries/network/net_epoll.cpp | 4 ++ src/core/libraries/network/net_epoll.h | 5 +- src/core/libraries/network/net_util.cpp | 9 ++- src/core/tls.cpp | 11 +++- src/emulator.cpp | 2 +- src/sdl_window.cpp | 5 +- src/video_core/buffer_cache/region_manager.h | 2 +- src/video_core/page_manager.cpp | 34 +++++----- src/video_core/page_manager.h | 10 +-- 20 files changed, 189 insertions(+), 95 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e6349ff9..147903a7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -245,7 +245,7 @@ find_package(VulkanMemoryAllocator 3.1.0 CONFIG) find_package(xbyak 7.07 CONFIG) find_package(xxHash 0.8.2 MODULE) find_package(ZLIB 1.3 MODULE) -find_package(Zydis 5.0.0 CONFIG) +find_package(Zydis 5.0.0 MODULE) find_package(pugixml 1.14 CONFIG) if (APPLE) find_package(date 3.0.1 CONFIG) @@ -1139,7 +1139,14 @@ create_target_directory_groups(shadps4) target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG) target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 SDL3_mixer::SDL3_mixer pugixml::pugixml) -target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib) +target_link_libraries(shadps4 PRIVATE stb::headers lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + target_link_libraries(shadps4 PRIVATE "/usr/lib/libusb.so") + target_link_libraries(shadps4 PRIVATE "/usr/local/lib/libuuid.so") +else() + target_link_libraries(shadps4 PRIVATE libusb::usb) +endif() target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") @@ -1185,6 +1192,8 @@ if (APPLE) # Replacement for std::chrono::time_zone target_link_libraries(shadps4 PRIVATE date::date-tz epoll-shim) +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + target_link_libraries(shadps4 PRIVATE date::date-tz epoll-shim) endif() if (WIN32) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 41a0f71c7..9e19e1404 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -210,8 +210,15 @@ endif() # libusb if (NOT TARGET libusb::usb) - add_subdirectory(ext-libusb) - add_library(libusb::usb ALIAS usb-1.0) + if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + # YOU MUST USE NATIVE LIBUSB + # using anything else will crash instantly, also freebsd will NOT like it + # no you cant vendor this libusb, its builtin on freebsd + find_package(libusb) + else() + add_subdirectory(ext-libusb) + add_library(libusb::usb ALIAS usb-1.0) + endif() endif() # Discord RPC @@ -233,25 +240,26 @@ endif() set(HWINFO_STATIC ON) add_subdirectory(hwinfo) -# Apple-only dependencies -if (APPLE) +if (APPLE OR ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") # date if (NOT TARGET date::date-tz) option(BUILD_TZ_LIB "" ON) option(USE_SYSTEM_TZ_DB "" ON) add_subdirectory(date) endif() + if (NOT TARGET epoll-shim) + add_subdirectory(epoll-shim) + endif() +endif() +# Apple-only dependencies +if (APPLE) # MoltenVK if (NOT TARGET MoltenVK) set(MVK_EXCLUDE_SPIRV_TOOLS ON) set(MVK_USE_METAL_PRIVATE_API ON) add_subdirectory(MoltenVK) endif() - - if (NOT TARGET epoll-shim) - add_subdirectory(epoll-shim) - endif() endif() #windows only diff --git a/src/common/adaptive_mutex.h b/src/common/adaptive_mutex.h index 2ab385bdb..247d4c1ec 100644 --- a/src/common/adaptive_mutex.h +++ b/src/common/adaptive_mutex.h @@ -3,7 +3,7 @@ #pragma once -#ifdef __linux__ +#if __unix__ #include #endif diff --git a/src/common/signal_context.cpp b/src/common/signal_context.cpp index 112160bc8..b1ac8d96f 100644 --- a/src/common/signal_context.cpp +++ b/src/common/signal_context.cpp @@ -7,6 +7,9 @@ #ifdef _WIN32 #include +#elif defined(__FreeBSD__) +#include +#include #else #include #endif @@ -22,6 +25,16 @@ void* GetXmmPointer(void* ctx, u8 index) { #define CASE(index) \ case index: \ return (void*)(&((ucontext_t*)ctx)->uc_mcontext->__fs.__fpu_xmm##index); +#elif defined(__FreeBSD__) + // In mc_fpstate + // See for the internals of mc_fpstate[]. +#define CASE(index) \ + case index: { \ + auto& mctx = ((ucontext_t*)ctx)->uc_mcontext; \ + ASSERT(mctx.mc_fpformat == _MC_FPFMT_XMM); \ + auto* s_fpu = (struct savefpu*)(&mctx.mc_fpstate[0]); \ + return (void*)(&(s_fpu->sv_xmm[0])); \ + } #else #define CASE(index) \ case index: \ @@ -57,6 +70,8 @@ void* GetRip(void* ctx) { return (void*)((EXCEPTION_POINTERS*)ctx)->ContextRecord->Rip; #elif defined(__APPLE__) return (void*)((ucontext_t*)ctx)->uc_mcontext->__ss.__rip; +#elif defined(__FreeBSD__) + return (void*)((ucontext_t*)ctx)->uc_mcontext.mc_rip; #else return (void*)((ucontext_t*)ctx)->uc_mcontext.gregs[REG_RIP]; #endif @@ -67,6 +82,8 @@ void IncrementRip(void* ctx, u64 length) { ((EXCEPTION_POINTERS*)ctx)->ContextRecord->Rip += length; #elif defined(__APPLE__) ((ucontext_t*)ctx)->uc_mcontext->__ss.__rip += length; +#elif defined(__FreeBSD__) + ((ucontext_t*)ctx)->uc_mcontext.mc_rip += length; #else ((ucontext_t*)ctx)->uc_mcontext.gregs[REG_RIP] += length; #endif @@ -75,18 +92,16 @@ void IncrementRip(void* ctx, u64 length) { bool IsWriteError(void* ctx) { #if defined(_WIN32) return ((EXCEPTION_POINTERS*)ctx)->ExceptionRecord->ExceptionInformation[0] == 1; -#elif defined(__APPLE__) -#if defined(ARCH_X86_64) +#elif defined(__APPLE__) && defined(ARCH_X86_64) return ((ucontext_t*)ctx)->uc_mcontext->__es.__err & 0x2; -#elif defined(ARCH_ARM64) +#elif defined(__APPLE__) && defined(ARCH_ARM64) return ((ucontext_t*)ctx)->uc_mcontext->__es.__esr & 0x40; -#endif -#else -#if defined(ARCH_X86_64) +#elif defined(__FreeBSD__) && defined(ARCH_X86_64) + return ((ucontext_t*)ctx)->uc_mcontext.mc_err & 0x2; +#elif defined(ARCH_X86_64) return ((ucontext_t*)ctx)->uc_mcontext.gregs[REG_ERR] & 0x2; #else #error "Unsupported architecture" #endif -#endif } -} // namespace Common \ No newline at end of file +} // namespace Common diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index ca3d52042..4830f65a5 100644 --- a/src/core/address_space.cpp +++ b/src/core/address_space.cpp @@ -613,7 +613,11 @@ struct AddressSpace::Impl { user_size = UserSize; constexpr int protection_flags = PROT_READ | PROT_WRITE; - constexpr int map_flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED; + int map_flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED; // compiler knows its constexpr +#if !defined(__FreeBSD__) + map_flags |= MAP_NORESERVE; +#endif + #if defined(__APPLE__) && defined(ARCH_X86_64) // On ARM64 Macs, we run into limitations due to the commpage from 0xFC0000000 - 0xFFFFFFFFF // and the GPU carveout region from 0x1000000000 - 0x6FFFFFFFFF. Because this creates gaps @@ -628,7 +632,7 @@ struct AddressSpace::Impl { mmap(reinterpret_cast(USER_MIN), user_size, protection_flags, map_flags, -1, 0)); #else const auto virtual_size = system_managed_size + system_reserved_size + user_size; -#if defined(ARCH_X86_64) +#if defined(ARCH_X86_64) && !defined(__FreeBSD__) const auto virtual_base = reinterpret_cast(mmap(reinterpret_cast(SYSTEM_MANAGED_MIN), virtual_size, protection_flags, map_flags, -1, 0)); @@ -636,8 +640,10 @@ struct AddressSpace::Impl { system_reserved_base = reinterpret_cast(SYSTEM_RESERVED_MIN); user_base = reinterpret_cast(USER_MIN); #else + // FreeBSD can't stand MAP_FIXED or it may overwrite mmap() itself! // Map memory wherever possible and instruction translation can handle offsetting to the // base. + map_flags &= ~MAP_FIXED; const auto virtual_base = reinterpret_cast(mmap(nullptr, virtual_size, protection_flags, map_flags, -1, 0)); system_managed_base = virtual_base; @@ -676,8 +682,13 @@ struct AddressSpace::Impl { } shm_unlink(shm_path.c_str()); #else +#ifndef __FreeBSD__ madvise(virtual_base, virtual_size, MADV_HUGEPAGE); - +#endif + // NOTE: If you add MFD_HUGETLB or whatever, remember that FBSD will break (libc bug) + // so please, do not, add MFD_* whatever unless you ifdef it away (must be 0 for FBSD) + // using sized pages as well causes incessant vm_reclaim calls in kernel, do not use on FBSD + // under any circumstances. backing_fd = memfd_create("BackingDmem", 0); if (backing_fd < 0) { LOG_CRITICAL(Kernel_Vmm, "memfd_create failed: {}", strerror(errno)); diff --git a/src/core/debugger.cpp b/src/core/debugger.cpp index 16071ee69..b396a3ba5 100644 --- a/src/core/debugger.cpp +++ b/src/core/debugger.cpp @@ -12,7 +12,7 @@ #elif defined(__linux__) #include #include -#elif defined(__APPLE__) +#elif defined(__APPLE__) || defined(__FreeBSD__) #include #include #include @@ -48,6 +48,8 @@ bool Core::Debugger::IsDebuggerAttached() { return (info.kp_proc.p_flag & P_TRACED) != 0; } return false; +#elif defined(__FreeBSD__) + return false; #else #error "Unsupported platform" #endif @@ -66,7 +68,7 @@ void Core::Debugger::WaitForDebuggerAttach() { int Core::Debugger::GetCurrentPid() { #if defined(_WIN32) return GetCurrentProcessId(); -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) return getpid(); #else #error "Unsupported platform" @@ -88,7 +90,7 @@ void Core::Debugger::WaitForPid(int pid) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::cerr << "Waiting for process " << pid << " to exit..." << std::endl; } -#elif defined(__APPLE__) +#elif defined(__APPLE__) || defined(__FreeBSD__) while (kill(pid, 0) == 0) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::cerr << "Waiting for process " << pid << " to exit..." << std::endl; diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index 2fdf8c10b..aa474d20a 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -95,7 +95,7 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea std::scoped_lock lk{m_mutex}; path_parts.clear(); auto current_path = host_path; - while (!std::filesystem::exists(current_path)) { + while (!current_path.empty() && !std::filesystem::exists(current_path)) { // We have probably cached this if it's a folder. if (auto it = path_cache.find(current_path); it != path_cache.end()) { current_path = it->second; @@ -104,38 +104,40 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea path_parts.emplace_back(current_path.filename()); current_path = current_path.parent_path(); } - // We have found an anchor. Traverse parts we recoded and see if they - // exist in filesystem but in different case. - auto guest_path = current_path; - while (!path_parts.empty()) { - const auto part = path_parts.back(); - const auto add_match = [&](const auto& host_part) { - current_path /= host_part; - guest_path /= part; - path_cache[guest_path] = current_path; - path_parts.pop_back(); - }; - // Can happen when the mismatch is in upper folder. - if (std::filesystem::exists(current_path / part)) { - add_match(part); - continue; - } - const auto part_low = Common::ToLower(part.string()); - bool found_match = false; - for (const auto& path : std::filesystem::directory_iterator(current_path)) { - const auto candidate = path.path().filename(); - const auto filename = Common::ToLower(candidate.string()); - // Check if a filename matches in case insensitive manner. - if (filename != part_low) { + if (!current_path.empty()) { + // We have found an anchor. Traverse parts we recoded and see if they + // exist in filesystem but in different case. + auto guest_path = current_path; + while (!path_parts.empty()) { + const auto part = path_parts.back(); + const auto add_match = [&](const auto& host_part) { + current_path /= host_part; + guest_path /= part; + path_cache[guest_path] = current_path; + path_parts.pop_back(); + }; + // Can happen when the mismatch is in upper folder. + if (std::filesystem::exists(current_path / part)) { + add_match(part); continue; } - // We found a match, record the actual path in the cache. - add_match(candidate); - found_match = true; - break; - } - if (!found_match) { - return std::optional({}); + const auto part_low = Common::ToLower(part.string()); + bool found_match = false; + for (const auto& path : std::filesystem::directory_iterator(current_path)) { + const auto candidate = path.path().filename(); + const auto filename = Common::ToLower(candidate.string()); + // Check if a filename matches in case insensitive manner. + if (filename != part_low) { + continue; + } + // We found a match, record the actual path in the cache. + add_match(candidate); + found_match = true; + break; + } + if (!found_match) { + return std::optional({}); + } } } return std::optional(current_path); diff --git a/src/core/libraries/kernel/threads/exception.cpp b/src/core/libraries/kernel/threads/exception.cpp index e2fd032f5..3d855af3d 100644 --- a/src/core/libraries/kernel/threads/exception.cpp +++ b/src/core/libraries/kernel/threads/exception.cpp @@ -215,6 +215,28 @@ void SigactionHandler(int native_signum, siginfo_t* inf, ucontext_t* raw_context ctx.uc_mcontext.mc_gs = regs.__gs; ctx.uc_mcontext.mc_rip = regs.__rip; ctx.uc_mcontext.mc_addr = reinterpret_cast(inf->si_addr); +#elif defined(__FreeBSD__) + const auto& regs = raw_context->uc_mcontext; + ctx.uc_mcontext.mc_r8 = regs.mc_r8; + ctx.uc_mcontext.mc_r9 = regs.mc_r9; + ctx.uc_mcontext.mc_r10 = regs.mc_r10; + ctx.uc_mcontext.mc_r11 = regs.mc_r11; + ctx.uc_mcontext.mc_r12 = regs.mc_r12; + ctx.uc_mcontext.mc_r13 = regs.mc_r13; + ctx.uc_mcontext.mc_r14 = regs.mc_r14; + ctx.uc_mcontext.mc_r15 = regs.mc_r15; + ctx.uc_mcontext.mc_rdi = regs.mc_rdi; + ctx.uc_mcontext.mc_rsi = regs.mc_rsi; + ctx.uc_mcontext.mc_rbp = regs.mc_rbp; + ctx.uc_mcontext.mc_rbx = regs.mc_rbx; + ctx.uc_mcontext.mc_rdx = regs.mc_rdx; + ctx.uc_mcontext.mc_rax = regs.mc_rax; + ctx.uc_mcontext.mc_rcx = regs.mc_rcx; + ctx.uc_mcontext.mc_rsp = regs.mc_rsp; + ctx.uc_mcontext.mc_fs = regs.mc_fs; + ctx.uc_mcontext.mc_gs = regs.mc_gs; + ctx.uc_mcontext.mc_rip = regs.mc_rip; + ctx.uc_mcontext.mc_addr = uint64_t(regs.mc_addr); #else const auto& regs = raw_context->uc_mcontext.gregs; ctx.uc_mcontext.mc_r8 = regs[REG_R8]; @@ -303,7 +325,7 @@ s32 PS4_SYSV_ABI posix_sigaction(s32 sig, Sigaction* act, Sigaction* oact) { *__Error() = POSIX_EINVAL; return ORBIS_FAIL; } -#ifndef __APPLE__ +#if !defined(__APPLE__) && !defined(__FreeBSD__) 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; diff --git a/src/core/libraries/kernel/threads/exception.h b/src/core/libraries/kernel/threads/exception.h index c07242c1d..f8cd06549 100644 --- a/src/core/libraries/kernel/threads/exception.h +++ b/src/core/libraries/kernel/threads/exception.h @@ -47,7 +47,7 @@ constexpr s32 POSIX_SIGUSR2 = 31; constexpr s32 POSIX_SIGTHR = 32; constexpr s32 POSIX_SIGLIBRT = 33; -#ifdef __linux__ +#if defined(__linux__) || defined(__FreeBSD__) constexpr s32 _SIGEMT = 128; constexpr s32 _SIGINFO = 129; #elif !defined(_WIN32) diff --git a/src/core/libraries/kernel/time.cpp b/src/core/libraries/kernel/time.cpp index 3e1648b98..2967dd4bf 100644 --- a/src/core/libraries/kernel/time.cpp +++ b/src/core/libraries/kernel/time.cpp @@ -17,7 +17,7 @@ #include #include "common/ntapi.h" #else -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__FreeBSD__) #include #endif #include @@ -501,7 +501,7 @@ s32 PS4_SYSV_ABI sceKernelConvertUtcToLocaltime(time_t time, time_t* local_time, *dst_sec = res == TIME_ZONE_ID_DAYLIGHT ? -_dstbias : 0; } #else -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__FreeBSD__) // std::chrono::current_zone() not available yet. const auto* time_zone = date::current_zone(); #else diff --git a/src/core/libraries/network/net.cpp b/src/core/libraries/network/net.cpp index 6bf4764c4..baf45b64f 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -663,10 +663,12 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or return ORBIS_NET_ERROR_EBADF; } +#ifndef __FreeBSD__ epoll_event native_event = {.events = ConvertEpollEventsIn(event->events), .data = {.fd = id}}; ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_ADD, *native_handle, &native_event) == 0); epoll->events.emplace_back(id, *event); +#endif break; } case Core::FileSys::FileType::Resolver: { @@ -711,10 +713,12 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or return ORBIS_NET_ERROR_EBADF; } +#ifndef __FreeBSD__ epoll_event native_event = {.events = ConvertEpollEventsIn(event->events), .data = {.fd = id}}; ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_MOD, *native_handle, &native_event) == 0); *it = {id, *event}; +#endif break; } default: @@ -752,9 +756,10 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or *sceNetErrnoLoc() = ORBIS_NET_EBADF; return ORBIS_NET_ERROR_EBADF; } - +#ifndef __FreeBSD__ ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_DEL, *native_handle, nullptr) == 0); epoll->events.erase(it); +#endif break; } case Core::FileSys::FileType::Resolver: { @@ -810,6 +815,9 @@ int PS4_SYSV_ABI sceNetEpollDestroy(OrbisNetId epollid) { int PS4_SYSV_ABI sceNetEpollWait(OrbisNetId epollid, OrbisNetEpollEvent* events, int maxevents, int timeout) { +#ifdef __FreeBSD__ + return 0; +#else auto file = FDTable::Instance()->GetEpoll(epollid); if (!file) { *sceNetErrnoLoc() = ORBIS_NET_EBADF; @@ -836,7 +844,6 @@ int PS4_SYSV_ABI sceNetEpollWait(OrbisNetId epollid, OrbisNetEpollEvent* events, } int i = 0; - if (result < 0) { LOG_ERROR(Lib_Net, "epoll_wait failed with {}", Common::GetLastErrorMsg()); switch (errno) { @@ -905,8 +912,8 @@ int PS4_SYSV_ABI sceNetEpollWait(OrbisNetId epollid, OrbisNetEpollEvent* events, ++i; } } - return i; +#endif } int* PS4_SYSV_ABI sceNetErrnoLoc() { diff --git a/src/core/libraries/network/net_epoll.cpp b/src/core/libraries/network/net_epoll.cpp index e64c8ac64..4f1b521ce 100644 --- a/src/core/libraries/network/net_epoll.cpp +++ b/src/core/libraries/network/net_epoll.cpp @@ -10,12 +10,14 @@ namespace Libraries::Net { u32 ConvertEpollEventsIn(u32 orbis_events) { u32 ret = 0; +#ifndef __FreeBSD__ if ((orbis_events & ORBIS_NET_EPOLLIN) != 0) { ret |= EPOLLIN; } if ((orbis_events & ORBIS_NET_EPOLLOUT) != 0) { ret |= EPOLLOUT; } +#endif return ret; } @@ -23,6 +25,7 @@ u32 ConvertEpollEventsIn(u32 orbis_events) { u32 ConvertEpollEventsOut(u32 epoll_events) { u32 ret = 0; +#ifndef __FreeBSD__ if ((epoll_events & EPOLLIN) != 0) { ret |= ORBIS_NET_EPOLLIN; } @@ -35,6 +38,7 @@ u32 ConvertEpollEventsOut(u32 epoll_events) { if ((epoll_events & EPOLLHUP) != 0) { ret |= ORBIS_NET_EPOLLHUP; } +#endif return ret; } diff --git a/src/core/libraries/network/net_epoll.h b/src/core/libraries/network/net_epoll.h index 37555484d..17716b36e 100644 --- a/src/core/libraries/network/net_epoll.h +++ b/src/core/libraries/network/net_epoll.h @@ -14,7 +14,8 @@ #include #endif -#if defined(__linux__) || defined(__APPLE__) +#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) +// ADD libepoll-shim if using freebsd! #include #include #endif @@ -82,4 +83,4 @@ private: std::mutex m_mutex; }; -} // namespace Libraries::Net \ No newline at end of file +} // namespace Libraries::Net diff --git a/src/core/libraries/network/net_util.cpp b/src/core/libraries/network/net_util.cpp index de47f7bc1..fd0ed877b 100644 --- a/src/core/libraries/network/net_util.cpp +++ b/src/core/libraries/network/net_util.cpp @@ -25,7 +25,7 @@ typedef int net_socket; #include #include #endif -#if __linux__ +#if defined(__linux__) || defined(__FreeBSD__) #include #include #include @@ -81,6 +81,8 @@ bool NetUtilInternal::RetrieveEthernetAddr() { } freeifaddrs(ifap); } +#elif defined(__FreeBSD__) + // todo #else ifreq ifr; ifconf ifc; @@ -226,7 +228,8 @@ bool NetUtilInternal::RetrieveDefaultGateway() { inet_ntop(AF_INET, gateAddr, str, sizeof(str)); this->default_gateway = str; return true; - +#elif defined(__FreeBSD__) + return true; #else std::ifstream route{"/proc/net/route"}; std::string line; @@ -398,4 +401,4 @@ int NetUtilInternal::ResolveHostname(const char* hostname, Libraries::Net::Orbis return ret; } -} // namespace NetUtil \ No newline at end of file +} // namespace NetUtil diff --git a/src/core/tls.cpp b/src/core/tls.cpp index 8b926cb39..f87248114 100644 --- a/src/core/tls.cpp +++ b/src/core/tls.cpp @@ -10,6 +10,8 @@ #ifdef _WIN32 #include +#elif defined(__FreeBSD__) +#include #elif defined(__APPLE__) && defined(ARCH_X86_64) #include #include @@ -157,12 +159,17 @@ Tcb* GetTcbBase() { #elif defined(ARCH_X86_64) -// Other POSIX x86_64 - +// Linux x86_64 +#if defined(__FreeBSD__) +void SetTcbBase(void* image_address) { + amd64_set_gsbase(image_address); +} +#else void SetTcbBase(void* image_address) { const int ret = syscall(SYS_arch_prctl, ARCH_SET_GS, (unsigned long)image_address); ASSERT_MSG(ret == 0, "Failed to set GS base: errno {}", errno); } +#endif Tcb* GetTcbBase() { return Libraries::Kernel::g_curthread->tcb; diff --git a/src/emulator.cpp b/src/emulator.cpp index a9496f5f0..616a21ed1 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -535,7 +535,7 @@ void Emulator::Restart(std::filesystem::path eboot_path, CloseHandle(pi.hProcess); CloseHandle(pi.hThread); -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) std::vector argv; // Emulator executable diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 766a336c2..060197533 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -96,7 +96,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controller 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()); + LOG_ERROR(Input, "Failed to initialize SDL camera subsystem: {}", SDL_GetError()); } SDL_InitSubSystem(SDL_INIT_AUDIO); @@ -141,7 +141,8 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controller window_info.type = WindowSystemType::Windows; window_info.render_surface = SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); -#elif defined(SDL_PLATFORM_LINUX) +#elif defined(SDL_PLATFORM_LINUX) || defined(__FreeBSD__) + // SDL doesn't have a platform define for FreeBSD AAAAAAAAAA if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) { window_info.type = WindowSystemType::X11; window_info.display_connection = SDL_GetPointerProperty( diff --git a/src/video_core/buffer_cache/region_manager.h b/src/video_core/buffer_cache/region_manager.h index a760dd596..742268753 100644 --- a/src/video_core/buffer_cache/region_manager.h +++ b/src/video_core/buffer_cache/region_manager.h @@ -7,7 +7,7 @@ #include "common/logging/log.h" #include "core/emulator_settings.h" -#ifdef __linux__ +#ifdef __unix__ #include "common/adaptive_mutex.h" #else #include "common/spin_lock.h" diff --git a/src/video_core/page_manager.cpp b/src/video_core/page_manager.cpp index 2bf16afe0..6a4bcbd7d 100644 --- a/src/video_core/page_manager.cpp +++ b/src/video_core/page_manager.cpp @@ -36,8 +36,8 @@ namespace VideoCore { -constexpr size_t PAGE_SIZE = 4_KB; -constexpr size_t PAGE_BITS = 12; +constexpr size_t PM_PAGE_SIZE = 4_KB; +constexpr size_t PM_PAGE_BITS = 12; struct PageManager::Impl { struct PageState { @@ -85,7 +85,7 @@ struct PageManager::Impl { }; static constexpr size_t ADDRESS_BITS = 40; - static constexpr size_t NUM_ADDRESS_PAGES = 1ULL << (40 - PAGE_BITS); + static constexpr size_t NUM_ADDRESS_PAGES = 1ULL << (40 - PM_PAGE_BITS); static constexpr size_t NUM_ADDRESS_LOCKS = NUM_ADDRESS_PAGES / PAGES_PER_LOCK; inline static Vulkan::Rasterizer* rasterizer; #ifdef ENABLE_USERFAULTFD @@ -222,8 +222,8 @@ struct PageManager::Impl { void UpdatePageWatchers(VAddr addr, u64 size) { RENDERER_TRACE; - size_t page = addr >> PAGE_BITS; - const u64 page_end = Common::DivCeil(addr + size, PAGE_SIZE); + size_t page = addr >> PM_PAGE_BITS; + const u64 page_end = Common::DivCeil(addr + size, PM_PAGE_SIZE); // Acquire locks for the range of pages const auto lock_start = locks.begin() + (page / PAGES_PER_LOCK); @@ -239,15 +239,15 @@ struct PageManager::Impl { if (range_bytes > 0) { RENDERER_TRACE; // Perform pending (un)protect action - Protect(range_begin << PAGE_BITS, range_bytes, perms); + Protect(range_begin << PM_PAGE_BITS, range_bytes, perms); range_bytes = 0; potential_range_bytes = 0; } }; // Iterate requested pages - const u64 aligned_addr = page << PAGE_BITS; - const u64 aligned_end = page_end << PAGE_BITS; + const u64 aligned_addr = page << PM_PAGE_BITS; + const u64 aligned_end = page_end << PM_PAGE_BITS; if (!rasterizer->IsMapped(aligned_addr, aligned_end - aligned_addr)) { LOG_WARNING(Render, "Tracking memory region {:#x} - {:#x} which is not fully GPU mapped.", @@ -266,7 +266,7 @@ struct PageManager::Impl { perms = new_perms; } else if (range_bytes != 0) { // If the protection did not change, extend the potential range - potential_range_bytes += PAGE_SIZE; + potential_range_bytes += PM_PAGE_SIZE; } // Only start a new range if the page must be (un)protected @@ -274,7 +274,7 @@ struct PageManager::Impl { if (range_bytes == 0) { // Start a new potential range range_begin = page; - potential_range_bytes = PAGE_SIZE; + potential_range_bytes = PM_PAGE_SIZE; } // Extend current range up to potential range range_bytes = potential_range_bytes; @@ -293,12 +293,12 @@ struct PageManager::Impl { if (start_range.second == end_range.second) { // if all pages are contiguous, use the regular UpdatePageWatchers - const VAddr start_addr = base_addr + (start_range.first << PAGE_BITS); - const u64 size = (start_range.second - start_range.first) << PAGE_BITS; + const VAddr start_addr = base_addr + (start_range.first << PM_PAGE_BITS); + const u64 size = (start_range.second - start_range.first) << PM_PAGE_BITS; return UpdatePageWatchers(start_addr, size); } - size_t base_page = (base_addr >> PAGE_BITS); + size_t base_page = (base_addr >> PM_PAGE_BITS); ASSERT(base_page % PAGES_PER_LOCK == 0); std::scoped_lock lk(locks[base_page / PAGES_PER_LOCK]); auto perms = cached_pages[base_page + start_range.first].Perms(); @@ -310,7 +310,7 @@ struct PageManager::Impl { if (range_bytes > 0) { RENDERER_TRACE; // Perform pending (un)protect action - Protect((range_begin << PAGE_BITS), range_bytes, perms); + Protect((range_begin << PM_PAGE_BITS), range_bytes, perms); range_bytes = 0; potential_range_bytes = 0; } @@ -331,7 +331,7 @@ struct PageManager::Impl { perms = new_perms; } else if (range_bytes != 0) { // If the protection did not change, extend the potential range - potential_range_bytes += PAGE_SIZE; + potential_range_bytes += PM_PAGE_SIZE; } // If the page is not being updated, skip it @@ -344,7 +344,7 @@ struct PageManager::Impl { if (range_bytes == 0) { // Start a new potential range range_begin = base_page + page; - potential_range_bytes = PAGE_SIZE; + potential_range_bytes = PM_PAGE_SIZE; } // Extend current rango up to potential range range_bytes = potential_range_bytes; @@ -356,7 +356,7 @@ struct PageManager::Impl { } std::array cached_pages{}; -#ifdef __linux__ +#ifdef PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP using LockType = Common::AdaptiveMutex; #else using LockType = Common::SpinLock; diff --git a/src/video_core/page_manager.h b/src/video_core/page_manager.h index 4ca41cb43..fb53f7c98 100644 --- a/src/video_core/page_manager.h +++ b/src/video_core/page_manager.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "common/alignment.h" #include "common/types.h" @@ -15,9 +16,10 @@ class Rasterizer; namespace VideoCore { class PageManager { + // PAGE_SIZE and PAGE_BITS conflicts with machine/param.h definitions on freebsd! // Use the same page size as the tracker. - static constexpr size_t PAGE_BITS = TRACKER_PAGE_BITS; - static constexpr size_t PAGE_SIZE = TRACKER_BYTES_PER_PAGE; + static constexpr size_t PM_PAGE_BITS = TRACKER_PAGE_BITS; + static constexpr size_t PM_PAGE_SIZE = TRACKER_BYTES_PER_PAGE; // Keep the lock granularity the same as region granularity. (since each regions has // itself a lock) @@ -43,12 +45,12 @@ public: /// Returns page aligned address. static constexpr VAddr GetPageAddr(VAddr addr) { - return Common::AlignDown(addr, PAGE_SIZE); + return Common::AlignDown(addr, PM_PAGE_SIZE); } /// Returns address of the next page. static constexpr VAddr GetNextPageAddr(VAddr addr) { - return Common::AlignUp(addr + 1, PAGE_SIZE); + return Common::AlignUp(addr + 1, PM_PAGE_SIZE); } private: From 969955b8a086b79dc107eabbb663ed9cf1308e60 Mon Sep 17 00:00:00 2001 From: Connor Garey Date: Mon, 30 Mar 2026 21:35:57 +0100 Subject: [PATCH 09/27] Addition of Nix flake development shell (#4184) * Initial devshell creation. * cmake has a check for clang and therefore override the stdenv. * Packages from old shell were renamed. * fixed xcb-util, added libglvnd * Added sdl3 dependencies provided by the website given on cmake configuration. * Lock file. * Nix format. * Added instructions for entering nix development shell. . * Added libuuid * Added copyright text to flake.nix * Added flake.lock to REUSE.toml as is a JSON file without comment support. * Updated instructions to refer to new build name. * Compiling however not yet correctly linking with debug derivation. * Hitting installPhase * Added nix result symlink. * correctly installs in place * Added a wrapper to load vulkan and ligl into environment. * Ensure that the name is applicable to the current project. . * Added mesa to LD_LIBRARY_PATH * game now launching with added X11 libraries. * Cleanup Formatting. Pulled cmakeFlags to top and added releaseWithDebugInfo Removed LD_LIBRARY_PATH from devshell. . * Added options for the different Nix build modes. * Debug / release mode flag cannot be bundled into one. --- .gitignore | 3 + REUSE.toml | 3 +- documents/building-linux.md | 17 ++++ flake.lock | 27 ++++++ flake.nix | 160 ++++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore index 683f6f0a6..7ace5e4f6 100644 --- a/.gitignore +++ b/.gitignore @@ -418,3 +418,6 @@ FodyWeavers.xsd # JetBrains .idea cmake-build-* + +# Nix Result symlink +result diff --git a/REUSE.toml b/REUSE.toml index 22bed2a50..e8997f007 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -22,6 +22,7 @@ path = [ "documents/Screenshots/Linux/*", "documents/Screenshots/Windows/*", "externals/MoltenVK/MoltenVK_icd.json", + "flake.lock", "scripts/ps4_names.txt", "src/images/bronze.png", "src/images/gold.png", @@ -130,4 +131,4 @@ SPDX-License-Identifier = "MIT" [[annotations]] path = "src/video_core/host_shaders/fsr/*" SPDX-FileCopyrightText = "Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved." -SPDX-License-Identifier = "MIT" \ No newline at end of file +SPDX-License-Identifier = "MIT" diff --git a/documents/building-linux.md b/documents/building-linux.md index 49aacdfc9..9495a226b 100644 --- a/documents/building-linux.md +++ b/documents/building-linux.md @@ -53,6 +53,23 @@ sudo zypper install clang git cmake libasound2 libpulse-devel \ nix-shell shell.nix ``` +#### Nix Flake Development Shell +```bash +nix develop +cmake -S . -B build/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +ln -s ./build/compile_commands.json . +``` + +#### Nix Flake Build +```bash +nix build .?submodules=1#linux.debug +``` +```bash +nix build .?submodules=1#linux.release +``` +```bash +nix build .?submodules=1#linux.releaseWithDebugInfo +``` #### Other Linux distributions You can try one of two methods: diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..246cfd4e7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1774386573, + "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..9042105c5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,160 @@ +## SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +## SPDX-License-Identifier: GPL-2.0-or-later + +{ + description = "shadPS4 Nix Flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + pkgsLinux = nixpkgs.legacyPackages.x86_64-linux; + in + { + devShells.x86_64-linux.default = pkgsLinux.mkShell.override { stdenv = pkgsLinux.clangStdenv; } { + packages = with pkgsLinux; [ + clang-tools + cmake + pkg-config + vulkan-tools + + renderdoc + gef + strace + + openal + zlib.dev + libedit.dev + vulkan-headers + vulkan-utility-libraries + ffmpeg.dev + fmt.dev + glslang.dev + wayland.dev + stb + libpng.dev + libuuid + + sdl3.dev + alsa-lib + hidapi + ibus.dev + jack2.dev + libdecor.dev + libthai.dev + fribidi.dev + libxcb.dev + libGL.dev + libpulseaudio.dev + libusb1.dev + libx11.dev + libxcursor.dev + libxext + libxfixes.dev + libxi.dev + libxinerama.dev + libxkbcommon + libxrandr.dev + libxrender.dev + libxtst + pipewire.dev + libxscrnsaver + sndio + ]; + shellHook = '' + echo "Entering shadPS4 development shell!" + ''; + }; + + linux = + let + execName = "shadps4"; + nativeInputs = with pkgsLinux; [ + cmake + ninja + pkg-config + magic-enum + fmt + eudev + ]; + buildInputs = with pkgsLinux; [ + boost + cli11 + openal + nlohmann_json + vulkan-loader + vulkan-headers + vulkan-memory-allocator + toml11 + zlib + zydis + pugixml + ffmpeg + libpulseaudio + pipewire + vulkan-loader + wayland + wayland-scanner + libX11 + libxrandr + libxext + libxcursor + libxi + libxscrnsaver + libxtst + libxcb + libdecor + libxkbcommon + libGL + libuuid + ]; + + defaultFlags = [ + "-DCMAKE_INSTALL_PREFIX=$out" + ]; + in + { + debug = pkgsLinux.stdenv.mkDerivation { + pname = "${execName}"; + version = "git"; + system = "x86_64-linux"; + src = ./.; + dontStrip = true; + + nativeBuildInputs = nativeInputs; + buildInputs = buildInputs; + cmakeFlags = [ + "-DCMAKE_BUILD_TYPE=Debug" + ] ++ [defaultFlags]; + }; + release = pkgsLinux.stdenv.mkDerivation { + pname = "${execName}"; + version = "git"; + system = "x86_64-linux"; + src = ./.; + + nativeBuildInputs = nativeInputs; + buildInputs = buildInputs; + cmakeFlags = [ + "-DCMAKE_BUILD_TYPE=Release" + ] ++ [defaultFlags]; + }; + releaseWithDebugInfo = pkgsLinux.stdenv.mkDerivation { + pname = "${execName}"; + version = "git"; + system = "x86_64-linux"; + src = ./.; + dontStrip = true; + + nativeBuildInputs = nativeInputs; + buildInputs = buildInputs; + cmakeFlags = [ + "-DCMAKE_BUILD_TYPE=Release" + ] ++ [defaultFlags]; + }; + }; + }; +} From 5d489ff03da937300dd4610c3a74ca18136d9d33 Mon Sep 17 00:00:00 2001 From: Ploo <239304139+xinitrcn1@users.noreply.github.com> Date: Wed, 1 Apr 2026 05:58:54 +0000 Subject: [PATCH 10/27] posix: implement sysconf() and sigalstack() (#4201) * posix: implement sysconf() and sigalstack() Signed-off-by: lizzie * mark as stubbed * oh clang format --------- Signed-off-by: lizzie Co-authored-by: lizzie --- src/core/libraries/kernel/kernel.cpp | 124 ++++++++++++++++++ src/core/libraries/kernel/kernel.h | 122 +++++++++++++++++ .../libraries/kernel/threads/exception.cpp | 25 ++++ src/core/libraries/kernel/threads/exception.h | 5 + 4 files changed, 276 insertions(+) diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index 6594bfab2..4d74e731b 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -298,6 +298,126 @@ s32 PS4_SYSV_ABI sceKernelGetAppInfo(s32 pid, OrbisKernelAppInfo* app_info) { return ORBIS_OK; } +// Nominally: long sysconf(int name); +u64 PS4_SYSV_ABI posix_sysconf(s32 name) { + switch (name) { + case 0: + return 0x20000; + case POSIX_SC_ARG_MAX: + return 0x588bc000; + case POSIX_SC_CHILD_MAX: + return 0x64; + case POSIX_SC_CLK_TCK: + return 0x20; + case POSIX_SC_NGROUPS_MAX: + return 0x644; + case POSIX_SC_OPEN_MAX: + return -0x1; + case POSIX_SC_JOB_CONTROL: + return 0x6; + case POSIX_SC_SAVED_IDS: + return 0x1; + case POSIX_SC_VERSION: + return 0x1; + case POSIX_SC_BC_BASE_MAX: + return 0x31069; + case POSIX_SC_BC_DIM_MAX: + return -0x1; + case POSIX_SC_BC_SCALE_MAX: + return 0x31069; + case POSIX_SC_BC_STRING_MAX: + return 0x31069; + case POSIX_SC_COLL_WEIGHTS_MAX: + return -0x1; + case POSIX_SC_EXPR_NEST_MAX: + return -0x1; + case POSIX_SC_LINE_MAX: + return 0x31069; + case POSIX_SC_RE_DUP_MAX: + return 0x31069; + case POSIX_SC_2_VERSION: + return 0x31069; + case POSIX_SC_2_C_BIND: + return 0x31069; + case POSIX_SC_2_C_DEV: + return 0x31069; + case POSIX_SC_2_CHAR_TERM: + return 0x31069; + case POSIX_SC_2_FORT_DEV: + return 0x31069; + case POSIX_SC_2_FORT_RUN: + return 0x31069; + case POSIX_SC_2_LOCALEDEF: + return -0x1; + case POSIX_SC_2_SW_DEV: + return -0x1; + case POSIX_SC_2_UPE: + return 0x0; + case POSIX_SC_STREAM_MAX: + return 0x7fffffff; + case POSIX_SC_TZNAME_MAX: + return -0x1; + case POSIX_SC_ASYNCHRONOUS_IO: + return 0x8000; + case POSIX_SC_MAPPED_FILES: + return 0x31069; + case POSIX_SC_MEMLOCK: + return 0x4000; + case POSIX_SC_MEMLOCK_RANGE: + return 0x1e; + case POSIX_SC_MEMORY_PROTECTION: + return 0x100; + case POSIX_SC_MESSAGE_PASSING: + return 0x7fffffff; + case POSIX_SC_PRIORITIZED_IO: + return -0x1; + case POSIX_SC_PRIORITY_SCHEDULING: + return -0x1; + case POSIX_SC_REALTIME_SIGNALS: + return 0x63; + case POSIX_SC_SEMAPHORES: + return 0x800; + case POSIX_SC_FSYNC: + return 0x63; + case POSIX_SC_SHARED_MEMORY_OBJECTS: + return 0x3e8; + case POSIX_SC_SYNCHRONIZED_IO: + return 0x2; + case POSIX_SC_THREAD_ATTR_STACKSIZE: + return 0x1; + case POSIX_SC_THREAD_CPUTIME: + return 0x1; + case POSIX_SC_THREAD_DESTRUCTOR_ITERATIONS: + return 0x48000; + case POSIX_SC_THREAD_KEYS_MAX: + return 0x1a078630b2dd7; + case POSIX_SC_THREAD_PRIO_INHERIT: + return -0x1; + case POSIX_SC_THREAD_PRIO_PROTECT: + return -0x1; + case POSIX_SC_THREAD_PRIORITY_SCHEDULING: + return 0x2bc; + case POSIX_SC_THREAD_PROCESS_SHARED: + return 0x2bc; + case POSIX_SC_THREAD_SAFE_FUNCTIONS: + return 0x1; + case POSIX_SC_THREAD_SPORADIC_SERVER: + return -0x1; + case POSIX_SC_THREAD_STACK_MIN: + return 0x1; + case POSIX_SC_THREAD_THREADS_MAX: + return 0x1; + case POSIX_SC_TIMEOUTS: + return -0x1; + // Manually specified + case POSIX_SC_PAGESIZE: + return posix_getpagesize(); + default: + LOG_ERROR(Lib_Kernel, "unhandled {}", name); + return 0; + } +} + void RegisterLib(Core::Loader::SymbolsResolver* sym) { service_thread = std::jthread{KernelServiceThread}; @@ -327,6 +447,10 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("9BcDykPmo1I", "libkernel", 1, "libkernel", __Error); LIB_FUNCTION("k+AXqu2-eBc", "libkernel", 1, "libkernel", posix_getpagesize); LIB_FUNCTION("k+AXqu2-eBc", "libScePosix", 1, "libkernel", posix_getpagesize); + + LIB_FUNCTION("mkawd0NA9ts", "libkernel", 1, "libkernel", posix_sysconf); + LIB_FUNCTION("mkawd0NA9ts", "libScePosix", 1, "libkernel", posix_sysconf); + LIB_FUNCTION("NWtTN10cJzE", "libSceLibcInternalExt", 1, "libSceLibcInternal", sceLibcHeapGetTraceInfo); diff --git a/src/core/libraries/kernel/kernel.h b/src/core/libraries/kernel/kernel.h index 315af6b06..ab5729f3b 100644 --- a/src/core/libraries/kernel/kernel.h +++ b/src/core/libraries/kernel/kernel.h @@ -80,4 +80,126 @@ struct OrbisKernelAppInfo { void RegisterLib(Core::Loader::SymbolsResolver* sym); +constexpr u32 POSIX_SC_ARG_MAX = 1; +constexpr u32 POSIX_SC_CHILD_MAX = 2; +constexpr u32 POSIX_SC_CLK_TCK = 3; +constexpr u32 POSIX_SC_NGROUPS_MAX = 4; +constexpr u32 POSIX_SC_OPEN_MAX = 5; +constexpr u32 POSIX_SC_JOB_CONTROL = 6; +constexpr u32 POSIX_SC_SAVED_IDS = 7; +constexpr u32 POSIX_SC_VERSION = 8; +constexpr u32 POSIX_SC_BC_BASE_MAX = 9; +constexpr u32 POSIX_SC_BC_DIM_MAX = 10; +constexpr u32 POSIX_SC_BC_SCALE_MAX = 11; +constexpr u32 POSIX_SC_BC_STRING_MAX = 12; +constexpr u32 POSIX_SC_COLL_WEIGHTS_MAX = 13; +constexpr u32 POSIX_SC_EXPR_NEST_MAX = 14; +constexpr u32 POSIX_SC_LINE_MAX = 15; +constexpr u32 POSIX_SC_RE_DUP_MAX = 16; +constexpr u32 POSIX_SC_2_VERSION = 17; +constexpr u32 POSIX_SC_2_C_BIND = 18; +constexpr u32 POSIX_SC_2_C_DEV = 19; +constexpr u32 POSIX_SC_2_CHAR_TERM = 20; +constexpr u32 POSIX_SC_2_FORT_DEV = 21; +constexpr u32 POSIX_SC_2_FORT_RUN = 22; +constexpr u32 POSIX_SC_2_LOCALEDEF = 23; +constexpr u32 POSIX_SC_2_SW_DEV = 24; +constexpr u32 POSIX_SC_2_UPE = 25; +constexpr u32 POSIX_SC_STREAM_MAX = 26; +constexpr u32 POSIX_SC_TZNAME_MAX = 27; +constexpr u32 POSIX_SC_ASYNCHRONOUS_IO = 28; +constexpr u32 POSIX_SC_MAPPED_FILES = 29; +constexpr u32 POSIX_SC_MEMLOCK = 30; +constexpr u32 POSIX_SC_MEMLOCK_RANGE = 31; +constexpr u32 POSIX_SC_MEMORY_PROTECTION = 32; +constexpr u32 POSIX_SC_MESSAGE_PASSING = 33; +constexpr u32 POSIX_SC_PRIORITIZED_IO = 34; +constexpr u32 POSIX_SC_PRIORITY_SCHEDULING = 35; +constexpr u32 POSIX_SC_REALTIME_SIGNALS = 36; +constexpr u32 POSIX_SC_SEMAPHORES = 37; +constexpr u32 POSIX_SC_FSYNC = 38; +constexpr u32 POSIX_SC_SHARED_MEMORY_OBJECTS = 39; +constexpr u32 POSIX_SC_SYNCHRONIZED_IO = 40; +constexpr u32 POSIX_SC_TIMERS = 41; +constexpr u32 POSIX_SC_AIO_LISTIO_MAX = 42; +constexpr u32 POSIX_SC_AIO_MAX = 43; +constexpr u32 POSIX_SC_AIO_PRIO_DELTA_MAX = 44; +constexpr u32 POSIX_SC_DELAYTIMER_MAX = 45; +constexpr u32 POSIX_SC_MQ_OPEN_MAX = 46; +constexpr u32 POSIX_SC_PAGESIZE = 47; +constexpr u32 POSIX_SC_RTSIG_MAX = 48; +constexpr u32 POSIX_SC_SEM_NSEMS_MAX = 49; +constexpr u32 POSIX_SC_SEM_VALUE_MAX = 50; +constexpr u32 POSIX_SC_SIGQUEUE_MAX = 51; +constexpr u32 POSIX_SC_TIMER_MAX = 52; +constexpr u32 POSIX_SC_2_PBS = 59; +constexpr u32 POSIX_SC_2_PBS_ACCOUNTING = 60; +constexpr u32 POSIX_SC_2_PBS_CHECKPOINT = 61; +constexpr u32 POSIX_SC_2_PBS_LOCATE = 62; +constexpr u32 POSIX_SC_2_PBS_MESSAGE = 63; +constexpr u32 POSIX_SC_2_PBS_TRACK = 64; +constexpr u32 POSIX_SC_ADVISORY_INFO = 65; +constexpr u32 POSIX_SC_BARRIERS = 66; +constexpr u32 POSIX_SC_CLOCK_SELECTION = 67; +constexpr u32 POSIX_SC_CPUTIME = 68; +constexpr u32 POSIX_SC_FILE_LOCKING = 69; +constexpr u32 POSIX_SC_GETGR_R_SIZE_MAX = 70; +constexpr u32 POSIX_SC_GETPW_R_SIZE_MAX = 71; +constexpr u32 POSIX_SC_HOST_NAME_MAX = 72; +constexpr u32 POSIX_SC_LOGIN_NAME_MAX = 73; +constexpr u32 POSIX_SC_MONOTONIC_CLOCK = 74; +constexpr u32 POSIX_SC_MQ_PRIO_MAX = 75; +constexpr u32 POSIX_SC_READER_WRITER_LOCKS = 76; +constexpr u32 POSIX_SC_REGEXP = 77; +constexpr u32 POSIX_SC_SHELL = 78; +constexpr u32 POSIX_SC_SPAWN = 79; +constexpr u32 POSIX_SC_SPIN_LOCKS = 80; +constexpr u32 POSIX_SC_SPORADIC_SERVER = 81; +constexpr u32 POSIX_SC_THREAD_ATTR_STACKADDR = 82; +constexpr u32 POSIX_SC_THREAD_ATTR_STACKSIZE = 83; +constexpr u32 POSIX_SC_THREAD_CPUTIME = 84; +constexpr u32 POSIX_SC_THREAD_DESTRUCTOR_ITERATIONS = 85; +constexpr u32 POSIX_SC_THREAD_KEYS_MAX = 86; +constexpr u32 POSIX_SC_THREAD_PRIO_INHERIT = 87; +constexpr u32 POSIX_SC_THREAD_PRIO_PROTECT = 88; +constexpr u32 POSIX_SC_THREAD_PRIORITY_SCHEDULING = 89; +constexpr u32 POSIX_SC_THREAD_PROCESS_SHARED = 90; +constexpr u32 POSIX_SC_THREAD_SAFE_FUNCTIONS = 91; +constexpr u32 POSIX_SC_THREAD_SPORADIC_SERVER = 92; +constexpr u32 POSIX_SC_THREAD_STACK_MIN = 93; +constexpr u32 POSIX_SC_THREAD_THREADS_MAX = 94; +constexpr u32 POSIX_SC_TIMEOUTS = 95; +constexpr u32 POSIX_SC_THREADS = 96; +constexpr u32 POSIX_SC_TRACE = 97; +constexpr u32 POSIX_SC_TRACE_EVENT_FILTER = 98; +constexpr u32 POSIX_SC_TRACE_INHERIT = 99; +constexpr u32 POSIX_SC_TRACE_LOG = 100; +constexpr u32 POSIX_SC_TTY_NAME_MAX = 101; +constexpr u32 POSIX_SC_TYPED_MEMORY_OBJECTS = 102; +constexpr u32 POSIX_SC_V6_ILP32_OFF32 = 103; +constexpr u32 POSIX_SC_V6_ILP32_OFFBIG = 104; +constexpr u32 POSIX_SC_V6_LP64_OFF64 = 105; +constexpr u32 POSIX_SC_V6_LPBIG_OFFBIG = 106; +constexpr u32 POSIX_SC_IPV6 = 118; +constexpr u32 POSIX_SC_RAW_SOCKETS = 119; +constexpr u32 POSIX_SC_SYMLOOP_MAX = 120; +constexpr u32 POSIX_SC_ATEXIT_MAX = 107; +constexpr u32 POSIX_SC_IOV_MAX = 56; +constexpr u32 POSIX_SC_XOPEN_CRYPT = 108; +constexpr u32 POSIX_SC_XOPEN_ENH_I18N = 109; +constexpr u32 POSIX_SC_XOPEN_LEGACY = 110; +constexpr u32 POSIX_SC_XOPEN_REALTIME = 111; +constexpr u32 POSIX_SC_XOPEN_REALTIME_THREADS = 112; +constexpr u32 POSIX_SC_XOPEN_SHM = 113; +constexpr u32 POSIX_SC_XOPEN_STREAMS = 114; +constexpr u32 POSIX_SC_XOPEN_UNIX = 115; +constexpr u32 POSIX_SC_XOPEN_VERSION = 116; +constexpr u32 POSIX_SC_XOPEN_XCU_VERSION = 117; +constexpr u32 POSIX_SC_NPROCESSORS_CONF = 57; +constexpr u32 POSIX_SC_NPROCESSORS_ONLN = 58; +constexpr u32 POSIX_SC_CPUSET_SIZE = 122; +constexpr u32 POSIX_SC_UEXTERR_MAXLEN = 123; +constexpr u32 POSIX_SC_NSIG = 124; +constexpr u32 POSIX_SC_PHYS_PAGES = 121; + } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/threads/exception.cpp b/src/core/libraries/kernel/threads/exception.cpp index 3d855af3d..f9a062da7 100644 --- a/src/core/libraries/kernel/threads/exception.cpp +++ b/src/core/libraries/kernel/threads/exception.cpp @@ -308,6 +308,28 @@ bool PS4_SYSV_ABI posix_sigisemptyset(Sigset* s) { return s->bits[0] == 0 && s->bits[1] == 0; } +s32 PS4_SYSV_ABI posix_sigalstack(const OrbisKernelExceptionHandlerStack* ss, + OrbisKernelExceptionHandlerStack* old_ss) { +#ifdef __unix__ + stack_t native_ss{}; + if (ss) { + native_ss.ss_sp = ss->ss_sp; + native_ss.ss_flags = ss->ss_flags; + native_ss.ss_size = ss->ss_size; + } + stack_t native_old_ss{}; + sigaltstack(&native_ss, &native_old_ss); + if (old_ss) { + old_ss->ss_sp = native_old_ss.ss_sp; + old_ss->ss_flags = native_old_ss.ss_flags; + old_ss->ss_size = native_old_ss.ss_size; + } +#else + LOG_ERROR(Lib_Kernel, "(stubbed)"); +#endif + return ORBIS_OK; +} + 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) { @@ -495,9 +517,12 @@ void RegisterException(Core::Loader::SymbolsResolver* sym) { 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("sHziAegVp74", "libkernel", 1, "libkernel", posix_sigalstack); + 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); + LIB_FUNCTION("sHziAegVp74", "libScePosix", 1, "libkernel", posix_sigalstack); } } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/threads/exception.h b/src/core/libraries/kernel/threads/exception.h index f8cd06549..f9655404a 100644 --- a/src/core/libraries/kernel/threads/exception.h +++ b/src/core/libraries/kernel/threads/exception.h @@ -12,6 +12,11 @@ class SymbolsResolver; namespace Libraries::Kernel { using OrbisKernelExceptionHandler = PS4_SYSV_ABI void (*)(int, void*); +struct OrbisKernelExceptionHandlerStack { + void* ss_sp; + int ss_flags; + size_t ss_size; +}; constexpr s32 POSIX_SIGHUP = 1; constexpr s32 POSIX_SIGINT = 2; From 5945c8719be3a89b7bf888f559b8b769ba51d411 Mon Sep 17 00:00:00 2001 From: Kravickas Date: Wed, 1 Apr 2026 11:55:06 +0200 Subject: [PATCH 11/27] Implement BUFFER_ATOMIC_FCMPSWAP (#4200) Implement BUFFER_ATOMIC_FCMPSWAP via descriptor aliasing + bitcast --- .../backend/spirv/emit_spirv_atomic.cpp | 9 +++++++++ .../backend/spirv/emit_spirv_instructions.h | 2 ++ .../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 + 8 files changed, 28 insertions(+) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp index 1055bf081..6155bdb29 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp @@ -351,6 +351,15 @@ Id EmitBufferAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id ad &Sirit::Module::OpAtomicCompareExchange); } +Id EmitBufferAtomicFCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id cmp_value) { + const auto u32_value = ctx.OpBitcast(ctx.U32[1], value); + const auto u32_cmp = ctx.OpBitcast(ctx.U32[1], cmp_value); + const auto result = BufferAtomicU32CmpSwap(ctx, inst, handle, address, u32_value, u32_cmp, + &Sirit::Module::OpAtomicCompareExchange); + return ctx.OpBitcast(ctx.F32[1], result); +} + Id EmitImageAtomicIAdd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value) { return ImageAtomicU32(ctx, inst, handle, coords, value, &Sirit::Module::OpAtomicIAdd); } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 69fa36eaa..da84e253c 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -108,6 +108,8 @@ Id EmitBufferAtomicXor32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addres Id EmitBufferAtomicSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, Id cmp_value); +Id EmitBufferAtomicFCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id cmp_value); Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, u32 index); Id EmitGetAttributeU32(EmitContext& ctx, IR::Attribute attr, u32 comp); void EmitSetAttribute(EmitContext& ctx, IR::Attribute attr, Id value, u32 comp); diff --git a/src/shader_recompiler/frontend/translate/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index 0d9e8f220..85ce2311a 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -116,6 +116,8 @@ void Translator::EmitVectorMemory(const GcnInst& inst) { return BUFFER_ATOMIC(AtomicOp::Fmin, inst); case Opcode::BUFFER_ATOMIC_FMAX: return BUFFER_ATOMIC(AtomicOp::Fmax, inst); + case Opcode::BUFFER_ATOMIC_FCMPSWAP: + return BUFFER_ATOMIC(AtomicOp::FCmpSwap, inst); // MIMG // Image load operations @@ -379,6 +381,10 @@ void Translator::BUFFER_ATOMIC(AtomicOp op, const GcnInst& inst) { const IR::Value cmp_val = ir.GetVectorReg(vdata + 1); return ir.BufferAtomicCmpSwap(handle, address, vdata_val, cmp_val, buffer_info); } + case AtomicOp::FCmpSwap: { + const IR::Value cmp_val = ir.GetVectorReg(vdata + 1); + return ir.BufferAtomicFCmpSwap(handle, address, vdata_val, cmp_val, buffer_info); + } case AtomicOp::Add: return ir.BufferAtomicIAdd(handle, address, vdata_val, buffer_info); case AtomicOp::Smin: diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index c681c3120..922f01b1f 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -627,6 +627,11 @@ Value IREmitter::BufferAtomicCmpSwap(const Value& handle, const Value& address, return Inst(Opcode::BufferAtomicCmpSwap32, Flags{info}, handle, address, vdata, cmp_value); } +Value IREmitter::BufferAtomicFCmpSwap(const Value& handle, const Value& address, const Value& vdata, + const Value& cmp_value, BufferInstInfo info) { + return Inst(Opcode::BufferAtomicFCmpSwap32, Flags{info}, handle, address, vdata, cmp_value); +} + U32 IREmitter::DataAppend(const U32& counter) { return Inst(Opcode::DataAppend, counter, Imm32(0)); } diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index adc8f5fb1..ec0edfed4 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -166,6 +166,9 @@ public: [[nodiscard]] Value BufferAtomicCmpSwap(const Value& handle, const Value& address, const Value& value, const Value& cmp_value, BufferInstInfo info); + [[nodiscard]] Value BufferAtomicFCmpSwap(const Value& handle, const Value& address, + const Value& value, const Value& cmp_value, + BufferInstInfo info); [[nodiscard]] U32 DataAppend(const U32& counter); [[nodiscard]] U32 DataConsume(const U32& counter); diff --git a/src/shader_recompiler/ir/microinstruction.cpp b/src/shader_recompiler/ir/microinstruction.cpp index cd0131770..837b9601e 100644 --- a/src/shader_recompiler/ir/microinstruction.cpp +++ b/src/shader_recompiler/ir/microinstruction.cpp @@ -82,6 +82,7 @@ bool Inst::MayHaveSideEffects() const noexcept { case Opcode::BufferAtomicXor32: case Opcode::BufferAtomicSwap32: case Opcode::BufferAtomicCmpSwap32: + case Opcode::BufferAtomicFCmpSwap32: case Opcode::DataAppend: case Opcode::DataConsume: case Opcode::WriteSharedU16: diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index 6304a96fa..ebbc702b7 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -150,6 +150,7 @@ OPCODE(BufferAtomicOr32, U32, Opaq OPCODE(BufferAtomicXor32, U32, Opaque, Opaque, U32, ) OPCODE(BufferAtomicSwap32, U32, Opaque, Opaque, U32, ) OPCODE(BufferAtomicCmpSwap32, U32, Opaque, Opaque, U32, U32, ) +OPCODE(BufferAtomicFCmpSwap32, F32, Opaque, Opaque, F32, F32, ) // Vector utility OPCODE(CompositeConstructU32x2, U32x2, U32, U32, ) diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 3b7888ab3..0b256a349 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -39,6 +39,7 @@ bool IsBufferAtomic(const IR::Inst& inst) { case IR::Opcode::BufferAtomicXor32: case IR::Opcode::BufferAtomicSwap32: case IR::Opcode::BufferAtomicCmpSwap32: + case IR::Opcode::BufferAtomicFCmpSwap32: return true; default: return false; From deb8c66ffb01f9d279e21e49371b791d7ba40600 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:12:04 -0500 Subject: [PATCH 12/27] Cleaner fix (#4167) --- src/core/libraries/videodec/videodec2.cpp | 43 ++++++--------- src/core/libraries/videodec/videodec2_avc.h | 53 ------------------- .../libraries/videodec/videodec2_impl.cpp | 50 ++++++----------- src/core/libraries/videodec/videodec2_impl.h | 1 - 4 files changed, 30 insertions(+), 117 deletions(-) diff --git a/src/core/libraries/videodec/videodec2.cpp b/src/core/libraries/videodec/videodec2.cpp index 121818f39..07055966f 100644 --- a/src/core/libraries/videodec/videodec2.cpp +++ b/src/core/libraries/videodec/videodec2.cpp @@ -203,36 +203,23 @@ s32 PS4_SYSV_ABI sceVideodec2GetPictureInfo(const OrbisVideodec2OutputInfo* outp LOG_ERROR(Lib_Vdec2, "No picture info available"); return ORBIS_OK; } + if (gPictureInfos.empty()) { + LOG_ERROR(Lib_Vdec2, "No picture info available"); + return ORBIS_OK; + } - // If the game uses the older Videodec2 structs, we need to accomodate that. - if (outputInfo->thisSize != sizeof(OrbisVideodec2OutputInfo)) { - if (gLegacyPictureInfos.empty()) { - LOG_ERROR(Lib_Vdec2, "No picture info available"); - return ORBIS_OK; - } - if (p1stPictureInfoOut) { - OrbisVideodec2LegacyAvcPictureInfo* picInfo = - static_cast(p1stPictureInfoOut); - if (picInfo->thisSize != sizeof(OrbisVideodec2LegacyAvcPictureInfo)) { - LOG_ERROR(Lib_Vdec2, "Invalid struct size"); - return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; - } - *picInfo = gLegacyPictureInfos.back(); - } - } else { - if (gPictureInfos.empty()) { - LOG_ERROR(Lib_Vdec2, "No picture info available"); - return ORBIS_OK; - } - if (p1stPictureInfoOut) { - OrbisVideodec2AvcPictureInfo* picInfo = - static_cast(p1stPictureInfoOut); - if (picInfo->thisSize != sizeof(OrbisVideodec2AvcPictureInfo)) { - LOG_ERROR(Lib_Vdec2, "Invalid struct size"); - return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; - } - *picInfo = gPictureInfos.back(); + if (p1stPictureInfoOut) { + // Copy enough data to check thisSize. + u64 picture_size = 0; + memcpy(&picture_size, p1stPictureInfoOut, sizeof(u64)); + if ((picture_size | 0x10) != sizeof(OrbisVideodec2AvcPictureInfo)) { + LOG_ERROR(Lib_Vdec2, "Invalid struct size"); + return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } + // Copy the requested picture data to the output. + memcpy(p1stPictureInfoOut, &gPictureInfos.back(), picture_size); + // Correct the outputted picture struct size. + memcpy(p1stPictureInfoOut, &picture_size, sizeof(u64)); } if (outputInfo->pictureCount > 1) { diff --git a/src/core/libraries/videodec/videodec2_avc.h b/src/core/libraries/videodec/videodec2_avc.h index dcf6ae007..cd14fe227 100644 --- a/src/core/libraries/videodec/videodec2_avc.h +++ b/src/core/libraries/videodec/videodec2_avc.h @@ -74,57 +74,4 @@ struct OrbisVideodec2AvcPictureInfo { }; static_assert(sizeof(OrbisVideodec2AvcPictureInfo) == 0x78); -// An older version of the OrbisVideodec2AvcPictureInfo struct -// Keeping this is needed for compatiblity with older games. -struct OrbisVideodec2LegacyAvcPictureInfo { - u64 thisSize; - - bool isValid; - - u64 ptsData; - u64 dtsData; - u64 attachedData; - - u8 idrPictureflag; - - u8 profile_idc; - u8 level_idc; - u32 pic_width_in_mbs_minus1; - u32 pic_height_in_map_units_minus1; - u8 frame_mbs_only_flag; - - u8 frame_cropping_flag; - u32 frameCropLeftOffset; - u32 frameCropRightOffset; - u32 frameCropTopOffset; - u32 frameCropBottomOffset; - - u8 aspect_ratio_info_present_flag; - u8 aspect_ratio_idc; - u16 sar_width; - u16 sar_height; - - u8 video_signal_type_present_flag; - u8 video_format; - u8 video_full_range_flag; - u8 colour_description_present_flag; - u8 colour_primaries; - u8 transfer_characteristics; - u8 matrix_coefficients; - - u8 timing_info_present_flag; - u32 num_units_in_tick; - u32 time_scale; - u8 fixed_frame_rate_flag; - - u8 bitstream_restriction_flag; - u8 max_dec_frame_buffering; - - u8 pic_struct_present_flag; - u8 pic_struct; - u8 field_pic_flag; - u8 bottom_field_flag; -}; -static_assert(sizeof(OrbisVideodec2LegacyAvcPictureInfo) == 0x68); - } // namespace Libraries::Videodec2 \ No newline at end of file diff --git a/src/core/libraries/videodec/videodec2_impl.cpp b/src/core/libraries/videodec/videodec2_impl.cpp index 08a7f8f00..e1ef5d587 100644 --- a/src/core/libraries/videodec/videodec2_impl.cpp +++ b/src/core/libraries/videodec/videodec2_impl.cpp @@ -12,7 +12,6 @@ namespace Libraries::Videodec2 { std::vector gPictureInfos; -std::vector gLegacyPictureInfos; static inline void CopyNV12Data(u8* dst, const AVFrame& src) { if (src.width == src.linesize[0]) { @@ -132,46 +131,27 @@ s32 VdecDecoder::Decode(const OrbisVideodec2InputData& inputData, outputInfo.isErrorFrame = false; outputInfo.pictureCount = 1; // TODO: 2 pictures for interlaced video - // For proper compatibility with older games, check the inputted OutputInfo struct size. + // Only set framePitchInBytes if the game uses the newer struct version. if (outputInfo.thisSize == sizeof(OrbisVideodec2OutputInfo)) { - // framePitchInBytes only exists in the newer struct. - outputInfo.framePitchInBytes = frame->width; - if (outputInfo.isValid) { - OrbisVideodec2AvcPictureInfo pictureInfo = {}; + outputInfo.framePitchInBytes = frame->linesize[0]; + } - pictureInfo.thisSize = sizeof(OrbisVideodec2AvcPictureInfo); - pictureInfo.isValid = true; + if (outputInfo.isValid) { + OrbisVideodec2AvcPictureInfo pictureInfo = {}; - pictureInfo.ptsData = inputData.ptsData; - pictureInfo.dtsData = inputData.dtsData; - pictureInfo.attachedData = inputData.attachedData; + pictureInfo.thisSize = sizeof(OrbisVideodec2AvcPictureInfo); + pictureInfo.isValid = true; - pictureInfo.frameCropLeftOffset = frame->crop_left; - pictureInfo.frameCropRightOffset = frame->crop_right; - pictureInfo.frameCropTopOffset = frame->crop_top; - pictureInfo.frameCropBottomOffset = frame->crop_bottom; + pictureInfo.ptsData = inputData.ptsData; + pictureInfo.dtsData = inputData.dtsData; + pictureInfo.attachedData = inputData.attachedData; - gPictureInfos.push_back(pictureInfo); - } - } else { - if (outputInfo.isValid) { - // If the game uses the older struct versions, we need to use it too. - OrbisVideodec2LegacyAvcPictureInfo pictureInfo = {}; + pictureInfo.frameCropLeftOffset = frame->crop_left; + pictureInfo.frameCropRightOffset = frame->crop_right; + pictureInfo.frameCropTopOffset = frame->crop_top; + pictureInfo.frameCropBottomOffset = frame->crop_bottom; - pictureInfo.thisSize = sizeof(OrbisVideodec2LegacyAvcPictureInfo); - pictureInfo.isValid = true; - - pictureInfo.ptsData = inputData.ptsData; - pictureInfo.dtsData = inputData.dtsData; - pictureInfo.attachedData = inputData.attachedData; - - pictureInfo.frameCropLeftOffset = frame->crop_left; - pictureInfo.frameCropRightOffset = frame->crop_right; - pictureInfo.frameCropTopOffset = frame->crop_top; - pictureInfo.frameCropBottomOffset = frame->crop_bottom; - - gLegacyPictureInfos.push_back(pictureInfo); - } + gPictureInfos.push_back(pictureInfo); } } diff --git a/src/core/libraries/videodec/videodec2_impl.h b/src/core/libraries/videodec/videodec2_impl.h index da16b3baa..382eab221 100644 --- a/src/core/libraries/videodec/videodec2_impl.h +++ b/src/core/libraries/videodec/videodec2_impl.h @@ -16,7 +16,6 @@ extern "C" { namespace Libraries::Videodec2 { extern std::vector gPictureInfos; -extern std::vector gLegacyPictureInfos; class VdecDecoder { public: From 26e2689b0619c95f47c7ed0c41d6909bbfa4a699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Thu, 2 Apr 2026 08:37:18 +0200 Subject: [PATCH 13/27] VOP3P instructions definitions (#4202) --- src/shader_recompiler/frontend/decode.cpp | 61 +++++++++++++- src/shader_recompiler/frontend/decode.h | 1 + src/shader_recompiler/frontend/format.cpp | 84 ++++++++++++++++++++ src/shader_recompiler/frontend/instruction.h | 36 +++++++++ src/shader_recompiler/frontend/opcodes.h | 57 +++++++++++++ 5 files changed, 237 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/frontend/decode.cpp b/src/shader_recompiler/frontend/decode.cpp index 37e8a0973..ae58f080b 100644 --- a/src/shader_recompiler/frontend/decode.cpp +++ b/src/shader_recompiler/frontend/decode.cpp @@ -3,6 +3,7 @@ #include #include "common/assert.h" +#include "core/libraries/kernel/process.h" #include "shader_recompiler/frontend/decode.h" #include @@ -39,6 +40,7 @@ InstEncoding GetInstructionEncoding(u32 token) { encoding = static_cast(token & (u32)EncodingMask::MASK_6bit); switch (encoding) { case InstEncoding::VOP3: + case InstEncoding::VOP3P: case InstEncoding::EXP: case InstEncoding::VINTRP: case InstEncoding::DS: @@ -82,7 +84,6 @@ InstEncoding GetInstructionEncoding(u32 token) { break; } - UNREACHABLE(); return InstEncoding::ILLEGAL; } @@ -111,7 +112,7 @@ GcnInst GcnDecodeContext::decodeInstruction(GcnCodeSlice& code) { const uint32_t token = code.at(0); InstEncoding encoding = GetInstructionEncoding(token); - ASSERT_MSG(encoding != InstEncoding::ILLEGAL, "illegal encoding"); + ASSERT_MSG(encoding != InstEncoding::ILLEGAL, "illegal encoding: {:#x}", token); uint32_t encodingLen = getEncodingLength(encoding); // Clear the instruction @@ -155,6 +156,7 @@ uint32_t GcnDecodeContext::getEncodingLength(InstEncoding encoding) { break; case InstEncoding::VOP3: + case InstEncoding::VOP3P: case InstEncoding::MUBUF: case InstEncoding::MTBUF: case InstEncoding::MIMG: @@ -189,6 +191,9 @@ uint32_t GcnDecodeContext::getOpMapOffset(InstEncoding encoding) { case InstEncoding::VOP3: offset = (uint32_t)OpcodeMap::OP_MAP_VOP3; break; + case InstEncoding::VOP3P: + offset = (uint32_t)OpcodeMap::OP_MAP_VOP3P; + break; case InstEncoding::EXP: offset = (uint32_t)OpcodeMap::OP_MAP_EXP; break; @@ -381,6 +386,9 @@ void GcnDecodeContext::decodeInstruction64(InstEncoding encoding, GcnCodeSlice& case InstEncoding::VOP3: decodeInstructionVOP3(hexInstruction); break; + case InstEncoding::VOP3P: + decodeInstructionVOP3P(hexInstruction); + break; case InstEncoding::MUBUF: decodeInstructionMUBUF(hexInstruction); break; @@ -712,6 +720,55 @@ void GcnDecodeContext::decodeInstructionVOP3(uint64_t hexInstruction) { } } +void GcnDecodeContext::decodeInstructionVOP3P(uint64_t hexInstruction) { + u32 vdst = bit::extract(hexInstruction, 7, 0); + u32 op = bit::extract(hexInstruction, 22, 16); + u32 src0 = bit::extract(hexInstruction, 40, 32); + u32 src1 = bit::extract(hexInstruction, 49, 41); + u32 src2 = bit::extract(hexInstruction, 58, 50); + + m_instruction.opcode = static_cast(op + static_cast(OpcodeMap::OP_MAP_VOP3P)); + + m_instruction.src[0].field = getOperandField(src0); + m_instruction.src[0].code = + m_instruction.src[0].field == OperandField::VectorGPR ? src0 - VectorGPRMin : src0; + m_instruction.src[1].field = getOperandField(src1); + m_instruction.src[1].code = + m_instruction.src[1].field == OperandField::VectorGPR ? src1 - VectorGPRMin : src1; + m_instruction.src[2].field = getOperandField(src2); + m_instruction.src[2].code = + m_instruction.src[2].field == OperandField::VectorGPR ? src2 - VectorGPRMin : src2; + m_instruction.dst[0].field = OperandField::VectorGPR; + m_instruction.dst[0].code = vdst; + + m_instruction.control.vop3p = *reinterpret_cast(&hexInstruction); + + // update input modifier + auto& control = m_instruction.control.vop3p; + for (u32 i = 0; i != 3; ++i) { + if (control.neg & (1u << i)) { + m_instruction.src[i].input_modifier.neg = true; + } + + if (control.neg_hi & (1u << i)) { + m_instruction.src[i].input_modifier.neg_hi = true; + } + + if (control.op_sel & (1u << i)) { + m_instruction.src[i].op_sel.op_sel = true; + } + + if (control.get_op_sel_hi(i)) { + m_instruction.src[i].op_sel.op_sel_hi = true; + } + } + + // update output modifier + auto& outputMod = m_instruction.dst[0].output_modifier; + + outputMod.clamp = static_cast(control.clamp); +} + void GcnDecodeContext::decodeInstructionMUBUF(uint64_t hexInstruction) { u32 op = bit::extract(hexInstruction, 24, 18); u32 vaddr = bit::extract(hexInstruction, 39, 32); diff --git a/src/shader_recompiler/frontend/decode.h b/src/shader_recompiler/frontend/decode.h index 8125ce7fb..605a4e229 100644 --- a/src/shader_recompiler/frontend/decode.h +++ b/src/shader_recompiler/frontend/decode.h @@ -84,6 +84,7 @@ private: void decodeInstructionVINTRP(uint32_t hexInstruction); // 64 bits encodings void decodeInstructionVOP3(uint64_t hexInstruction); + void decodeInstructionVOP3P(uint64_t hexInstruction); void decodeInstructionMUBUF(uint64_t hexInstruction); void decodeInstructionMTBUF(uint64_t hexInstruction); void decodeInstructionMIMG(uint64_t hexInstruction); diff --git a/src/shader_recompiler/frontend/format.cpp b/src/shader_recompiler/frontend/format.cpp index 4a90fe358..bc0ab3707 100644 --- a/src/shader_recompiler/frontend/format.cpp +++ b/src/shader_recompiler/frontend/format.cpp @@ -1784,6 +1784,88 @@ constexpr std::array InstructionFormatVOP3 = {{ {}, }}; +constexpr std::array InstructionFormatVOP3P = {{ + // 0 = V_PK_MAD_I16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 3, 1, ScalarType::Sint16, + ScalarType::Sint16}, + // 1 = V_PK_MUL_LO_U16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16, + ScalarType::Uint16}, + // 2 = V_PK_ADD_I16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16, + ScalarType::Sint16}, + // 3 = V_PK_SUB_I16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16, + ScalarType::Sint16}, + // 4 = V_PK_LSHLREV_B16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16, + ScalarType::Uint16}, + // 5 = V_PK_LSHRREV_B16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16, + ScalarType::Uint16}, + // 6 = V_PK_ASHRREV_I16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16, + ScalarType::Uint16}, + // 7 = V_PK_MAX_I16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16, + ScalarType::Sint16}, + // 8 = V_PK_MIN_I16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16, + ScalarType::Sint16}, + // 9 = V_PK_MAD_U16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 3, 1, ScalarType::Uint16, + ScalarType::Uint16}, + // 10 = V_PK_ADD_U16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16, + ScalarType::Uint16}, + // 11 = V_PK_SUB_U16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16, + ScalarType::Uint16}, + // 12 = V_PK_MAX_U16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16, + ScalarType::Uint16}, + // 13 = V_PK_MIN_U16 + {InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16, + ScalarType::Uint16}, + // 14 = V_PK_FMA_F16 + {InstClass::VectorFpArith16, InstCategory::VectorALU, 3, 1, ScalarType::Float16, + ScalarType::Float16}, + // 15 = V_PK_ADD_F16 + {InstClass::VectorFpArith16, InstCategory::VectorALU, 2, 1, ScalarType::Float16, + ScalarType::Float16}, + // 16 = V_PK_MUL_F16 + {InstClass::VectorFpArith16, InstCategory::VectorALU, 2, 1, ScalarType::Float16, + ScalarType::Float16}, + // 17 = V_PK_MIN_F16 + {InstClass::VectorFpArith16, InstCategory::VectorALU, 2, 1, ScalarType::Float16, + ScalarType::Float16}, + // 18 = V_PK_MAX_F16 + {InstClass::VectorFpArith16, InstCategory::VectorALU, 2, 1, ScalarType::Float16, + ScalarType::Float16}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + // 32 = V_MAD_MIX_F32 + {InstClass::VectorFpArith32, InstCategory::VectorALU, 3, 1, ScalarType::Float32, + ScalarType::Float32}, + // 33 = V_MAD_MIXLO_F16 + {InstClass::VectorFpArith32, InstCategory::VectorALU, 3, 1, ScalarType::Float32, + ScalarType::Float32}, + // 34 = V_MAD_MIXHI_F16 + {InstClass::VectorFpArith32, InstCategory::VectorALU, 3, 1, ScalarType::Float32, + ScalarType::Float32}, +}}; + constexpr std::array InstructionFormatVOP1 = {{ // 0 = V_NOP {InstClass::VectorMisc, InstCategory::VectorALU, 0, 1, ScalarType::Any, ScalarType::Any}, @@ -3705,6 +3787,8 @@ InstFormat InstructionFormat(InstEncoding encoding, uint32_t opcode) { return InstructionFormatVOPC[opcode]; case InstEncoding::VOP3: return InstructionFormatVOP3[opcode]; + case InstEncoding::VOP3P: + return InstructionFormatVOP3P[opcode]; case InstEncoding::EXP: return InstructionFormatEXP[opcode]; case InstEncoding::VINTRP: diff --git a/src/shader_recompiler/frontend/instruction.h b/src/shader_recompiler/frontend/instruction.h index f4e7bc9f2..05d5ce7b7 100644 --- a/src/shader_recompiler/frontend/instruction.h +++ b/src/shader_recompiler/frontend/instruction.h @@ -3,6 +3,7 @@ #pragma once +#include "common/assert.h" #include "shader_recompiler/frontend/opcodes.h" namespace Shader::Gcn { @@ -25,6 +26,7 @@ enum OperandFieldRange { /// These are applied after loading an operand register. struct InputModifiers { bool neg = false; + bool neg_hi = false; bool abs = false; }; @@ -34,11 +36,18 @@ struct OutputModifiers { float multiplier = 0.f; }; +struct OperandSelection { + bool op_sel = false; + bool op_sel_hi = false; +}; + struct InstOperand { OperandField field = OperandField::Undefined; ScalarType type = ScalarType::Undefined; InputModifiers input_modifier = {}; OutputModifiers output_modifier = {}; + // only valid for packed 16bit operations + OperandSelection op_sel = {}; u32 code = 0xFFFFFFFF; }; @@ -90,6 +99,32 @@ struct InstControlVOP3 { u64 neg : 3; }; +struct InstControlVOP3P { + u64 : 8; + u64 neg_hi : 3; + u64 op_sel : 3; + u64 op_sel_hi_2 : 1; + u64 clamp : 1; + u64 : 43; + u64 op_sel_hi_01 : 2; + u64 neg : 3; + + bool get_op_sel_hi(int idx) { + switch (idx) { + case 0: + return (op_sel_hi_01 & 1) == 1; + case 1: + return ((op_sel_hi_01 >> 1) & 1) == 1; + case 2: + return (op_sel_hi_2 & 1) == 1; + default: + UNREACHABLE_MSG("get_op_sel_hi: {}", idx); + } + } +}; + +static_assert(sizeof(InstControlVOP3P) == 8); + struct InstControlSMRD { u32 offset : 8; u32 imm : 1; @@ -174,6 +209,7 @@ union InstControl { InstControlSOPK sopk; InstControlSOPP sopp; InstControlVOP3 vop3; + InstControlVOP3P vop3p; InstControlSMRD smrd; InstControlMUBUF mubuf; InstControlMTBUF mtbuf; diff --git a/src/shader_recompiler/frontend/opcodes.h b/src/shader_recompiler/frontend/opcodes.h index 7390a3940..6d6df88a6 100644 --- a/src/shader_recompiler/frontend/opcodes.h +++ b/src/shader_recompiler/frontend/opcodes.h @@ -662,6 +662,33 @@ enum class OpcodeVOP3 : u32 { OP_RANGE_VOP3 = V_EXP_LEGACY_F32 + 1, }; +enum class OpcodeVOP3P : u32 { + V_PK_MAD_I16 = 0, + V_PK_MUL_LO_U16 = 1, + V_PK_ADD_I16 = 2, + V_PK_SUB_I16 = 3, + V_PK_LSHLREV_B16 = 4, + V_PK_LSHRREV_B16 = 5, + V_PK_ASHRREV_I16 = 6, + V_PK_MAX_I16 = 7, + V_PK_MIN_I16 = 8, + V_PK_MAD_U16 = 9, + V_PK_ADD_U16 = 10, + V_PK_SUB_U16 = 11, + V_PK_MAX_U16 = 12, + V_PK_MIN_U16 = 13, + V_PK_FMA_F16 = 14, + V_PK_ADD_F16 = 15, + V_PK_MUL_F16 = 16, + V_PK_MIN_F16 = 17, + V_PK_MAX_F16 = 18, + V_MAD_MIX_F32 = 32, + V_MAD_MIXLO_F16 = 33, + V_MAD_MIXHI_F16 = 34, + + OP_RANGE_VOP3P = V_MAD_MIXHI_F16 + 1, +}; + enum class OpcodeVOP1 : u32 { V_NOP = 0, V_MOV_B32 = 1, @@ -1313,6 +1340,7 @@ enum class OpcodeMap : u32 { OP_MAP_MTBUF = OP_MAP_MUBUF + (u32)OpcodeMUBUF::OP_RANGE_MUBUF, OP_MAP_MIMG = OP_MAP_MTBUF + (u32)OpcodeMTBUF::OP_RANGE_MTBUF, OP_MAP_EXP = OP_MAP_MIMG + (u32)OpcodeMIMG::OP_RANGE_MIMG, + OP_MAP_VOP3P = OP_MAP_EXP + (u32)OpcodeEXP::OP_RANGE_EXP, }; enum class Opcode : u32 { @@ -2192,6 +2220,29 @@ enum class Opcode : u32 { IMAGE_SAMPLE_C_CD_CL_O = 111 + (u32)OpcodeMap::OP_MAP_MIMG, // EXP EXP = 0 + (u32)OpcodeMap::OP_MAP_EXP, + // VOP3P + V_PK_MAD_I16 = 0 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_MUL_LO_U16 = 1 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_ADD_I16 = 2 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_SUB_I16 = 3 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_LSHLREV_B16 = 4 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_LSHRREV_B16 = 5 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_ASHRREV_I16 = 6 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_MAX_I16 = 7 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_MIN_I16 = 8 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_MAD_U16 = 9 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_ADD_U16 = 10 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_SUB_U16 = 11 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_MAX_U16 = 12 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_MIN_U16 = 13 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_FMA_F16 = 14 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_ADD_F16 = 15 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_MUL_F16 = 16 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_MIN_F16 = 17 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_PK_MAX_F16 = 18 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_MAD_MIX_F32 = 32 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_MAD_MIXLO_F16 = 33 + (u32)OpcodeMap::OP_MAP_VOP3P, + V_MAD_MIXHI_F16 = 34 + (u32)OpcodeMap::OP_MAP_VOP3P, }; enum class EncodingMask : u32 { @@ -2222,6 +2273,8 @@ enum class InstEncoding : u32 { VOP3 = 0x00000034u << 26, /// bits [31:26] - (1 1 1 1 1 0) EXP = 0x0000003Eu << 26, + /// bits [31:26] - (1 1 0 0 1 1) + VOP3P = 0x00000033u << 26, /// bits [31:26] - (1 1 0 0 1 0) VINTRP = 0x00000032u << 26, /// bits [31:26] - (1 1 0 1 1 0) @@ -2271,6 +2324,7 @@ enum class InstClass : u32 { VectorBitField32, VectorThreadMask, VectorBitField64, + VectorFpArith16, VectorFpArith32, VectorFpRound32, VectorFpField32, @@ -2281,6 +2335,7 @@ enum class InstClass : u32 { VectorFpField64, VectorFpTran64, VectorFpCmp64, + VectorIntArith16, VectorIntArith32, VectorIntArith64, VectorIntCmp32, @@ -2360,8 +2415,10 @@ enum class InstCategory : u32 { enum class ScalarType : u32 { Undefined, Any, + Uint16, Uint32, Uint64, + Sint16, Sint32, Sint64, Float16, From 450472b51fbc53c504be2e738a8f2d5660ea7029 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:38:51 +0800 Subject: [PATCH 14/27] Update serdes.h (#4214) --- src/common/serdes.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/common/serdes.h b/src/common/serdes.h index f91a0ace8..a8152e95a 100644 --- a/src/common/serdes.h +++ b/src/common/serdes.h @@ -42,7 +42,8 @@ struct Archive { } void Advance(size_t size) { - ASSERT(offset + size <= container.size()); + ASSERT_MSG(offset + size <= container.size(), + "Invalid or corrupted deserialization container/shader cache"); offset += size; } @@ -104,7 +105,8 @@ struct Writer { struct Reader { template void Read(T* ptr, size_t size) { - ASSERT(ar.offset + size <= ar.container.size()); + ASSERT_MSG(ar.offset + size <= ar.container.size(), + "Invalid or corrupted deserialization container/shader cache"); std::memcpy(reinterpret_cast(ptr), ar.CurrPtr(), size); ar.Advance(size); } From 73520ae094a502d2d3992b627ea0f17eb8b17900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Sat, 4 Apr 2026 12:02:44 +0200 Subject: [PATCH 15/27] GS fixes (#4213) * Handle S_BFM_I32 in a copy shader * GS: set dwords_per_vertex based on the number of buffer loads if different from VERT_ITEMSIZE --- src/shader_recompiler/frontend/copy_shader.cpp | 9 +++++++++ src/shader_recompiler/frontend/copy_shader.h | 1 + .../ir/passes/ring_access_elimination.cpp | 9 ++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/shader_recompiler/frontend/copy_shader.cpp b/src/shader_recompiler/frontend/copy_shader.cpp index 795003e43..ff3045b86 100644 --- a/src/shader_recompiler/frontend/copy_shader.cpp +++ b/src/shader_recompiler/frontend/copy_shader.cpp @@ -45,6 +45,14 @@ CopyShaderData ParseCopyShader(std::span code) { sources[inst.dst[0].code] += inst.control.sopk.simm; break; } + case Gcn::Opcode::S_BFM_B32: { + ASSERT(inst.src[0].field == Gcn::OperandField::SignedConstIntPos && + inst.src[1].field == Gcn::OperandField::SignedConstIntPos); + const auto src0 = inst.src[0].code - Gcn::OperandFieldRange::SignedConstIntPosMin + 1; + const auto src1 = inst.src[1].code - Gcn::OperandFieldRange::SignedConstIntPosMin + 1; + sources[inst.dst[0].code] = ((1 << src0) - 1) << src1; + break; + } case Gcn::Opcode::EXP: { const auto& exp = inst.control.exp; const IR::Attribute semantic = static_cast(exp.target); @@ -71,6 +79,7 @@ CopyShaderData ParseCopyShader(std::span code) { ASSERT(sources[index] != -1); offsets[inst.src[1].code] += sources[index]; } + data.num_comps++; break; } default: diff --git a/src/shader_recompiler/frontend/copy_shader.h b/src/shader_recompiler/frontend/copy_shader.h index 24c7060ed..c416bde99 100644 --- a/src/shader_recompiler/frontend/copy_shader.h +++ b/src/shader_recompiler/frontend/copy_shader.h @@ -15,6 +15,7 @@ struct CopyShaderData { std::map> attr_map; u32 num_attrs{0}; u32 output_vertices{0}; + u32 num_comps{0}; }; CopyShaderData ParseCopyShader(std::span code); diff --git a/src/shader_recompiler/ir/passes/ring_access_elimination.cpp b/src/shader_recompiler/ir/passes/ring_access_elimination.cpp index f8818b622..a7a23a726 100644 --- a/src/shader_recompiler/ir/passes/ring_access_elimination.cpp +++ b/src/shader_recompiler/ir/passes/ring_access_elimination.cpp @@ -104,6 +104,13 @@ void RingAccessElimination(const IR::Program& program, const RuntimeInfo& runtim output_vertices, info.gs_copy_data.output_vertices); output_vertices = info.gs_copy_data.output_vertices; } + u32 dwords_per_vertex = gs_info.out_vertex_data_size; + if (info.gs_copy_data.num_comps && info.gs_copy_data.num_comps != dwords_per_vertex) { + LOG_WARNING(Render_Vulkan, + "VERT_ITEMSIZE {} is different than actual number of dwords per vertex {}", + dwords_per_vertex, info.gs_copy_data.num_comps); + dwords_per_vertex = info.gs_copy_data.num_comps; + } ForEachInstruction([&](IR::IREmitter& ir, IR::Inst& inst) { const auto opcode = inst.GetOpcode(); @@ -139,7 +146,7 @@ void RingAccessElimination(const IR::Program& program, const RuntimeInfo& runtim const auto offset = inst.Flags().inst_offset.Value(); const auto data = ir.BitCast(IR::U32{inst.Arg(2)}); const auto comp_ofs = output_vertices * 4u; - const auto output_size = comp_ofs * gs_info.out_vertex_data_size; + const auto output_size = comp_ofs * dwords_per_vertex; const auto vc_read_ofs = (((offset / comp_ofs) * comp_ofs) % output_size) * 16u; const auto& it = info.gs_copy_data.attr_map.find(vc_read_ofs); From b365d5fe7819d4d0231fb74561e725e136090a28 Mon Sep 17 00:00:00 2001 From: Niram7777 Date: Sat, 4 Apr 2026 20:57:22 +0200 Subject: [PATCH 16/27] Vulkan device destroy DescriptorPool on Shutdown (#4217) Validation Error: [ VUID-vkDestroyDevice-device-05137 ] | MessageID = 0x4872eaa0 vkDestroyDevice(): Object Tracking - For VkDevice 0x55ac6bd22670, VkDescriptorPool 0x300000000030 has not been destroyed. The Vulkan spec states: All child objects created on device that can be destroyed or freed must have been destroyed or freed prior to destroying device (https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#VUID-vkDestroyDevice-device-05137) Objects: 1 [0] VkDescriptorPool 0x300000000030 --- src/imgui/renderer/imgui_impl_vulkan.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/imgui/renderer/imgui_impl_vulkan.cpp b/src/imgui/renderer/imgui_impl_vulkan.cpp index 97f44c318..0ef1d263d 100644 --- a/src/imgui/renderer/imgui_impl_vulkan.cpp +++ b/src/imgui/renderer/imgui_impl_vulkan.cpp @@ -1219,6 +1219,10 @@ void ImGuiImplVulkanDestroyDeviceObjects() { v.device.destroyDescriptorSetLayout(bd->descriptor_set_layout, v.allocator); bd->descriptor_set_layout = VK_NULL_HANDLE; } + if (bd->descriptor_pool) { + v.device.destroyDescriptorPool(bd->descriptor_pool, v.allocator); + bd->descriptor_pool = VK_NULL_HANDLE; + } if (bd->pipeline_layout) { v.device.destroyPipelineLayout(bd->pipeline_layout, v.allocator); bd->pipeline_layout = VK_NULL_HANDLE; From fb067bc43fbf69e39c708129c3a753f82d0b5c78 Mon Sep 17 00:00:00 2001 From: Niram7777 Date: Sat, 4 Apr 2026 21:30:28 +0200 Subject: [PATCH 17/27] Vulkan device destroy images_view on Swapchain::Destroy (#4218) --- src/video_core/renderer_vulkan/vk_swapchain.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index 04f9d8504..4904319bd 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -261,6 +261,12 @@ void Swapchain::Destroy() { LOG_WARNING(Render_Vulkan, "Failed to wait for device to become idle: {}", vk::to_string(wait_result)); } + + for (auto& image_view : images_view) { + device.destroyImageView(image_view); + } + images_view.clear(); + if (swapchain) { device.destroySwapchainKHR(swapchain); } From 93733a9ae85fa5dab673f073c3568f543ee2bed2 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:26:41 +0200 Subject: [PATCH 18/27] Fix edge case for touch states (#4219) --- src/core/libraries/pad/pad.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index f38621191..a24d4bc1a 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -423,12 +423,18 @@ int ProcessStates(s32 handle, OrbisPadData* pData, Input::GameController& contro states[i].touchpad[1].ID = 2; } - pData[i].touchData.touch[0].x = states[i].touchpad[0].x; - pData[i].touchData.touch[0].y = states[i].touchpad[0].y; - pData[i].touchData.touch[0].id = states[i].touchpad[0].ID; - pData[i].touchData.touch[1].x = states[i].touchpad[1].x; - pData[i].touchData.touch[1].y = states[i].touchpad[1].y; - pData[i].touchData.touch[1].id = states[i].touchpad[1].ID; + if (!states[i].touchpad[0].state && states[i].touchpad[1].state) { + pData[i].touchData.touch[0].x = states[i].touchpad[1].x; + pData[i].touchData.touch[0].y = states[i].touchpad[1].y; + pData[i].touchData.touch[0].id = states[i].touchpad[1].ID; + } else { + pData[i].touchData.touch[0].x = states[i].touchpad[0].x; + pData[i].touchData.touch[0].y = states[i].touchpad[0].y; + pData[i].touchData.touch[0].id = states[i].touchpad[0].ID; + pData[i].touchData.touch[1].x = states[i].touchpad[1].x; + pData[i].touchData.touch[1].y = states[i].touchpad[1].y; + pData[i].touchData.touch[1].id = states[i].touchpad[1].ID; + } pData[i].connected = connected; pData[i].timestamp = states[i].time; pData[i].connectedCount = connected_count; From e24f42b03ce2c16db0c6b3106ca1fbcc97764e29 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:35:53 -0500 Subject: [PATCH 19/27] Lib.Sysmodule: Allow libSceSystemGesture LLE (#4223) * Run libSceSystemGesture LLE It's fully safe. * Update sysmodule table appropriately. --- README.md | 12 ++++++------ src/core/libraries/libs.cpp | 2 -- src/core/libraries/sysmodule/sysmodule_internal.cpp | 4 +++- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0fb5c26ed..2a2aa9f34 100644 --- a/README.md +++ b/README.md @@ -150,12 +150,12 @@ The following firmware modules are supported and must be placed in shadPS4's `sy
-| Modules | Modules | Modules | Modules | -|-------------------------|-------------------------|-------------------------|-------------------------| -| libSceCesCs.sprx | libSceFont.sprx | libSceFontFt.sprx | libSceFreeTypeOt.sprx | -| libSceJpegDec.sprx | libSceJpegEnc.sprx | libSceJson.sprx | libSceJson2.sprx | -| libSceLibcInternal.sprx | libSceNgs2.sprx | libScePngEnc.sprx | libSceRtc.sprx | -| libSceUlt.sprx | libSceAudiodec.sprx | | | +| Modules | Modules | Modules | Modules | +|--------------------------|--------------------------|--------------------------|--------------------------| +| libSceAudiodec.sprx | libSceCesCs.sprx | libSceFont.sprx | libSceFontFt.sprx | +| libSceFreeTypeOt.sprx | libSceJpegDec.sprx | libSceJpegEnc.sprx | libSceJson.sprx | +| libSceJson2.sprx | libSceLibcInternal.sprx | libSceNgs2.sprx | libScePngEnc.sprx | +| libSceRtc.sprx | libSceSystemGesture.sprx | libSceUlt.sprx | |
> [!Caution] diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 6db1ba18d..762753fd1 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -63,7 +63,6 @@ #include "core/libraries/system/posix.h" #include "core/libraries/system/systemservice.h" #include "core/libraries/system/userservice.h" -#include "core/libraries/system_gesture/system_gesture.h" #include "core/libraries/ulobjmgr/ulobjmgr.h" #include "core/libraries/usbd/usbd.h" #include "core/libraries/videodec/videodec.h" @@ -120,7 +119,6 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Random::RegisterLib(sym); Libraries::Usbd::RegisterLib(sym); Libraries::Pad::RegisterLib(sym); - Libraries::SystemGesture::RegisterLib(sym); Libraries::Ajm::RegisterLib(sym); Libraries::ErrorDialog::RegisterLib(sym); Libraries::ImeDialog::RegisterLib(sym); diff --git a/src/core/libraries/sysmodule/sysmodule_internal.cpp b/src/core/libraries/sysmodule/sysmodule_internal.cpp index 56e130289..56a5a60b9 100644 --- a/src/core/libraries/sysmodule/sysmodule_internal.cpp +++ b/src/core/libraries/sysmodule/sysmodule_internal.cpp @@ -20,6 +20,7 @@ #include "core/libraries/sysmodule/sysmodule_error.h" #include "core/libraries/sysmodule/sysmodule_internal.h" #include "core/libraries/sysmodule/sysmodule_table.h" +#include "core/libraries/system_gesture/system_gesture.h" #include "core/linker.h" #include "emulator.h" @@ -223,7 +224,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}, + {"libSceSystemGesture.sprx", &Libraries::SystemGesture::RegisterLib}}); // Iterate through the allowed array const auto it = std::ranges::find_if( From 162cb18d9d26555be71ef3c1992eeba37ef16a7d Mon Sep 17 00:00:00 2001 From: Niram7777 Date: Sat, 4 Apr 2026 23:47:26 +0200 Subject: [PATCH 20/27] Vulkan destroy presenter members before device (#4222) --- src/video_core/renderer_vulkan/vk_instance.cpp | 2 ++ src/video_core/renderer_vulkan/vk_presenter.cpp | 1 - src/video_core/renderer_vulkan/vk_presenter.h | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 6898df97a..8bd2b19d6 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -8,6 +8,7 @@ #include "common/assert.h" #include "common/debug.h" #include "common/types.h" +#include "imgui/renderer/imgui_core.h" #include "sdl_window.h" #include "video_core/renderer_vulkan/liverpool_to_vk.h" #include "video_core/renderer_vulkan/vk_instance.h" @@ -183,6 +184,7 @@ Instance::Instance(Frontend::WindowSDL& window, s32 physical_device_index, } Instance::~Instance() { + ImGui::Core::Shutdown(GetDevice()); vmaDestroyAllocator(allocator); } diff --git a/src/video_core/renderer_vulkan/vk_presenter.cpp b/src/video_core/renderer_vulkan/vk_presenter.cpp index c2a2a6621..49002c741 100644 --- a/src/video_core/renderer_vulkan/vk_presenter.cpp +++ b/src/video_core/renderer_vulkan/vk_presenter.cpp @@ -144,7 +144,6 @@ Presenter::~Presenter() { device.destroyImageView(frame.image_view); device.destroyFence(frame.present_done); } - ImGui::Core::Shutdown(device); } bool Presenter::IsVideoOutSurface(const AmdGpu::ColorBuffer& color_buffer) const { diff --git a/src/video_core/renderer_vulkan/vk_presenter.h b/src/video_core/renderer_vulkan/vk_presenter.h index c1748e9dd..614b9b77e 100644 --- a/src/video_core/renderer_vulkan/vk_presenter.h +++ b/src/video_core/renderer_vulkan/vk_presenter.h @@ -112,13 +112,13 @@ private: u32 expected_frame_width{1920}; u32 expected_frame_height{1080}; + Frontend::WindowSDL& window; + Instance instance; HostPasses::FsrPass fsr_pass; HostPasses::FsrPass::Settings fsr_settings{}; HostPasses::PostProcessingPass::Settings pp_settings{}; HostPasses::PostProcessingPass pp_pass; - Frontend::WindowSDL& window; AmdGpu::Liverpool* liverpool; - Instance instance; Scheduler draw_scheduler; Scheduler present_scheduler; Scheduler flip_scheduler; From 304a2c7c782eb6ab18686c6def3db1b9239771d7 Mon Sep 17 00:00:00 2001 From: Niram7777 Date: Sun, 5 Apr 2026 06:44:50 +0200 Subject: [PATCH 21/27] Vulkan presenter reset CommandBuffer on *_scheduler (#4221) [Render.Vulkan] (shadPS4:Main) vk_platform.cpp:58 DebugUtilsCallback: VUID-vkDestroyImage-image-01000: vkDestroyImage(): can't be called on VkImage 0x850000000085[Frame image #2] that is currently in use by VkCommandBuffer 0x560ea08752c0[CommandPool: Command Buffer 2]. The Vulkan spec states: All submitted commands that refer to image, either directly or via a VkImageView, must have completed execution (https://docs.vulkan.org/spec/latest/chapters/resources.html#VUID-vkDestroyImage-image-01000) Finish needed because of: [Render.Vulkan] (shadPS4:Main) vk_platform.cpp:58 DebugUtilsCallback: VUID-vkEndCommandBuffer-commandBuffer-00059: vkEndCommandBuffer(): Cannot be called for VkCommandBuffer 0x55c62bfbb580[CommandPool: Command Buffer 3] when it is not in a recording state, vkBeginCommandBuffer() must first be called. The Vulkan spec states: commandBuffer must be in the recording state (https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html#VUID-vkEndCommandBuffer-commandBuffer-00059) --- src/video_core/renderer_vulkan/vk_presenter.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_presenter.cpp b/src/video_core/renderer_vulkan/vk_presenter.cpp index 49002c741..7d403e7ee 100644 --- a/src/video_core/renderer_vulkan/vk_presenter.cpp +++ b/src/video_core/renderer_vulkan/vk_presenter.cpp @@ -137,7 +137,14 @@ Presenter::Presenter(Frontend::WindowSDL& window_, AmdGpu::Liverpool* liverpool_ Presenter::~Presenter() { ImGui::Layer::RemoveLayer(Common::Singleton::Instance()); + draw_scheduler.Finish(); + present_scheduler.Finish(); + flip_scheduler.Finish(); + Check(draw_scheduler.CommandBuffer().reset()); + Check(present_scheduler.CommandBuffer().reset()); + Check(flip_scheduler.CommandBuffer().reset()); + const vk::Device device = instance.GetDevice(); for (auto& frame : present_frames) { vmaDestroyImage(instance.GetAllocator(), frame.image, frame.allocation); From eb429be1cc7ab1a6f9437338653004b7a0076ab4 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:56:18 +0800 Subject: [PATCH 22/27] FS: easy mods folder for games (#4216) * Implement mods folder * Add logging when modified files exist * get mod folder entries in MntPoints::IterateDirectory * support eboot from mods folder --- src/core/file_sys/fs.cpp | 66 +++++++++++++++++++++++++++++++--------- src/core/file_sys/fs.h | 10 +++++- src/emulator.cpp | 10 +++++- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index aa474d20a..8327b51de 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -42,7 +42,7 @@ void MntPoints::UnmountAll() { } std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_read_only, - bool force_base_path) { + HostPathType path_type) { // Evil games like Turok2 pass double slashes e.g /app0//game.kpf std::string corrected_path(path); size_t pos = corrected_path.find("//"); @@ -80,8 +80,24 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea } patch_path /= rel_path; + std::filesystem::path mods_path = mount->host_path; + mods_path += "-mods"; + mods_path /= rel_path; + + if (path_type == HostPathType::Mod) { + return mods_path; + } else if (path_type == HostPathType::Patch) { + return patch_path; + } + if ((corrected_path.starts_with("/app0") || corrected_path.starts_with("/hostapp")) && - !force_base_path && !ignore_game_patches && std::filesystem::exists(patch_path)) { + path_type != HostPathType::Base && std::filesystem::exists(mods_path)) { + return mods_path; + } + + if ((corrected_path.starts_with("/app0") || corrected_path.starts_with("/hostapp")) && + path_type != HostPathType::Base && !ignore_game_patches && + std::filesystem::exists(patch_path)) { return patch_path; } @@ -143,7 +159,7 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea return std::optional(current_path); }; - if (!force_base_path && !ignore_game_patches) { + if (path_type != HostPathType::Base && !ignore_game_patches) { if (const auto path = search(patch_path)) { return *path; } @@ -160,34 +176,54 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea // TODO: Does not handle mount points inside mount points. void MntPoints::IterateDirectory(std::string_view guest_directory, const IterateDirectoryCallback& callback) { - const auto base_path = GetHostPath(guest_directory, nullptr, true); - const auto patch_path = GetHostPath(guest_directory, nullptr, false); - // Only need to consider patch path if it exists and does not resolve to the same as base. - const auto apply_patch = base_path != patch_path && std::filesystem::exists(patch_path); + const auto base_path = GetHostPath(guest_directory, nullptr, HostPathType::Base); + + // Forces path types so as not to resolve to base path + const auto patch_path = GetHostPath(guest_directory, nullptr, HostPathType::Patch); + const auto mod_path = GetHostPath(guest_directory, nullptr, HostPathType::Mod); // Prepend entries for . and .., as both are treated as files on PS4. callback(base_path / ".", false); callback(base_path / "..", false); - // Pass 1: Any files that existed in the base directory, using patch directory if needed. + // Pass 1: Any files that existed in the base directory, using mod/patch directory if needed. if (std::filesystem::exists(base_path)) { for (const auto& entry : std::filesystem::directory_iterator(base_path)) { - if (apply_patch) { - const auto patch_entry_path = patch_path / entry.path().filename(); - if (std::filesystem::exists(patch_entry_path)) { - callback(patch_entry_path, !std::filesystem::is_directory(patch_entry_path)); - continue; - } + const auto mod_entry_path = mod_path / entry.path().filename(); + const auto patch_entry_path = patch_path / entry.path().filename(); + if (std::filesystem::exists(mod_entry_path)) { + callback(mod_entry_path, !std::filesystem::is_directory(mod_entry_path)); + continue; + } else if (std::filesystem::exists(patch_entry_path)) { + callback(patch_entry_path, !std::filesystem::is_directory(patch_entry_path)); + continue; } callback(entry.path(), !entry.is_directory()); } } // Pass 2: Any files that exist only in the patch directory. - if (apply_patch) { + if (std::filesystem::exists(patch_path)) { for (const auto& entry : std::filesystem::directory_iterator(patch_path)) { const auto base_entry_path = base_path / entry.path().filename(); if (!std::filesystem::exists(base_entry_path)) { + const auto mod_entry_path = mod_path / entry.path().filename(); + if (std::filesystem::exists(mod_entry_path)) { + callback(mod_entry_path, !std::filesystem::is_directory(mod_entry_path)); + continue; + } + callback(entry.path(), !entry.is_directory()); + } + } + } + + // Pass 3: Any files that exist only in the mod directory (confirmed this can be valid) + if (std::filesystem::exists(mod_path)) { + for (const auto& entry : std::filesystem::directory_iterator(mod_path)) { + const auto base_entry_path = base_path / entry.path().filename(); + const auto patch_entry_path = patch_path / entry.path().filename(); + if (!std::filesystem::exists(base_entry_path) && + !std::filesystem::exists(patch_entry_path)) { callback(entry.path(), !entry.is_directory()); } } diff --git a/src/core/file_sys/fs.h b/src/core/file_sys/fs.h index 0522c3d8a..9a782b19e 100644 --- a/src/core/file_sys/fs.h +++ b/src/core/file_sys/fs.h @@ -36,6 +36,13 @@ public: bool read_only; }; + enum class HostPathType { + Default, // Prioritizes Mod, then patch, then base + Base, + Patch, + Mod + }; + explicit MntPoints() = default; ~MntPoints() = default; @@ -45,7 +52,8 @@ public: void UnmountAll(); std::filesystem::path GetHostPath(std::string_view guest_directory, - bool* is_read_only = nullptr, bool force_base_path = false); + bool* is_read_only = nullptr, + HostPathType host_path = HostPathType::Default); using IterateDirectoryCallback = std::function; void IterateDirectory(std::string_view guest_directory, diff --git a/src/emulator.cpp b/src/emulator.cpp index 616a21ed1..3815c8f3d 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -109,7 +109,8 @@ void Emulator::Run(std::filesystem::path file, std::vector args, } else { game_folder = file.parent_path(); if (const auto game_folder_name = game_folder.filename().string(); - game_folder_name.ends_with("-UPDATE") || game_folder_name.ends_with("-patch")) { + game_folder_name.ends_with("-UPDATE") || game_folder_name.ends_with("-patch") || + game_folder_name.ends_with("-mods")) { // If an executable was launched from a separate update directory, // use the base game directory as the game folder. const std::string base_name = game_folder_name.substr(0, game_folder_name.rfind('-')); @@ -292,6 +293,13 @@ void Emulator::Run(std::filesystem::path file, std::vector args, } } + std::filesystem::path mods_folder = game_folder; + mods_folder += "-mods"; + + if (std::filesystem::exists(mods_folder) && !std::filesystem::is_empty(mods_folder)) { + LOG_INFO(Loader, "Files found in game mods folder"); + } + // Create stdin/stdout/stderr Common::Singleton::Instance()->CreateStdHandles(); From 13da5a82ccecb407bb7060c9e6b54353d71d0758 Mon Sep 17 00:00:00 2001 From: Kravickas Date: Sun, 5 Apr 2026 18:44:09 +0200 Subject: [PATCH 23/27] protect (#4212) Protection change may override page fault tracking set by the GPU buffer cache. Invalidate so the cache re-uploads and re-tracks. --- src/core/libraries/kernel/memory.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 378064e44..f0b7df4c9 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -339,7 +339,11 @@ s32 PS4_SYSV_ABI sceKernelMprotect(const void* addr, u64 size, s32 prot) { Core::MemoryManager* memory_manager = Core::Memory::Instance(); Core::MemoryProt protection_flags = static_cast(prot); - return memory_manager->Protect(aligned_addr, aligned_size, protection_flags); + s32 result = memory_manager->Protect(aligned_addr, aligned_size, protection_flags); + if (result == ORBIS_OK) { + memory_manager->InvalidateMemory(aligned_addr, aligned_size); + } + return result; } s32 PS4_SYSV_ABI posix_mprotect(const void* addr, u64 size, s32 prot) { @@ -370,6 +374,7 @@ s32 PS4_SYSV_ABI sceKernelMtypeprotect(const void* addr, u64 size, s32 mtype, s3 s32 result = memory_manager->Protect(aligned_addr, aligned_size, protection_flags); if (result == ORBIS_OK) { memory_manager->SetDirectMemoryType(aligned_addr, aligned_size, mtype); + memory_manager->InvalidateMemory(aligned_addr, aligned_size); } return result; } From d3c25281e95468c5276f12f1b03b0a17ddf707f6 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:15:19 -0500 Subject: [PATCH 24/27] Add proper function names (#4231) Found these with the help of @red-prig --- src/core/libraries/np/np_partner.cpp | 29 +++++++++------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/core/libraries/np/np_partner.cpp b/src/core/libraries/np/np_partner.cpp index 447144344..bf60772d9 100644 --- a/src/core/libraries/np/np_partner.cpp +++ b/src/core/libraries/np/np_partner.cpp @@ -14,10 +14,7 @@ namespace Libraries::Np::NpPartner { static bool g_library_init = false; std::mutex g_library_mutex{}; -/** - * Terminates the library - */ -s32 PS4_SYSV_ABI Func_A4CC5784DA33517F() { +s32 PS4_SYSV_ABI sceNpEAAccessTerminate() { LOG_ERROR(Lib_NpPartner, "(STUBBED) called"); if (!g_library_init) { return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED; @@ -27,10 +24,7 @@ s32 PS4_SYSV_ABI Func_A4CC5784DA33517F() { return ORBIS_OK; } -/** - * Aborts requests started by Func_F8E9DB52CD425743 - */ -s32 PS4_SYSV_ABI Func_A507D84D91F39CC7() { +s32 PS4_SYSV_ABI sceNpHasEAAccessSubscriptionAbortRequest() { LOG_ERROR(Lib_NpPartner, "(STUBBED) called"); if (!g_library_init) { return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED; @@ -39,20 +33,15 @@ s32 PS4_SYSV_ABI Func_A507D84D91F39CC7() { return ORBIS_OK; } -/** - * Initializes the library - */ -s32 PS4_SYSV_ABI Func_EC2C48E74FF19429() { +s32 PS4_SYSV_ABI sceNpEAAccessInitialize() { LOG_ERROR(Lib_NpPartner, "(STUBBED) called"); g_library_init = true; // Also retrieves and sends compiled SDK version to server. return ORBIS_OK; } -/** - * Creates an NP request to determine if the user has a subscription to EA's services. - */ -s32 PS4_SYSV_ABI Func_F8E9DB52CD425743(UserService::OrbisUserServiceUserId user_id, bool* result) { +s32 PS4_SYSV_ABI sceNpHasEAAccessSubscription(UserService::OrbisUserServiceUserId user_id, + bool* result) { LOG_ERROR(Lib_NpPartner, "(STUBBED) called"); if (!g_library_init) { return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED; @@ -71,13 +60,13 @@ s32 PS4_SYSV_ABI Func_F8E9DB52CD425743(UserService::OrbisUserServiceUserId user_ void RegisterLib(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("pMxXhNozUX8", "libSceNpPartner001", 1, "libSceNpPartner001", - Func_A4CC5784DA33517F); + sceNpEAAccessTerminate); LIB_FUNCTION("pQfYTZHznMc", "libSceNpPartner001", 1, "libSceNpPartner001", - Func_A507D84D91F39CC7); + sceNpHasEAAccessSubscriptionAbortRequest); LIB_FUNCTION("7CxI50-xlCk", "libSceNpPartner001", 1, "libSceNpPartner001", - Func_EC2C48E74FF19429); + sceNpEAAccessInitialize); LIB_FUNCTION("+OnbUs1CV0M", "libSceNpPartner001", 1, "libSceNpPartner001", - Func_F8E9DB52CD425743); + sceNpHasEAAccessSubscription); }; } // namespace Libraries::Np::NpPartner \ No newline at end of file From e64f038ad6a0f78cd59754134b2b20c3b8ab79f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 7 Apr 2026 18:01:37 +0200 Subject: [PATCH 25/27] Give MS priority over LOD in Image opcodes (#4207) --- .../ir/passes/resource_tracking_pass.cpp | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 0b256a349..52942338b 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -1089,7 +1089,8 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) { } const auto image_handle = inst.Arg(0); - const auto& image_res = info.images[image_handle.U32() & 0xFFFF]; + const auto binding_index = image_handle.U32() & 0xFFFF; + const auto& image_res = info.images[binding_index]; auto image = image_res.GetSharp(info); // Sample instructions must be handled separately using address register data. @@ -1098,7 +1099,7 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) { } IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; - const auto inst_info = inst.Flags(); + auto inst_info = inst.Flags(); const auto view_type = image.GetViewType(image_res.is_array); // Now that we know the image type, adjust texture coordinate vector. @@ -1109,8 +1110,26 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) { return {body->Arg(0), body->Arg(1)}; case AmdGpu::ImageType::Color1DArray: // x, slice, [lod] case AmdGpu::ImageType::Color2D: // x, y, [lod] - case AmdGpu::ImageType::Color2DMsaa: // x, y. (sample is passed on different argument) return {ir.CompositeConstruct(body->Arg(0), body->Arg(1)), body->Arg(2)}; + case AmdGpu::ImageType::Color2DMsaa: // x, y. (sample is passed on different argument) + { + auto skip_lod = false; + if (inst_info.has_lod) { + const auto mipid = body->Arg(2); + if (mipid.IsImmediate() && mipid.U32() == 0) { + // if image_x_mip refers to a MSAA image, and mipid is 0, it is safe to be + // skipped and fragid is taken from the next arg + LOG_WARNING(Render_Recompiler, "Encountered a _mip instruction with MSAA " + "image, and mipid is 0, skipping LoD"); + inst_info.has_lod.Assign(false); + skip_lod = true; + } else { + UNREACHABLE_MSG( + "Encountered a _mip instruction with MSAA image, and mipid is non-zero"); + } + } + return {ir.CompositeConstruct(body->Arg(0), body->Arg(1)), body->Arg(skip_lod ? 3 : 2)}; + } case AmdGpu::ImageType::Color2DArray: // x, y, slice, [lod] case AmdGpu::ImageType::Color2DMsaaArray: // x, y, slice. (sample is passed on different // argument) From c11fa6ff811dc3c498536fa34eccdd25b6342e29 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:08:53 +0200 Subject: [PATCH 26/27] Update scePadGetInfo (#4234) * Add OrbisPadInfo struct and return correct controller colours * oof --- src/core/libraries/pad/pad.cpp | 17 ++++++++--------- src/core/libraries/pad/pad.h | 14 +++++++++++++- src/input/controller.cpp | 4 ++++ src/input/controller.h | 1 + 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index a24d4bc1a..ca759083b 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -177,19 +177,18 @@ int PS4_SYSV_ABI scePadGetIdleCount() { return ORBIS_OK; } -int PS4_SYSV_ABI scePadGetInfo(u32* data) { +int PS4_SYSV_ABI scePadGetInfo(OrbisPadInfo* 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; // ? + auto& controllers = *Common::Singleton::Instance(); + auto col = controllers[0]->GetLightBarRGB(); + std::memset(data, 0, sizeof(OrbisPadInfo)); + data->unk1 = 0x1; + data->pad_handle = 1; + data->unk3 = 0x00000101; + data->colour = col.r + (col.g << 8) + (col.b << 16); return ORBIS_OK; } diff --git a/src/core/libraries/pad/pad.h b/src/core/libraries/pad/pad.h index 2f4cbcc6a..d0a9dfd34 100644 --- a/src/core/libraries/pad/pad.h +++ b/src/core/libraries/pad/pad.h @@ -253,6 +253,18 @@ struct OrbisPadVibrationParam { u8 smallMotor; }; +struct OrbisPadInfo { + u32 unk1; + u32 unk2; + u32 pad_handle; + u32 unk3; + u32 unk4; + u32 unk5; + u32 colour; + u32 unk6; + u32 unk[30]; +}; + int PS4_SYSV_ABI scePadClose(s32 handle); int PS4_SYSV_ABI scePadConnectPort(); int PS4_SYSV_ABI scePadDeviceClassGetExtendedInformation( @@ -280,7 +292,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(u32* data); +int PS4_SYSV_ABI scePadGetInfo(OrbisPadInfo* data); int PS4_SYSV_ABI scePadGetInfoByPortType(); int PS4_SYSV_ABI scePadGetLicenseControllerInformation(); int PS4_SYSV_ABI scePadGetMotionSensorPosition(); diff --git a/src/input/controller.cpp b/src/input/controller.cpp index c1ba584e3..04e845bf4 100644 --- a/src/input/controller.cpp +++ b/src/input/controller.cpp @@ -157,6 +157,10 @@ void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) { } } +Colour GameController::GetLightBarRGB() { + return colour; +} + void GameController::PollLightColour() { if (m_sdl_gamepad != nullptr) { SDL_SetGamepadLED(m_sdl_gamepad, colour.r, colour.g, colour.b); diff --git a/src/input/controller.h b/src/input/controller.h index 1c711c488..fcd1308f4 100644 --- a/src/input/controller.h +++ b/src/input/controller.h @@ -126,6 +126,7 @@ public: void UpdateAcceleration(const float acceleration[3]); void UpdateAxisSmoothing(); void SetLightBarRGB(u8 r, u8 g, u8 b); + Colour GetLightBarRGB(); void PollLightColour(); bool SetVibration(u8 smallMotor, u8 largeMotor); void SetTouchpadState(int touchIndex, bool touchDown, float x, float y); From 36e11fcce58d04a3c01e850e9659d2f10fac0532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Wed, 8 Apr 2026 12:01:23 +0200 Subject: [PATCH 27/27] GS: Only use number of comps if greater than VERT_ITEMSIZE (#4236) --- src/shader_recompiler/ir/passes/ring_access_elimination.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shader_recompiler/ir/passes/ring_access_elimination.cpp b/src/shader_recompiler/ir/passes/ring_access_elimination.cpp index a7a23a726..2a54002f0 100644 --- a/src/shader_recompiler/ir/passes/ring_access_elimination.cpp +++ b/src/shader_recompiler/ir/passes/ring_access_elimination.cpp @@ -105,7 +105,7 @@ void RingAccessElimination(const IR::Program& program, const RuntimeInfo& runtim output_vertices = info.gs_copy_data.output_vertices; } u32 dwords_per_vertex = gs_info.out_vertex_data_size; - if (info.gs_copy_data.num_comps && info.gs_copy_data.num_comps != dwords_per_vertex) { + if (info.gs_copy_data.num_comps && info.gs_copy_data.num_comps > dwords_per_vertex) { LOG_WARNING(Render_Vulkan, "VERT_ITEMSIZE {} is different than actual number of dwords per vertex {}", dwords_per_vertex, info.gs_copy_data.num_comps);