diff --git a/.gitmodules b/.gitmodules index 38aed89a0..43ba2a7c1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -108,7 +108,7 @@ branch = dist [submodule "externals/MoltenVK"] path = externals/MoltenVK - url = https://github.com/KhronosGroup/MoltenVK.git + url = https://github.com/shadPS4-emu/ext-MoltenVK.git shallow = true [submodule "externals/json"] path = externals/json diff --git a/CMakeLists.txt b/CMakeLists.txt index b75da04d6..d26581790 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,7 +203,7 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") set(EMULATOR_VERSION_MINOR "12") -set(EMULATOR_VERSION_PATCH "1") +set(EMULATOR_VERSION_PATCH "6") set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}") @@ -539,6 +539,10 @@ set(RANDOM_LIB src/core/libraries/random/random.cpp set(USBD_LIB src/core/libraries/usbd/usbd.cpp src/core/libraries/usbd/usbd.h src/core/libraries/usbd/usb_backend.h + src/core/libraries/usbd/emulated/dimensions.cpp + src/core/libraries/usbd/emulated/dimensions.h + src/core/libraries/usbd/emulated/infinity.cpp + src/core/libraries/usbd/emulated/infinity.h src/core/libraries/usbd/emulated/skylander.cpp src/core/libraries/usbd/emulated/skylander.h ) diff --git a/dist/net.shadps4.shadPS4.metainfo.xml b/dist/net.shadps4.shadPS4.metainfo.xml index ae07e79d8..5798876f6 100644 --- a/dist/net.shadps4.shadPS4.metainfo.xml +++ b/dist/net.shadps4.shadPS4.metainfo.xml @@ -37,6 +37,9 @@ Game + + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.12.5 + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.12.0 diff --git a/documents/building-windows.md b/documents/building-windows.md index aa7213abc..88c5b6830 100644 --- a/documents/building-windows.md +++ b/documents/building-windows.md @@ -36,16 +36,11 @@ Go through the Git for Windows installation as normal 1. Open up Visual Studio, select `Open a local folder` and select the folder with the shadPS4 source code. The folder should contain `CMakeLists.txt` 2. Change Clang x64 Debug to Clang x64 Release if you want a regular, non-debug build. -3. If you want to build shadPS4 with the Qt Gui, simply select Clang x64 Release with Qt instead. -4. Change the project to build to shadps4.exe -5. Build -> Build All +3. Change the project to build to shadps4.exe +4. Build -> Build All Your shadps4.exe will be in `C:\path\to\source\Build\x64-Clang-Release\` -To automatically populate the necessary files to run shadPS4.exe, run in a command prompt or terminal: -`C:\Qt\\msvc2022_64\bin\windeployqt6.exe "C:\path\to\shadps4.exe"` -(Change Qt path if you've installed it to non-default path) - ## Option 2: MSYS2/MinGW > [!IMPORTANT] @@ -73,7 +68,6 @@ ARM64-based computers, follow: 1. Open "MSYS2 CLANGARM64" from your new applications 2. Run `pacman -Syu`, let it complete; 3. Run `pacman -S --needed git mingw-w64-clang-aarch64-binutils mingw-w64-clang-aarch64-clang mingw-w64-clang-aarch64-rapidjson mingw-w64-clang-aarch64-cmake mingw-w64-clang-aarch64-ninja mingw-w64-clang-aarch64-ffmpeg` - 1. Optional (Qt only): run `pacman -S --needed mingw-w64-clang-aarch64-qt6-base mingw-w64-clang-aarch64-qt6-tools mingw-w64-clang-aarch64-qt6-multimedia` 4. Run `git clone --depth 1 --recursive https://github.com/shadps4-emu/shadPS4` 5. Run `cd shadPS4` 6. Run `cmake -S . -B build -DCMAKE_C_COMPILER="clang.exe" -DCMAKE_CXX_COMPILER="clang++.exe" -DCMAKE_CXX_FLAGS="-O2 -march=native"` diff --git a/externals/MoltenVK b/externals/MoltenVK index b23d42534..f168dec05 160000 --- a/externals/MoltenVK +++ b/externals/MoltenVK @@ -1 +1 @@ -Subproject commit b23d42534622cd9926fe526fec1b7f8795a2853c +Subproject commit f168dec05998ab0ca09a400bab6831a95c0bdb2e diff --git a/shell.nix b/shell.nix index 50336d1b2..611095268 100644 --- a/shell.nix +++ b/shell.nix @@ -6,42 +6,43 @@ with import (fetchTarball "https://github.com/nixos/nixpkgs/archive/cfd19cdc5468 pkgs.mkShell { name = "shadps4-build-env"; - nativeBuildInputs = [ - pkgs.llvmPackages_18.clang - pkgs.cmake - pkgs.pkg-config - pkgs.git + nativeBuildInputs = with pkgs; [ + llvmPackages_18.clang + cmake + pkg-config + git + util-linux ]; - buildInputs = [ - pkgs.alsa-lib - pkgs.libpulseaudio - pkgs.openal - pkgs.zlib - pkgs.libedit - pkgs.udev - pkgs.libevdev - pkgs.SDL2 - pkgs.jack2 - pkgs.sndio + buildInputs = with pkgs; [ + alsa-lib + libpulseaudio + openal + zlib + libedit + udev + libevdev + SDL2 + jack2 + sndio - pkgs.vulkan-headers - pkgs.vulkan-utility-libraries - pkgs.vulkan-tools + vulkan-headers + vulkan-utility-libraries + vulkan-tools - pkgs.ffmpeg - pkgs.fmt - pkgs.glslang - pkgs.libxkbcommon - pkgs.wayland - pkgs.xorg.libxcb - pkgs.xorg.xcbutil - pkgs.xorg.xcbutilkeysyms - pkgs.xorg.xcbutilwm - pkgs.sdl3 - pkgs.stb - pkgs.wayland-protocols - pkgs.libpng + ffmpeg + fmt + glslang + libxkbcommon + wayland + xorg.libxcb + xorg.xcbutil + xorg.xcbutilkeysyms + xorg.xcbutilwm + sdl3 + stb + wayland-protocols + libpng ]; shellHook = '' diff --git a/src/common/elf_info.h b/src/common/elf_info.h index 2f2f150b2..0b2589e95 100644 --- a/src/common/elf_info.h +++ b/src/common/elf_info.h @@ -68,6 +68,7 @@ class ElfInfo { std::string app_ver{}; u32 firmware_ver = 0; u32 raw_firmware_ver = 0; + u32 sdk_ver = 0; PSFAttributes psf_attributes{}; std::filesystem::path splash_path{}; @@ -117,6 +118,11 @@ public: return raw_firmware_ver; } + [[nodiscard]] u32 CompiledSdkVer() const { + ASSERT(initialized); + return sdk_ver; + } + [[nodiscard]] const PSFAttributes& GetPSFAttributes() const { ASSERT(initialized); return psf_attributes; diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index 3f2d94cbf..3f063ea76 100644 --- a/src/core/address_space.cpp +++ b/src/core/address_space.cpp @@ -6,6 +6,7 @@ #include "common/arch.h" #include "common/assert.h" #include "common/config.h" +#include "common/elf_info.h" #include "common/error.h" #include "core/address_space.h" #include "core/libraries/kernel/memory.h" @@ -103,8 +104,8 @@ struct AddressSpace::Impl { GetSystemInfo(&sys_info); u64 alignment = sys_info.dwAllocationGranularity; - // Determine the host OS build number - // Retrieve module handle for ntdll + // Older Windows builds have a severe performance issue with VirtualAlloc2. + // We need to get the host's Windows version, then determine if it needs a workaround. auto ntdll_handle = GetModuleHandleW(L"ntdll.dll"); ASSERT_MSG(ntdll_handle, "Failed to retrieve ntdll handle"); @@ -120,12 +121,20 @@ struct AddressSpace::Impl { u64 supported_user_max = USER_MAX; // This is the build number for Windows 11 22H2 static constexpr s32 AffectedBuildNumber = 22621; - if (os_version_info.dwBuildNumber <= AffectedBuildNumber) { - // Older Windows builds have an issue with VirtualAlloc2 on higher addresses. - // To prevent regressions, limit the maximum address we reserve for this platform. - supported_user_max = 0x11000000000ULL; - LOG_WARNING(Core, "Windows 10 detected, reducing user max to {:#x} to avoid problems", - supported_user_max); + + // Higher PS4 firmware versions prevent higher address mappings too. + s32 sdk_ver = Common::ElfInfo::Instance().CompiledSdkVer(); + if (os_version_info.dwBuildNumber <= AffectedBuildNumber || + sdk_ver >= Common::ElfInfo::FW_30) { + supported_user_max = 0x10000000000ULL; + // Only log the message if we're restricting the user max due to operating system. + // Since higher compiled SDK versions also get reduced max, we don't need to log there. + if (sdk_ver < Common::ElfInfo::FW_30) { + LOG_WARNING( + Core, + "Older Windows version detected, reducing user max to {:#x} to avoid problems", + supported_user_max); + } } // Determine the free address ranges we can access. diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index d5cbe6074..7ded1f33e 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -640,17 +640,29 @@ s32 PS4_SYSV_ABI posix_stat(const char* path, OrbisKernelStat* sb) { *__Error() = POSIX_ENOENT; return -1; } + + // get the difference between file clock and system clock + const auto now_sys = std::chrono::system_clock::now(); + const auto now_file = std::filesystem::file_time_type::clock::now(); + // calculate the file modified time + const auto mtime = std::filesystem::last_write_time(path_name); + const auto mtimestamp = now_sys + (mtime - now_file); + if (std::filesystem::is_directory(path_name)) { sb->st_mode = 0000777u | 0040000u; sb->st_size = 65536; sb->st_blksize = 65536; sb->st_blocks = 128; + sb->st_mtim.tv_sec = + std::chrono::duration_cast(mtimestamp.time_since_epoch()).count(); // TODO incomplete } else { sb->st_mode = 0000777u | 0100000u; sb->st_size = static_cast(std::filesystem::file_size(path_name)); sb->st_blksize = 512; sb->st_blocks = (sb->st_size + 511) / 512; + sb->st_mtim.tv_sec = + std::chrono::duration_cast(mtimestamp.time_since_epoch()).count(); // TODO incomplete } diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index 054f34c15..6594bfab2 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -236,15 +236,24 @@ s32 PS4_SYSV_ABI sceKernelSetGPO() { return ORBIS_OK; } +s32 PS4_SYSV_ABI sceKernelGetAllowedSdkVersionOnSystem(s32* ver) { + if (ver == nullptr) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + // Returns the highest game SDK version this PS4 allows. + *ver = CURRENT_FIRMWARE_VERSION | 0xfff; + LOG_INFO(Lib_Kernel, "called, returned sw version: {}", *ver); + return ORBIS_OK; +} + s32 PS4_SYSV_ABI sceKernelGetSystemSwVersion(SwVersionStruct* ret) { if (ret == nullptr) { - return ORBIS_OK; // but why? + return ORBIS_OK; } - ASSERT(ret->struct_size == 40); - u32 fake_fw = Common::ElfInfo::Instance().RawFirmwareVer(); + u32 fake_fw = CURRENT_FIRMWARE_VERSION; ret->hex_representation = fake_fw; std::snprintf(ret->text_representation, 28, "%2x.%03x.%03x", fake_fw >> 0x18, - fake_fw >> 0xc & 0xfff, fake_fw & 0xfff); // why %2x? + fake_fw >> 0xc & 0xfff, fake_fw & 0xfff); LOG_INFO(Lib_Kernel, "called, returned sw version: {}", ret->text_representation); return ORBIS_OK; } @@ -257,9 +266,13 @@ const char** PS4_SYSV_ABI getargv() { return entry_params.argv; } -s32 PS4_SYSV_ABI get_authinfo(int pid, AuthInfoData* p2) { +s32 PS4_SYSV_ABI get_authinfo(s32 pid, AuthInfoData* p2) { LOG_WARNING(Lib_Kernel, "(STUBBED) called, pid: {}", pid); - if ((pid != 0) && (pid != GLOBAL_PID)) { + if (p2 == nullptr) { + *Kernel::__Error() = POSIX_EPERM; + return -1; + } + if (pid != 0 && pid != GLOBAL_PID) { *Kernel::__Error() = POSIX_ESRCH; return -1; } @@ -269,6 +282,22 @@ s32 PS4_SYSV_ABI get_authinfo(int pid, AuthInfoData* p2) { return ORBIS_OK; } +s32 PS4_SYSV_ABI sceKernelGetAppInfo(s32 pid, OrbisKernelAppInfo* app_info) { + LOG_WARNING(Lib_Kernel, "(STUBBED) called, pid: {}", pid); + if (pid != GLOBAL_PID) { + return ORBIS_KERNEL_ERROR_EPERM; + } + if (app_info == nullptr) { + return ORBIS_OK; + } + + auto& game_info = Common::ElfInfo::Instance(); + *app_info = {}; + app_info->has_param_sfo = 1; + strncpy(app_info->cusa_name, game_info.GameSerial().data(), 10); + return ORBIS_OK; +} + void RegisterLib(Core::Loader::SymbolsResolver* sym) { service_thread = std::jthread{KernelServiceThread}; @@ -285,8 +314,10 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { LIB_OBJ("f7uOxY9mM1U", "libkernel", 1, "libkernel", &g_stack_chk_guard); LIB_FUNCTION("D4yla3vx4tY", "libkernel", 1, "libkernel", sceKernelError); + LIB_FUNCTION("YeU23Szo3BM", "libkernel", 1, "libkernel", sceKernelGetAllowedSdkVersionOnSystem); LIB_FUNCTION("Mv1zUObHvXI", "libkernel", 1, "libkernel", sceKernelGetSystemSwVersion); LIB_FUNCTION("igMefp4SAv0", "libkernel", 1, "libkernel", get_authinfo); + LIB_FUNCTION("G-MYv5erXaU", "libkernel", 1, "libkernel", sceKernelGetAppInfo); LIB_FUNCTION("PfccT7qURYE", "libkernel", 1, "libkernel", kernel_ioctl); LIB_FUNCTION("wW+k21cmbwQ", "libkernel", 1, "libkernel", kernel_ioctl); LIB_FUNCTION("JGfTMBOdUJo", "libkernel", 1, "libkernel", sceKernelGetFsSandboxRandomWord); diff --git a/src/core/libraries/kernel/kernel.h b/src/core/libraries/kernel/kernel.h index 74c457dc5..91fac4c71 100644 --- a/src/core/libraries/kernel/kernel.h +++ b/src/core/libraries/kernel/kernel.h @@ -36,6 +36,8 @@ struct OrbisWrapperImpl { #define ORBIS(func) (Libraries::Kernel::OrbisWrapperImpl::wrap) +#define CURRENT_FIRMWARE_VERSION 0x13020011 + s32* PS4_SYSV_ABI __Error(); struct SwVersionStruct { @@ -51,6 +53,30 @@ struct AuthInfoData { u64 ucred[8]; }; +struct OrbisKernelTitleWorkaround { + s32 version; + s32 align; + u64 ids[2]; +}; + +struct OrbisKernelAppInfo { + s32 app_id; + s32 mmap_flags; + s32 attribute_exe; + s32 attribute2; + char cusa_name[10]; + u8 debug_level; + u8 slv_flags; + u8 mini_app_dmem_flags; + u8 render_mode; + u8 mdbg_out; + u8 required_hdcp_type; + u64 preload_prx_flags; + s32 attribute1; + s32 has_param_sfo; + OrbisKernelTitleWorkaround title_workaround; +}; + void RegisterLib(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index 0e168e43a..02da041c3 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -21,8 +21,22 @@ s32 PS4_SYSV_ABI sceKernelIsNeoMode() { Common::ElfInfo::Instance().GetPSFAttributes().support_neo_mode; } +s32 PS4_SYSV_ABI sceKernelHasNeoMode() { + return Config::isNeoModeConsole(); +} + +s32 PS4_SYSV_ABI sceKernelGetMainSocId() { + // These hardcoded values are based on hardware observations. + // Different models of PS4/PS4 Pro likely return slightly different values. + LOG_DEBUG(Lib_Kernel, "called"); + if (Config::isNeoModeConsole()) { + return 0x740f30; + } + return 0x710f10; +} + s32 PS4_SYSV_ABI sceKernelGetCompiledSdkVersion(s32* ver) { - s32 version = Common::ElfInfo::Instance().RawFirmwareVer(); + s32 version = Common::ElfInfo::Instance().CompiledSdkVer(); *ver = version; return (version >= 0) ? ORBIS_OK : ORBIS_KERNEL_ERROR_EINVAL; } @@ -31,6 +45,11 @@ s32 PS4_SYSV_ABI sceKernelGetCpumode() { return 0; } +s32 PS4_SYSV_ABI sceKernelGetCurrentCpu() { + LOG_DEBUG(Lib_Kernel, "called"); + return 0; +} + void* PS4_SYSV_ABI sceKernelGetProcParam() { auto* linker = Common::Singleton::Instance(); return linker->GetProcParam(); @@ -208,7 +227,10 @@ void RegisterProcess(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("xeu-pV8wkKs", "libkernel", 1, "libkernel", sceKernelIsInSandbox); LIB_FUNCTION("WB66evu8bsU", "libkernel", 1, "libkernel", sceKernelGetCompiledSdkVersion); LIB_FUNCTION("WslcK1FQcGI", "libkernel", 1, "libkernel", sceKernelIsNeoMode); + LIB_FUNCTION("rNRtm1uioyY", "libkernel", 1, "libkernel", sceKernelHasNeoMode); + LIB_FUNCTION("0vTn5IDMU9A", "libkernel", 1, "libkernel", sceKernelGetMainSocId); LIB_FUNCTION("VOx8NGmHXTs", "libkernel", 1, "libkernel", sceKernelGetCpumode); + LIB_FUNCTION("g0VTBxfJyu0", "libkernel", 1, "libkernel", sceKernelGetCurrentCpu); LIB_FUNCTION("959qrazPIrg", "libkernel", 1, "libkernel", sceKernelGetProcParam); LIB_FUNCTION("wzvqT4UqKX8", "libkernel", 1, "libkernel", sceKernelLoadStartModule); LIB_FUNCTION("LwG8g3niqwA", "libkernel", 1, "libkernel", sceKernelDlsym); diff --git a/src/core/libraries/kernel/threads/condvar.cpp b/src/core/libraries/kernel/threads/condvar.cpp index 98f7397ad..9d429ed7d 100644 --- a/src/core/libraries/kernel/threads/condvar.cpp +++ b/src/core/libraries/kernel/threads/condvar.cpp @@ -354,6 +354,8 @@ void RegisterCond(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("Op8TBGY5KHg", "libkernel", 1, "libkernel", posix_pthread_cond_wait); LIB_FUNCTION("mkx2fVhNMsg", "libkernel", 1, "libkernel", posix_pthread_cond_broadcast); LIB_FUNCTION("2MOy+rUfuhQ", "libkernel", 1, "libkernel", posix_pthread_cond_signal); + LIB_FUNCTION("RXXqi4CtF8w", "libkernel", 1, "libkernel", posix_pthread_cond_destroy); + LIB_FUNCTION("27bAgiJmOh0", "libkernel", 1, "libkernel", posix_pthread_cond_timedwait); LIB_FUNCTION("mKoTx03HRWA", "libkernel", 1, "libkernel", posix_pthread_condattr_init); LIB_FUNCTION("dJcuQVn6-Iw", "libkernel", 1, "libkernel", posix_pthread_condattr_destroy); diff --git a/src/core/libraries/kernel/threads/mutex.cpp b/src/core/libraries/kernel/threads/mutex.cpp index 505bd0d9d..5d97c5dc1 100644 --- a/src/core/libraries/kernel/threads/mutex.cpp +++ b/src/core/libraries/kernel/threads/mutex.cpp @@ -442,6 +442,8 @@ void RegisterMutex(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("ltCfaGr2JGE", "libkernel", 1, "libkernel", posix_pthread_mutex_destroy); LIB_FUNCTION("dQHWEsJtoE4", "libkernel", 1, "libkernel", posix_pthread_mutexattr_init); LIB_FUNCTION("mDmgMOGVUqg", "libkernel", 1, "libkernel", posix_pthread_mutexattr_settype); + LIB_FUNCTION("HF7lK46xzjY", "libkernel", 1, "libkernel", posix_pthread_mutexattr_destroy); + LIB_FUNCTION("K-jXhbt2gn4", "libkernel", 1, "libkernel", posix_pthread_mutex_trylock); // Orbis LIB_FUNCTION("cmo1RIYva9o", "libkernel", 1, "libkernel", ORBIS(scePthreadMutexInit)); diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index 09e8b9558..8ab8b72c3 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -663,6 +663,9 @@ void RegisterThread(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("Z4QosVuAsA0", "libkernel", 1, "libkernel", posix_pthread_once); LIB_FUNCTION("EotR8a3ASf4", "libkernel", 1, "libkernel", posix_pthread_self); LIB_FUNCTION("OxhIB8LB-PQ", "libkernel", 1, "libkernel", posix_pthread_create); + LIB_FUNCTION("+U1R4WtXvoc", "libkernel", 1, "libkernel", posix_pthread_detach); + LIB_FUNCTION("7Xl257M4VNI", "libkernel", 1, "libkernel", posix_pthread_equal); + LIB_FUNCTION("h9CcP3J0oVM", "libkernel", 1, "libkernel", posix_pthread_join); LIB_FUNCTION("Jb2uGFMr688", "libkernel", 1, "libkernel", posix_pthread_getaffinity_np); LIB_FUNCTION("5KWrg7-ZqvE", "libkernel", 1, "libkernel", posix_pthread_setaffinity_np); LIB_FUNCTION("3eqs37G74-s", "libkernel", 1, "libkernel", posix_pthread_getthreadid_np); diff --git a/src/core/libraries/network/net.cpp b/src/core/libraries/network/net.cpp index d9ef76afc..97005813b 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -1285,7 +1285,8 @@ u16 PS4_SYSV_ABI sceNetNtohs(u16 net16) { int PS4_SYSV_ABI sceNetPoolCreate(const char* name, int size, int flags) { LOG_ERROR(Lib_Net, "(DUMMY) name = {} size = {} flags = {} ", std::string(name), size, flags); - return ORBIS_OK; + static s32 id = 1; + return id++; } int PS4_SYSV_ABI sceNetPoolDestroy() { diff --git a/src/core/libraries/usbd/emulated/dimensions.cpp b/src/core/libraries/usbd/emulated/dimensions.cpp new file mode 100644 index 000000000..272f2f649 --- /dev/null +++ b/src/core/libraries/usbd/emulated/dimensions.cpp @@ -0,0 +1,651 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "dimensions.h" + +#include +#include + +namespace Libraries::Usbd { + +static constexpr std::array COMMAND_KEY = {0x55, 0xFE, 0xF6, 0xB0, 0x62, 0xBF, 0x0B, 0x41, + 0xC9, 0xB3, 0x7C, 0xB4, 0x97, 0x3E, 0x29, 0x7B}; + +static constexpr std::array CHAR_CONSTANT = {0xB7, 0xD5, 0xD7, 0xE6, 0xE7, 0xBA, + 0x3C, 0xA8, 0xD8, 0x75, 0x47, 0x68, + 0xCF, 0x23, 0xE9, 0xFE, 0xAA}; + +static constexpr std::array PWD_CONSTANT = { + 0x28, 0x63, 0x29, 0x20, 0x43, 0x6F, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, + 0x20, 0x4C, 0x45, 0x47, 0x4F, 0x20, 0x32, 0x30, 0x31, 0x34, 0xAA, 0xAA}; + +DimensionsToypad::DimensionsToypad() {} + +void DimensionsToypad::LoadFigure(std::string file_name, u8 pad, u8 index) { + Common::FS::IOFile file(file_name, Common::FS::FileAccessMode::ReadWrite); + std::array data; + ASSERT(file.Read(data) == data.size()); + LoadDimensionsFigure(data, std::move(file), pad, index); +} + +u32 DimensionsToypad::LoadDimensionsFigure(const std::array& buf, + Common::FS::IOFile file, u8 pad, u8 index) { + std::lock_guard lock(m_dimensions_mutex); + + const u32 id = GetFigureId(buf); + + DimensionsFigure& figure = GetFigureByIndex(index); + figure.dimFile = std::move(file); + figure.id = id; + figure.pad = pad; + figure.index = index + 1; + figure.data = buf; + // When a figure is added to the toypad, respond to the game with the pad they were added to, + // their index, the direction (0x00 in byte 6 for added) and their UID + std::array figureChangeResponse = {0x56, 0x0b, figure.pad, 0x00, figure.index, + 0x00, buf[0], buf[1], buf[2], buf[4], + buf[5], buf[6], buf[7]}; + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figure_added_removed_responses.push(figureChangeResponse); + + return id; +} + +void DimensionsToypad::RemoveFigure(u8 pad, u8 index, bool fullRemove) { + std::lock_guard lock(m_dimensions_mutex); + + DimensionsFigure& figure = GetFigureByIndex(index); + if (figure.index == 255) + return; + + // When a figure is removed from the toypad, respond to the game with the pad they were removed + // from, their index, the direction (0x01 in byte 6 for removed) and their UID + if (fullRemove) { + std::array figureChangeResponse = { + 0x56, 0x0b, figure.pad, 0x00, figure.index, + 0x01, figure.data[0], figure.data[1], figure.data[2], figure.data[4], + figure.data[5], figure.data[6], figure.data[7]}; + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figure_added_removed_responses.push(figureChangeResponse); + figure.Save(); + figure.dimFile.Close(); + } + + figure.index = 255; + figure.pad = 255; + figure.id = 0; +} + +void DimensionsToypad::MoveFigure(u8 new_pad, u8 new_index, u8 old_pad, u8 old_index) { + if (old_index == new_index) { + // Don't bother removing and loading again, just send response to the game + CancelRemoveFigure(new_index); + return; + } + + // When moving figures between spaces on the toypad, remove any figure from the space they are + // moving to, then remove them from their current space, then load them to the space they are + // moving to + RemoveFigure(new_pad, new_index, true); + + DimensionsFigure& figure = GetFigureByIndex(old_index); + const std::array data = figure.data; + Common::FS::IOFile inFile = std::move(figure.dimFile); + + RemoveFigure(old_pad, old_index, false); + + LoadDimensionsFigure(data, std::move(inFile), new_pad, new_index); +} + +void DimensionsToypad::TempRemoveFigure(u8 index) { + std::lock_guard lock(m_dimensions_mutex); + + DimensionsFigure& figure = GetFigureByIndex(index); + if (figure.index == 255) + return; + + // Send a response to the game that the figure has been "Picked up" from existing slot, + // until either the movement is cancelled, or user chooses a space to move to + std::array figureChangeResponse = { + 0x56, 0x0b, figure.pad, 0x00, figure.index, + 0x01, figure.data[0], figure.data[1], figure.data[2], figure.data[4], + figure.data[5], figure.data[6], figure.data[7]}; + + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figure_added_removed_responses.push(figureChangeResponse); +} + +void DimensionsToypad::CancelRemoveFigure(u8 index) { + std::lock_guard lock(m_dimensions_mutex); + + DimensionsFigure& figure = GetFigureByIndex(index); + if (figure.index == 255) + return; + + // Cancel the previous movement of the figure + std::array figureChangeResponse = { + 0x56, 0x0b, figure.pad, 0x00, figure.index, + 0x00, figure.data[0], figure.data[1], figure.data[2], figure.data[4], + figure.data[5], figure.data[6], figure.data[7]}; + + figureChangeResponse[13] = GenerateChecksum(figureChangeResponse, 13); + m_figure_added_removed_responses.push(figureChangeResponse); +} + +u8 DimensionsToypad::GenerateChecksum(const std::array& data, u32 num_of_bytes) { + int checksum = 0; + ASSERT(num_of_bytes <= data.size()); + for (u8 i = 0; i < num_of_bytes; i++) { + checksum += data[i]; + } + return (checksum & 0xFF); +} + +void DimensionsToypad::GetBlankResponse(u8 type, u8 sequence, std::array& reply_buf) { + reply_buf[0] = 0x55; + reply_buf[1] = type; + reply_buf[2] = sequence; + reply_buf[3] = GenerateChecksum(reply_buf, 3); +} + +void DimensionsToypad::GenerateRandomNumber(const u8* buf, u8 sequence, + std::array& reply_buf) { + // Decrypt payload into an 8 byte array + const std::array value = Decrypt(buf, std::nullopt); + // Seed is the first 4 bytes (little endian) of the decrypted payload + const u32 seed = (u32&)value[0]; + // Confirmation is the second 4 bytes (big endian) of the decrypted payload + // const u32 conf = (u32be&)value[4]; + // Initialize rng using the seed from decrypted payload + InitializeRNG(seed); + // Encrypt 8 bytes, first 4 bytes is the decrypted confirmation from payload, 2nd 4 bytes are + // blank + std::array value_to_encrypt = {value[4], value[5], value[6], value[7], 0, 0, 0, 0}; + const std::array encrypted = Encrypt(value_to_encrypt.data(), std::nullopt); + reply_buf[0] = 0x55; + reply_buf[1] = 0x09; + reply_buf[2] = sequence; + // Copy encrypted value to response data + std::memcpy(&reply_buf[3], encrypted.data(), encrypted.size()); + reply_buf[11] = GenerateChecksum(reply_buf, 11); +} + +void DimensionsToypad::InitializeRNG(u32 seed) { + m_random_a = 0xF1EA5EED; + m_random_b = seed; + m_random_c = seed; + m_random_d = seed; + + for (int i = 0; i < 42; i++) { + GetNext(); + } +} + +u32 DimensionsToypad::GetNext() { + const u32 e = m_random_a - std::rotl(m_random_b, 21); + m_random_a = m_random_b ^ std::rotl(m_random_c, 19); + m_random_b = m_random_c + std::rotl(m_random_d, 6); + m_random_c = m_random_d + e; + m_random_d = e + m_random_a; + return m_random_d; +} + +std::array DimensionsToypad::Decrypt(const u8* buf, std::optional> key) { + // Value to decrypt is separated in to two little endian 32 bit unsigned integers + u32 data_one = u32(buf[0]) | (u32(buf[1]) << 8) | (u32(buf[2]) << 16) | (u32(buf[3]) << 24); + u32 data_two = u32(buf[4]) | (u32(buf[5]) << 8) | (u32(buf[6]) << 16) | (u32(buf[7]) << 24); + + // Use the key as 4 32 bit little endian unsigned integers + u32 key_one; + u32 key_two; + u32 key_three; + u32 key_four; + + if (key) { + key_one = u32(key.value()[0]) | (u32(key.value()[1]) << 8) | (u32(key.value()[2]) << 16) | + (u32(key.value()[3]) << 24); + key_two = u32(key.value()[4]) | (u32(key.value()[5]) << 8) | (u32(key.value()[6]) << 16) | + (u32(key.value()[7]) << 24); + key_three = u32(key.value()[8]) | (u32(key.value()[9]) << 8) | + (u32(key.value()[10]) << 16) | (u32(key.value()[11]) << 24); + key_four = u32(key.value()[12]) | (u32(key.value()[13]) << 8) | + (u32(key.value()[14]) << 16) | (u32(key.value()[15]) << 24); + } else { + key_one = u32(COMMAND_KEY[0]) | (u32(COMMAND_KEY[1]) << 8) | (u32(COMMAND_KEY[2]) << 16) | + (u32(COMMAND_KEY[3]) << 24); + key_two = u32(COMMAND_KEY[4]) | (u32(COMMAND_KEY[5]) << 8) | (u32(COMMAND_KEY[6]) << 16) | + (u32(COMMAND_KEY[7]) << 24); + key_three = u32(COMMAND_KEY[8]) | (u32(COMMAND_KEY[9]) << 8) | + (u32(COMMAND_KEY[10]) << 16) | (u32(COMMAND_KEY[11]) << 24); + key_four = u32(COMMAND_KEY[12]) | (u32(COMMAND_KEY[13]) << 8) | + (u32(COMMAND_KEY[14]) << 16) | (u32(COMMAND_KEY[15]) << 24); + } + + u32 sum = 0xC6EF3720; + constexpr u32 delta = 0x9E3779B9; + + for (int i = 0; i < 32; i++) { + data_two -= + (((data_one << 4) + key_three) ^ (data_one + sum) ^ ((data_one >> 5) + key_four)); + data_one -= (((data_two << 4) + key_one) ^ (data_two + sum) ^ ((data_two >> 5) + key_two)); + sum -= delta; + } + + ASSERT_MSG(sum == 0, "Decryption failed, sum inequal to 0"); + + std::array decrypted = {u8(data_one & 0xFF), u8((data_one >> 8) & 0xFF), + u8((data_one >> 16) & 0xFF), u8((data_one >> 24) & 0xFF), + u8(data_two & 0xFF), u8((data_two >> 8) & 0xFF), + u8((data_two >> 16) & 0xFF), u8((data_two >> 24) & 0xFF)}; + return decrypted; +} + +std::array DimensionsToypad::Encrypt(const u8* buf, std::optional> key) { + // Value to encrypt is separated in to two little endian 32 bit unsigned integers + u32 data_one = u32(buf[0]) | (u32(buf[1]) << 8) | (u32(buf[2]) << 16) | (u32(buf[3]) << 24); + u32 data_two = u32(buf[4]) | (u32(buf[5]) << 8) | (u32(buf[6]) << 16) | (u32(buf[7]) << 24); + + // Use the key as 4 32 bit little endian unsigned integers + u32 key_one; + u32 key_two; + u32 key_three; + u32 key_four; + + if (key) { + key_one = u32(key.value()[0]) | (u32(key.value()[1]) << 8) | (u32(key.value()[2]) << 16) | + (u32(key.value()[3]) << 24); + key_two = u32(key.value()[4]) | (u32(key.value()[5]) << 8) | (u32(key.value()[6]) << 16) | + (u32(key.value()[7]) << 24); + key_three = u32(key.value()[8]) | (u32(key.value()[9]) << 8) | + (u32(key.value()[10]) << 16) | (u32(key.value()[11]) << 24); + key_four = u32(key.value()[12]) | (u32(key.value()[13]) << 8) | + (u32(key.value()[14]) << 16) | (u32(key.value()[15]) << 24); + } else { + key_one = u32(COMMAND_KEY[0]) | (u32(COMMAND_KEY[1]) << 8) | (u32(COMMAND_KEY[2]) << 16) | + (u32(COMMAND_KEY[3]) << 24); + key_two = u32(COMMAND_KEY[4]) | (u32(COMMAND_KEY[5]) << 8) | (u32(COMMAND_KEY[6]) << 16) | + (u32(COMMAND_KEY[7]) << 24); + key_three = u32(COMMAND_KEY[8]) | (u32(COMMAND_KEY[9]) << 8) | + (u32(COMMAND_KEY[10]) << 16) | (u32(COMMAND_KEY[11]) << 24); + key_four = u32(COMMAND_KEY[12]) | (u32(COMMAND_KEY[13]) << 8) | + (u32(COMMAND_KEY[14]) << 16) | (u32(COMMAND_KEY[15]) << 24); + } + + u32 sum = 0; + u32 delta = 0x9E3779B9; + + for (int i = 0; i < 32; i++) { + sum += delta; + data_one += (((data_two << 4) + key_one) ^ (data_two + sum) ^ ((data_two >> 5) + key_two)); + data_two += + (((data_one << 4) + key_three) ^ (data_one + sum) ^ ((data_one >> 5) + key_four)); + } + + std::array encrypted = {u8(data_one & 0xFF), u8((data_one >> 8) & 0xFF), + u8((data_one >> 16) & 0xFF), u8((data_one >> 24) & 0xFF), + u8(data_two & 0xFF), u8((data_two >> 8) & 0xFF), + u8((data_two >> 16) & 0xFF), u8((data_two >> 24) & 0xFF)}; + return encrypted; +} + +std::array DimensionsToypad::GenerateFigureKey(const std::array& buf) { + std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; + + u32 scrambleA = Scramble(uid, 3); + u32 scrambleB = Scramble(uid, 4); + u32 scrambleC = Scramble(uid, 5); + u32 scrambleD = Scramble(uid, 6); + + return { + u8((scrambleA >> 24) & 0xFF), u8((scrambleA >> 16) & 0xFF), u8((scrambleA >> 8) & 0xFF), + u8(scrambleA & 0xFF), u8((scrambleB >> 24) & 0xFF), u8((scrambleB >> 16) & 0xFF), + u8((scrambleB >> 8) & 0xFF), u8(scrambleB & 0xFF), u8((scrambleC >> 24) & 0xFF), + u8((scrambleC >> 16) & 0xFF), u8((scrambleC >> 8) & 0xFF), u8(scrambleC & 0xFF), + u8((scrambleD >> 24) & 0xFF), u8((scrambleD >> 16) & 0xFF), u8((scrambleD >> 8) & 0xFF), + u8(scrambleD & 0xFF)}; +} + +u32 DimensionsToypad::Scramble(const std::array& uid, u8 count) { + std::vector to_scramble; + to_scramble.reserve(uid.size() + CHAR_CONSTANT.size()); + for (u8 x : uid) { + to_scramble.push_back(x); + } + for (u8 c : CHAR_CONSTANT) { + to_scramble.push_back(c); + } + to_scramble[(count * 4) - 1] = 0xaa; + + std::array randomized = DimensionsRandomize(to_scramble, count); + + return (u32(randomized[0]) << 24) | (u32(randomized[1]) << 16) | (u32(randomized[2]) << 8) | + u32(randomized[3]); +} + +std::array DimensionsToypad::PWDGenerate(const std::array& uid) { + std::vector pwdCalc = {PWD_CONSTANT.begin(), PWD_CONSTANT.end() - 1}; + for (u8 i = 0; i < uid.size(); i++) { + pwdCalc.insert(pwdCalc.begin() + i, uid[i]); + } + + return DimensionsRandomize(pwdCalc, 8); +} + +std::array DimensionsToypad::DimensionsRandomize(const std::vector& key, u8 count) { + u32 scrambled = 0; + for (u8 i = 0; i < count; i++) { + const u32 v4 = std::rotr(scrambled, 25); + const u32 v5 = std::rotr(scrambled, 10); + const u32 b = u32(key[i * 4]) | (u32(key[(i * 4) + 1]) << 8) | + (u32(key[(i * 4) + 2]) << 16) | (u32(key[(i * 4) + 3]) << 24); + scrambled = b + v4 + v5 - scrambled; + } + return {u8(scrambled & 0xFF), u8(scrambled >> 8 & 0xFF), u8(scrambled >> 16 & 0xFF), + u8(scrambled >> 24 & 0xFF)}; +} + +u32 DimensionsToypad::GetFigureId(const std::array& buf) { + const std::array figure_key = GenerateFigureKey(buf); + + const std::array decrypted = Decrypt(&buf[36 * 4], figure_key); + + const u32 fig_num = u32(decrypted[0]) | (u32(decrypted[1]) << 8) | (u32(decrypted[2]) << 16) | + (u32(decrypted[3]) << 24); + // Characters have their model number encrypted in page 36 + if (fig_num < 1000) { + return fig_num; + } + // Vehicles/Gadgets have their model number written as little endian in page 36 + return u32(buf[36 * 4]) | (u32(buf[(36 * 4) + 1]) << 8) | (u32(buf[(36 * 4) + 2]) << 16) | + (u32(buf[(36 * 4) + 3]) << 24); +} + +DimensionsFigure& DimensionsToypad::GetFigureByIndex(u8 index) { + return m_figures[index]; +} + +void DimensionsToypad::RandomUID(u8* uid_buffer) { + uid_buffer[0] = 0x04; + uid_buffer[7] = 0x80; + + for (u8 i = 1; i < 7; i++) { + u8 random = rand() % 255; + uid_buffer[i] = random; + } +} + +void DimensionsToypad::GetChallengeResponse(const u8* buf, u8 sequence, + std::array& reply_buf) { + // Decrypt payload into an 8 byte array + const std::array value = Decrypt(buf, std::nullopt); + // Confirmation is the first 4 bytes of the decrypted payload + // const u32 conf = read_from_ptr>(value); + // Generate next random number based on RNG + const u32 next_random = GetNext(); + // Encrypt an 8 byte array, first 4 bytes are the next random number (little endian) + // followed by the confirmation from the decrypted payload + std::array value_to_encrypt = {u8(next_random & 0xFF), + u8((next_random >> 8) & 0xFF), + u8((next_random >> 16) & 0xFF), + u8((next_random >> 24) & 0xFF), + value[0], + value[1], + value[2], + value[3]}; + const std::array encrypted = Encrypt(value_to_encrypt.data(), std::nullopt); + reply_buf[0] = 0x55; + reply_buf[1] = 0x09; + reply_buf[2] = sequence; + // Copy encrypted value to response data + std::memcpy(&reply_buf[3], encrypted.data(), encrypted.size()); + reply_buf[11] = GenerateChecksum(reply_buf, 11); +} + +void DimensionsToypad::QueryBlock(u8 index, u8 page, std::array& reply_buf, u8 sequence) { + std::lock_guard lock(m_dimensions_mutex); + + reply_buf[0] = 0x55; + reply_buf[1] = 0x12; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + + // Index from game begins at 1 rather than 0, so minus 1 here + if (const u8 figure_index = index - 1; figure_index < MAX_DIMENSIONS_FIGURES) { + const DimensionsFigure& figure = GetFigureByIndex(figure_index); + + // Query 4 pages of 4 bytes from the figure, copy this to the response + if (figure.index != 255 && (4 * page) < ((0x2D * 4) - 16)) { + std::memcpy(&reply_buf[4], figure.data.data() + (4 * page), 16); + } + } + reply_buf[20] = GenerateChecksum(reply_buf, 20); +} + +void DimensionsToypad::WriteBlock(u8 index, u8 page, const u8* to_write_buf, + std::array& reply_buf, u8 sequence) { + std::lock_guard lock(m_dimensions_mutex); + + reply_buf[0] = 0x55; + reply_buf[1] = 0x02; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + + // Index from game begins at 1 rather than 0, so minus 1 here + if (const u8 figure_index = index - 1; figure_index < MAX_DIMENSIONS_FIGURES) { + DimensionsFigure& figure = GetFigureByIndex(figure_index); + + // Copy 4 bytes to the page on the figure requested by the game + if (figure.index != 255 && page < 0x2D) { + // Id is written to page 36 + if (page == 36) { + figure.id = u32(to_write_buf[0]) | (u32(to_write_buf[1]) << 8) | + (u32(to_write_buf[2]) << 16) | (u32(to_write_buf[3]) << 24); + } + std::memcpy(figure.data.data() + (page * 4), to_write_buf, 4); + figure.Save(); + } + } + reply_buf[4] = GenerateChecksum(reply_buf, 4); +} + +void DimensionsToypad::GetModel(const u8* buf, u8 sequence, std::array& reply_buf) { + // Decrypt payload to 8 byte array, byte 1 is the index, 4-7 are the confirmation + const std::array value = Decrypt(buf, std::nullopt); + const u8 index = value[0]; + // const u32 conf = read_from_ptr>(value, 4); + std::array value_to_encrypt = {}; + // Response is the figure's id (little endian) followed by the confirmation from payload + // Index from game begins at 1 rather than 0, so minus 1 here + if (const u8 figure_index = index - 1; figure_index < MAX_DIMENSIONS_FIGURES) { + const DimensionsFigure& figure = GetFigureByIndex(figure_index); + value_to_encrypt = {u8(figure.id & 0xFF), + u8((figure.id >> 8) & 0xFF), + u8((figure.id >> 16) & 0xFF), + u8((figure.id >> 24) & 0xFF), + value[4], + value[5], + value[6], + value[7]}; + } + const std::array encrypted = Encrypt(value_to_encrypt.data(), std::nullopt); + reply_buf[0] = 0x55; + reply_buf[1] = 0x0a; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + // Copy encrypted message to response + std::memcpy(&reply_buf[4], encrypted.data(), encrypted.size()); + reply_buf[12] = GenerateChecksum(reply_buf, 12); +} + +std::optional> DimensionsToypad::PopAddedRemovedResponse() { + std::lock_guard lock(m_dimensions_mutex); + + if (m_figure_added_removed_responses.empty()) { + return std::nullopt; + } + + std::array response = m_figure_added_removed_responses.front(); + m_figure_added_removed_responses.pop(); + return response; +} + +libusb_endpoint_descriptor* DimensionsBackend::FillEndpointDescriptorPair() { + return m_endpoint_descriptors.data(); +} + +libusb_interface_descriptor* DimensionsBackend::FillInterfaceDescriptor( + libusb_endpoint_descriptor* descs) { + m_interface_descriptors[0].endpoint = descs; + return m_interface_descriptors.data(); +} + +libusb_config_descriptor* DimensionsBackend::FillConfigDescriptor(libusb_interface* inter) { + m_config_descriptors[0].interface = inter; + return m_config_descriptors.data(); +} + +libusb_device_descriptor* DimensionsBackend::FillDeviceDescriptor() { + return m_device_descriptors.data(); +} + +libusb_transfer_status DimensionsBackend::HandleAsyncTransfer(libusb_transfer* transfer) { + ASSERT(transfer->length == 32); + + switch (transfer->endpoint) { + case 0x81: { + // Read Endpoint, wait to respond with either an added/removed figure response, or a queued + // response from a previous write + bool responded = false; + while (!responded) { + std::lock_guard lock(m_query_mutex); + std::optional> response = + m_dimensions_toypad->PopAddedRemovedResponse(); + if (response) { + std::memcpy(transfer->buffer, response.value().data(), 0x20); + transfer->length = 32; + responded = true; + } else if (!m_queries.empty()) { + std::memcpy(transfer->buffer, m_queries.front().data(), 0x20); + transfer->length = 32; + m_queries.pop(); + responded = true; + } + } + break; + } + case 0x01: { + // Write endpoint, similar structure of request to the Infinity Base with a command for byte + // 3, sequence for byte 4, the payload after that, then a checksum for the final byte. + + const u8 command = transfer->buffer[2]; + const u8 sequence = transfer->buffer[3]; + + std::array q_result{}; + + switch (command) { + case 0xB0: // Wake + { + // Consistent device response to the wake command + q_result = {0x55, 0x0e, 0x01, 0x28, 0x63, 0x29, 0x20, 0x4c, 0x45, + 0x47, 0x4f, 0x20, 0x32, 0x30, 0x31, 0x34, 0x46}; + break; + } + case 0xB1: // Seed + { + // Initialise a random number generator using the seed provided + m_dimensions_toypad->GenerateRandomNumber(&transfer->buffer[4], sequence, q_result); + break; + } + case 0xB3: // Challenge + { + // Get the next number in the sequence based on the RNG from 0xB1 command + m_dimensions_toypad->GetChallengeResponse(&transfer->buffer[4], sequence, q_result); + break; + } + case 0xC0: // Color + case 0xC1: // Get Pad Color + case 0xC2: // Fade + case 0xC3: // Flash + case 0xC4: // Fade Random + case 0xC6: // Fade All + case 0xC7: // Flash All + case 0xC8: // Color All + { + // Send a blank response to acknowledge color has been sent to toypad + m_dimensions_toypad->GetBlankResponse(0x01, sequence, q_result); + break; + } + case 0xD2: // Read + { + // Read 4 pages from the figure at index (buf[4]), starting with page buf[5] + m_dimensions_toypad->QueryBlock(transfer->buffer[4], transfer->buffer[5], q_result, + sequence); + break; + } + case 0xD3: // Write + { + // Write 4 bytes to page buf[5] to the figure at index buf[4] + m_dimensions_toypad->WriteBlock(transfer->buffer[4], transfer->buffer[5], + &transfer->buffer[6], q_result, sequence); + break; + } + case 0xD4: // Model + { + // Get the model id of the figure at index buf[4] + m_dimensions_toypad->GetModel(&transfer->buffer[4], sequence, q_result); + break; + } + case 0xD0: // Tag List + case 0xE1: // PWD + case 0xE5: // Active + case 0xFF: // LEDS Query + { + // Further investigation required + LOG_ERROR(Lib_Usbd, "Unimplemented LD Function: {:x}", command); + break; + } + default: { + LOG_ERROR(Lib_Usbd, "Unknown LD Function: {:x}", command); + break; + } + } + std::lock_guard lock(m_query_mutex); + m_queries.push(q_result); + break; + } + default: + break; + } + return LIBUSB_TRANSFER_COMPLETED; +} + +s32 DimensionsBackend::SubmitTransfer(libusb_transfer* transfer) { + if (transfer->endpoint == 0x01) { + std::thread write_thread([this, transfer] { + HandleAsyncTransfer(transfer); + + const u8 flags = transfer->flags; + transfer->status = LIBUSB_TRANSFER_COMPLETED; + transfer->actual_length = transfer->length; + if (transfer->callback) { + transfer->callback(transfer); + } + if (flags & LIBUSB_TRANSFER_FREE_TRANSFER) { + libusb_free_transfer(transfer); + } + }); + write_thread.detach(); + return LIBUSB_SUCCESS; + } + + return UsbEmulatedBackend::SubmitTransfer(transfer); +} + +void DimensionsFigure::Save() { + if (!dimFile.IsOpen()) + return; + + dimFile.Seek(0); + dimFile.Write(data); +} +} // namespace Libraries::Usbd \ No newline at end of file diff --git a/src/core/libraries/usbd/emulated/dimensions.h b/src/core/libraries/usbd/emulated/dimensions.h new file mode 100644 index 000000000..d9573b5f4 --- /dev/null +++ b/src/core/libraries/usbd/emulated/dimensions.h @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/io_file.h" +#include "core/libraries/usbd/usb_backend.h" + +namespace Libraries::Usbd { + +constexpr u8 DIMEMSIONS_BLOCK_COUNT = 0x2D; +constexpr u8 DIMENSIONS_BLOCK_SIZE = 0x04; +constexpr u8 DIMENSIONS_FIGURE_SIZE = DIMEMSIONS_BLOCK_COUNT * DIMENSIONS_BLOCK_SIZE; +constexpr u8 MAX_DIMENSIONS_FIGURES = 7; + +struct DimensionsFigure final { + Common::FS::IOFile dimFile; + std::array data{}; + u8 index = 255; + u8 pad = 255; + u32 id = 0; + void Save(); +}; + +class DimensionsToypad final : public UsbEmulatedImpl { +public: + DimensionsToypad(); + ~DimensionsToypad() override = default; + + static void GetBlankResponse(u8 type, u8 sequence, std::array& reply_buf); + void GenerateRandomNumber(const u8* buf, u8 sequence, std::array& reply_buf); + void InitializeRNG(u32 seed); + void GetChallengeResponse(const u8* buf, u8 sequence, std::array& reply_buf); + void QueryBlock(u8 index, u8 page, std::array& reply_buf, u8 sequence); + void WriteBlock(u8 index, u8 page, const u8* to_write_buf, std::array& reply_buf, + u8 sequence); + void GetModel(const u8* buf, u8 sequence, std::array& reply_buf); + std::optional> PopAddedRemovedResponse(); + + void LoadFigure(std::string file_name, u8 pad, u8 slot) override; + void RemoveFigure(u8 pad, u8 slot, bool full_remove) override; + void MoveFigure(u8 new_pad, u8 new_index, u8 old_pad, u8 old_index) override; + void TempRemoveFigure(u8 index) override; + void CancelRemoveFigure(u8 index) override; + + u32 LoadDimensionsFigure(const std::array& buf, Common::FS::IOFile file, + u8 pad, u8 index); + +protected: + std::mutex m_dimensions_mutex; + std::array m_figures{}; + +private: + static void RandomUID(u8* uid_buffer); + static u8 GenerateChecksum(const std::array& data, u32 num_of_bytes); + static std::array Decrypt(const u8* buf, std::optional> key); + static std::array Encrypt(const u8* buf, std::optional> key); + static std::array GenerateFigureKey(const std::array& buf); + static u32 Scramble(const std::array& uid, u8 count); + static std::array PWDGenerate(const std::array& uid); + static std::array DimensionsRandomize(const std::vector& key, u8 count); + static u32 GetFigureId(const std::array& buf); + u32 GetNext(); + DimensionsFigure& GetFigureByIndex(u8 index); + + u32 m_random_a{}; + u32 m_random_b{}; + u32 m_random_c{}; + u32 m_random_d{}; + + u8 m_figure_order = 0; + std::queue> m_figure_added_removed_responses; +}; + +class DimensionsBackend final : public UsbEmulatedBackend { +protected: + libusb_endpoint_descriptor* FillEndpointDescriptorPair() override; + libusb_interface_descriptor* FillInterfaceDescriptor( + libusb_endpoint_descriptor* descs) override; + libusb_config_descriptor* FillConfigDescriptor(libusb_interface* inter) override; + libusb_device_descriptor* FillDeviceDescriptor() override; + + s32 GetMaxPacketSize(libusb_device* dev, u8 endpoint) override { + return 32; + } + + s32 SubmitTransfer(libusb_transfer* transfer) override; + + libusb_transfer_status HandleAsyncTransfer(libusb_transfer* transfer) override; + + std::shared_ptr GetImplRef() override { + return m_dimensions_toypad; + } + + std::mutex m_query_mutex; + std::queue> m_queries; + +private: + std::shared_ptr m_dimensions_toypad = std::make_shared(); + + std::array m_endpoint_out_extra = {0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x1d, 0x00}; + std::vector m_endpoint_descriptors = { + {0x7, 0x5, 0x81, 0x3, 0x20, 0x1, 0x0, 0x0}, {0x7, 0x5, 0x1, 0x3, 0x20, 0x1, 0x0, 0x0}}; + std::vector m_interface_descriptors = { + {0x9, 0x4, 0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0}}; + std::vector m_config_descriptors = { + {0x9, 0x2, 0x29, 0x1, 0x1, 0x0, 0x80, 0xFA}}; + std::vector m_device_descriptors = { + {0x12, 0x1, 0x200, 0x0, 0x0, 0x0, 0x20, 0x0E6F, 0x0241, 0x200, 0x1, 0x2, 0x0, 0x1}}; +}; +} // namespace Libraries::Usbd \ No newline at end of file diff --git a/src/core/libraries/usbd/emulated/infinity.cpp b/src/core/libraries/usbd/emulated/infinity.cpp new file mode 100644 index 000000000..af8e13dcc --- /dev/null +++ b/src/core/libraries/usbd/emulated/infinity.cpp @@ -0,0 +1,392 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "infinity.h" + +#include + +namespace Libraries::Usbd { + +InfinityBase::InfinityBase() {} + +void InfinityBase::LoadFigure(std::string file_name, u8 pad, u8 slot) { + Common::FS::IOFile file(file_name, Common::FS::FileAccessMode::ReadWrite); + std::array data; + ASSERT(file.Read(data) == data.size()); + LoadInfinityFigure(data, std::move(file), slot); +} + +void InfinityBase::RemoveFigure(u8 pad, u8 slot, bool full_remove) { + std::lock_guard lock(infinity_mutex); + InfinityFigure& figure = infinity_figures[slot]; + + if (!figure.present) { + return; + } + + slot = DeriveFigurePosition(slot); + if (slot == 0) { + return; + } + + figure.present = false; + + std::array figure_change_response = {0xab, 0x04, slot, 0x09, figure.order_added, 0x01}; + figure_change_response[6] = GenerateChecksum(figure_change_response, 6); + m_figure_added_removed_responses.push(figure_change_response); + + figure.Save(); + figure.infFile.Close(); +} + +void InfinityBase::LoadInfinityFigure(const std::array& buf, + Common::FS::IOFile file, u8 position) { + std::lock_guard lock(infinity_mutex); + u8 order_added; + + InfinityFigure& figure = infinity_figures[position]; + + figure.infFile = std::move(file); + memcpy(figure.data.data(), buf.data(), figure.data.size()); + figure.present = true; + if (figure.order_added == 255) { + figure.order_added = m_figure_order; + m_figure_order++; + } + order_added = figure.order_added; + + position = DeriveFigurePosition(position); + if (position == 0) { + return; + } + + std::array figure_change_response = {0xab, 0x04, position, 0x09, order_added, 0x00}; + figure_change_response[6] = GenerateChecksum(figure_change_response, 6); + m_figure_added_removed_responses.push(figure_change_response); +} + +void InfinityBase::GetBlankResponse(u8 sequence, std::array& reply_buf) { + reply_buf[0] = 0xaa; + reply_buf[1] = 0x01; + reply_buf[2] = sequence; + reply_buf[3] = GenerateChecksum(reply_buf, 3); +} + +void InfinityBase::DescrambleAndSeed(u8* buf, u8 sequence, std::array& reply_buf) { + u64 value = u64(buf[4]) << 56 | u64(buf[5]) << 48 | u64(buf[6]) << 40 | u64(buf[7]) << 32 | + u64(buf[8]) << 24 | u64(buf[9]) << 16 | u64(buf[10]) << 8 | u64(buf[11]); + u32 seed = Descramble(value); + GenerateSeed(seed); + GetBlankResponse(sequence, reply_buf); +} + +void InfinityBase::GetNextAndScramble(u8 sequence, std::array& reply_buf) { + const u32 next_random = GetNext(); + const u64 scrambled_next_random = Scramble(next_random, 0); + reply_buf = {0xAA, 0x09, sequence}; + reply_buf[3] = u8((scrambled_next_random >> 56) & 0xFF); + reply_buf[4] = u8((scrambled_next_random >> 48) & 0xFF); + reply_buf[5] = u8((scrambled_next_random >> 40) & 0xFF); + reply_buf[6] = u8((scrambled_next_random >> 32) & 0xFF); + reply_buf[7] = u8((scrambled_next_random >> 24) & 0xFF); + reply_buf[8] = u8((scrambled_next_random >> 16) & 0xFF); + reply_buf[9] = u8((scrambled_next_random >> 8) & 0xFF); + reply_buf[10] = u8(scrambled_next_random & 0xFF); + reply_buf[11] = GenerateChecksum(reply_buf, 11); +} + +void InfinityBase::GetPresentFigures(u8 sequence, std::array& reply_buf) { + int x = 3; + for (u8 i = 0; i < infinity_figures.size(); i++) { + u8 slot = i == 0 ? 0x10 : (i < 4) ? 0x20 : 0x30; + if (infinity_figures[i].present) { + reply_buf[x] = slot + infinity_figures[i].order_added; + reply_buf[x + 1] = 0x09; + x += 2; + } + } + reply_buf[0] = 0xaa; + reply_buf[1] = x - 2; + reply_buf[2] = sequence; + reply_buf[x] = GenerateChecksum(reply_buf, x); +} + +void InfinityBase::QueryBlock(u8 fig_num, u8 block, std::array& reply_buf, u8 sequence) { + std::lock_guard lock(infinity_mutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + reply_buf[0] = 0xaa; + reply_buf[1] = 0x12; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + const u8 file_block = (block == 0) ? 1 : (block * 4); + if (figure.present && file_block < 20) { + memcpy(&reply_buf[4], figure.data.data() + (16 * file_block), 16); + } + reply_buf[20] = GenerateChecksum(reply_buf, 20); +} + +void InfinityBase::WriteBlock(u8 fig_num, u8 block, const u8* to_write_buf, + std::array& reply_buf, u8 sequence) { + std::lock_guard lock(infinity_mutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + reply_buf[0] = 0xaa; + reply_buf[1] = 0x02; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + const u8 file_block = (block == 0) ? 1 : (block * 4); + if (figure.present && file_block < 20) { + memcpy(figure.data.data() + (file_block * 16), to_write_buf, 16); + figure.Save(); + } + reply_buf[4] = GenerateChecksum(reply_buf, 4); +} + +void InfinityBase::GetFigureIdentifier(u8 fig_num, u8 sequence, std::array& reply_buf) { + std::lock_guard lock(infinity_mutex); + + InfinityFigure& figure = GetFigureByOrder(fig_num); + + reply_buf[0] = 0xaa; + reply_buf[1] = 0x09; + reply_buf[2] = sequence; + reply_buf[3] = 0x00; + + if (figure.present) { + memcpy(&reply_buf[4], figure.data.data(), 7); + } + reply_buf[11] = GenerateChecksum(reply_buf, 11); +} + +std::optional> InfinityBase::PopAddedRemovedResponse() { + if (m_figure_added_removed_responses.empty()) + return std::nullopt; + + std::array response = m_figure_added_removed_responses.front(); + m_figure_added_removed_responses.pop(); + return response; +} + +u8 InfinityBase::GenerateChecksum(const std::array& data, int num_of_bytes) const { + int checksum = 0; + for (int i = 0; i < num_of_bytes; i++) { + checksum += data[i]; + } + return (checksum & 0xFF); +} + +u32 InfinityBase::Descramble(u64 num_to_descramble) { + u64 mask = 0x8E55AA1B3999E8AA; + u32 ret = 0; + + for (int i = 0; i < 64; i++) { + if (mask & 0x8000000000000000) { + ret = (ret << 1) | (num_to_descramble & 0x01); + } + + num_to_descramble >>= 1; + mask <<= 1; + } + + return ret; +} + +u64 InfinityBase::Scramble(u32 num_to_scramble, u32 garbage) { + u64 mask = 0x8E55AA1B3999E8AA; + u64 ret = 0; + + for (int i = 0; i < 64; i++) { + ret <<= 1; + + if ((mask & 1) != 0) { + ret |= (num_to_scramble & 1); + num_to_scramble >>= 1; + } else { + ret |= (garbage & 1); + garbage >>= 1; + } + + mask >>= 1; + } + + return ret; +} + +void InfinityBase::GenerateSeed(u32 seed) { + random_a = 0xF1EA5EED; + random_b = seed; + random_c = seed; + random_d = seed; + + for (int i = 0; i < 23; i++) { + GetNext(); + } +} + +u32 InfinityBase::GetNext() { + u32 a = random_a; + u32 b = random_b; + u32 c = random_c; + u32 ret = std::rotl(random_b, 27); + + const u32 temp = (a + ((ret ^ 0xFFFFFFFF) + 1)); + b ^= std::rotl(c, 17); + a = random_d; + c += a; + ret = b + temp; + a += temp; + + random_c = a; + random_a = b; + random_b = c; + random_d = ret; + + return ret; +} + +InfinityFigure& InfinityBase::GetFigureByOrder(u8 order_added) { + for (u8 i = 0; i < infinity_figures.size(); i++) { + if (infinity_figures[i].order_added == order_added) { + return infinity_figures[i]; + } + } + return infinity_figures[0]; +} + +u8 InfinityBase::DeriveFigurePosition(u8 position) { + switch (position) { + case 0: + case 1: + case 2: + return 1; + case 3: + case 4: + case 5: + return 2; + case 6: + case 7: + case 8: + return 3; + + default: + return 0; + } +} + +libusb_endpoint_descriptor* InfinityBackend::FillEndpointDescriptorPair() { + return m_endpoint_descriptors.data(); +} + +libusb_interface_descriptor* InfinityBackend::FillInterfaceDescriptor( + libusb_endpoint_descriptor* descs) { + m_interface_descriptors[0].endpoint = descs; + return m_interface_descriptors.data(); +} + +libusb_config_descriptor* InfinityBackend::FillConfigDescriptor(libusb_interface* inter) { + m_config_descriptors[0].interface = inter; + return m_config_descriptors.data(); +} + +libusb_device_descriptor* InfinityBackend::FillDeviceDescriptor() { + return m_device_descriptors.data(); +} + +libusb_transfer_status InfinityBackend::HandleAsyncTransfer(libusb_transfer* transfer) { + switch (transfer->endpoint) { + case 0x81: { + // Respond after FF command + std::optional> response = m_infinity_base->PopAddedRemovedResponse(); + if (response) { + memcpy(transfer->buffer, response.value().data(), 0x20); + } else if (!m_queries.empty()) { + memcpy(transfer->buffer, m_queries.front().data(), 0x20); + m_queries.pop(); + } + break; + } + case 0x01: { + const u8 command = transfer->buffer[2]; + const u8 sequence = transfer->buffer[3]; + LOG_INFO(Lib_Usbd, "Infinity Backend Transfer command: {:x}", command); + + std::array q_result{}; + + switch (command) { + case 0x80: { + q_result = {0xaa, 0x15, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x03, 0x02, 0x09, 0x09, 0x43, + 0x20, 0x32, 0x62, 0x36, 0x36, 0x4b, 0x34, 0x99, 0x67, 0x31, 0x93, 0x8c}; + break; + } + case 0x81: { + // Initiate Challenge + m_infinity_base->DescrambleAndSeed(transfer->buffer, sequence, q_result); + break; + } + case 0x83: { + // Challenge Response + m_infinity_base->GetNextAndScramble(sequence, q_result); + break; + } + case 0x90: + case 0x92: + case 0x93: + case 0x95: + case 0x96: { + // Color commands + m_infinity_base->GetBlankResponse(sequence, q_result); + break; + } + case 0xA1: { + // Get Present Figures + m_infinity_base->GetPresentFigures(sequence, q_result); + break; + } + case 0xA2: { + // Read Block from Figure + m_infinity_base->QueryBlock(transfer->buffer[4], transfer->buffer[5], q_result, + sequence); + break; + } + case 0xA3: { + // Write block to figure + m_infinity_base->WriteBlock(transfer->buffer[4], transfer->buffer[5], + &transfer->buffer[7], q_result, sequence); + break; + } + case 0xB4: { + // Get figure ID + m_infinity_base->GetFigureIdentifier(transfer->buffer[4], sequence, q_result); + break; + } + case 0xB5: { + // Get status? + m_infinity_base->GetBlankResponse(sequence, q_result); + break; + } + default: + LOG_ERROR(Lib_Usbd, "Unhandled Infinity Query: {}", command); + break; + } + + m_queries.push(q_result); + break; + } + default: + LOG_ERROR(Lib_Usbd, "Unhandled Infinity Endpoint: {}", transfer->endpoint); + break; + } + return LIBUSB_TRANSFER_COMPLETED; +} + +void InfinityFigure::Save() { + if (!infFile.IsOpen()) + return; + + infFile.Seek(0); + infFile.Write(data); +} +} // namespace Libraries::Usbd \ No newline at end of file diff --git a/src/core/libraries/usbd/emulated/infinity.h b/src/core/libraries/usbd/emulated/infinity.h new file mode 100644 index 000000000..4c8a5305f --- /dev/null +++ b/src/core/libraries/usbd/emulated/infinity.h @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/io_file.h" +#include "core/libraries/usbd/usb_backend.h" + +namespace Libraries::Usbd { + +constexpr u16 INFINITY_BLOCK_COUNT = 0x14; +constexpr u16 INFINITY_BLOCK_SIZE = 0x10; +constexpr u16 INFINITY_FIGURE_SIZE = INFINITY_BLOCK_COUNT * INFINITY_BLOCK_SIZE; +constexpr u8 MAX_INFINITY_FIGURES = 9; + +struct InfinityFigure final { + Common::FS::IOFile infFile; + std::array data{}; + bool present = false; + u8 order_added = 255; + void Save(); +}; + +class InfinityBase final : public UsbEmulatedImpl { +public: + InfinityBase(); + ~InfinityBase() override = default; + + void GetBlankResponse(u8 sequence, std::array& reply_buf); + void DescrambleAndSeed(u8* buf, u8 sequence, std::array& reply_buf); + void GetNextAndScramble(u8 sequence, std::array& reply_buf); + void GetPresentFigures(u8 sequence, std::array& reply_buf); + void QueryBlock(u8 fig_num, u8 block, std::array& reply_buf, u8 sequence); + void WriteBlock(u8 fig_num, u8 block, const u8* to_write_buf, std::array& reply_buf, + u8 sequence); + void GetFigureIdentifier(u8 fig_num, u8 sequence, std::array& reply_buf); + std::optional> PopAddedRemovedResponse(); + + void LoadFigure(std::string file_name, u8 pad, u8 slot) override; + void RemoveFigure(u8 pad, u8 slot, bool full_remove) override; + void MoveFigure(u8 new_pad, u8 new_index, u8 old_pad, u8 old_index) override {} + void TempRemoveFigure(u8 index) override {} + void CancelRemoveFigure(u8 index) override {} + + void LoadInfinityFigure(const std::array& buf, Common::FS::IOFile file, + u8 position); + +protected: + std::mutex infinity_mutex; + std::array infinity_figures; + +private: + u8 GenerateChecksum(const std::array& data, int num_of_bytes) const; + u32 Descramble(u64 num_to_descramble); + u64 Scramble(u32 num_to_scramble, u32 garbage); + void GenerateSeed(u32 seed); + u32 GetNext(); + InfinityFigure& GetFigureByOrder(u8 order_added); + u8 DeriveFigurePosition(u8 position); + + u32 random_a = 0; + u32 random_b = 0; + u32 random_c = 0; + u32 random_d = 0; + + u8 m_figure_order = 0; + std::queue> m_figure_added_removed_responses; +}; + +class InfinityBackend final : public UsbEmulatedBackend { +protected: + libusb_endpoint_descriptor* FillEndpointDescriptorPair() override; + libusb_interface_descriptor* FillInterfaceDescriptor( + libusb_endpoint_descriptor* descs) override; + libusb_config_descriptor* FillConfigDescriptor(libusb_interface* inter) override; + libusb_device_descriptor* FillDeviceDescriptor() override; + + s32 ControlTransfer(libusb_device_handle* dev_handle, u8 bmRequestType, u8 bRequest, u16 wValue, + u16 wIndex, u8* data, u16 wLength, u32 timeout) override { + return LIBUSB_SUCCESS; + } + + libusb_transfer_status HandleAsyncTransfer(libusb_transfer* transfer) override; + + std::shared_ptr GetImplRef() override { + return m_infinity_base; + } + +private: + std::shared_ptr m_infinity_base = std::make_shared(); + + std::array m_endpoint_out_extra = {0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x1d, 0x00}; + std::vector m_endpoint_descriptors = { + {0x7, 0x5, 0x81, 0x3, 0x20, 0x1, 0x0, 0x0}, + {0x7, 0x5, 0x1, 0x3, 0x20, 0x1, 0x0, 0x0, m_endpoint_out_extra.data(), 9}}; + std::vector m_interface_descriptors = { + {0x9, 0x4, 0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0}}; + std::vector m_config_descriptors = { + {0x9, 0x2, 0x29, 0x1, 0x1, 0x0, 0x80, 0xFA}}; + std::vector m_device_descriptors = { + {0x12, 0x1, 0x200, 0x0, 0x0, 0x0, 0x20, 0x0E6F, 0x0129, 0x200, 0x1, 0x2, 0x3, 0x1}}; + + std::queue> m_queries; +}; +} // namespace Libraries::Usbd \ No newline at end of file diff --git a/src/core/libraries/usbd/usb_backend.h b/src/core/libraries/usbd/usb_backend.h index 7a5c2899a..e668d5509 100644 --- a/src/core/libraries/usbd/usb_backend.h +++ b/src/core/libraries/usbd/usb_backend.h @@ -312,7 +312,7 @@ public: const auto endpoint_descs = FillEndpointDescriptorPair(); const auto interface_desc = FillInterfaceDescriptor(endpoint_descs); - const auto interface = static_cast(calloc(1, sizeof(libusb_interface*))); + const auto interface = static_cast(calloc(1, sizeof(libusb_interface))); interface->altsetting = interface_desc; interface->num_altsetting = 1; @@ -366,7 +366,7 @@ public: const auto desc = FillDeviceDescriptor(); ASSERT(desc); - const auto fake = static_cast(calloc(1, sizeof(UsbDevice*))); + const auto fake = static_cast(calloc(1, sizeof(UsbDevice))); fake->bus_number = 0; fake->port_number = 0; fake->device_address = 0; diff --git a/src/core/libraries/usbd/usbd.h b/src/core/libraries/usbd/usbd.h index 60a3e753b..fef9d6871 100644 --- a/src/core/libraries/usbd/usbd.h +++ b/src/core/libraries/usbd/usbd.h @@ -4,6 +4,8 @@ #pragma once #include "common/types.h" +#include "emulated/dimensions.h" +#include "emulated/infinity.h" #include "emulated/skylander.h" #include "usb_backend.h" @@ -33,10 +35,9 @@ using SceUsbdTransfer = libusb_transfer; using SceUsbdControlSetup = libusb_control_setup; using SceUsbdTransferCallback = void PS4_SYSV_ABI (*)(SceUsbdTransfer* transfer); -// TODO: implement emulated devices using SkylandersPortalBackend = SkylanderBackend; -using InfinityBaseBackend = UsbRealBackend; -using DimensionsToypadBackend = UsbRealBackend; +using InfinityBaseBackend = InfinityBackend; +using DimensionsToypadBackend = DimensionsBackend; enum class SceUsbdSpeed : u32 { UNKNOWN = 0, diff --git a/src/emulator.cpp b/src/emulator.cpp index 0e7485b89..ad407f9b6 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -113,6 +113,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, std::string id; std::string title; std::string app_version; + u32 sdk_version; u32 fw_version; Common::PSFAttributes psf_attributes{}; if (param_sfo_exists) { @@ -132,8 +133,48 @@ void Emulator::Run(std::filesystem::path file, std::vector args, if (const auto raw_attributes = param_sfo->GetInteger("ATTRIBUTE")) { psf_attributes.raw = *raw_attributes; } + + // Extract sdk version from pubtool info. + std::string_view pubtool_info = + param_sfo->GetString("PUBTOOLINFO").value_or("Unknown value"); + u64 sdk_ver_offset = pubtool_info.find("sdk_ver"); + + if (sdk_ver_offset == pubtool_info.npos) { + // Default to using firmware version if SDK version is not found. + sdk_version = fw_version; + } else { + // Increment offset to account for sdk_ver= part of string. + sdk_ver_offset += 8; + u64 sdk_ver_len = pubtool_info.find(",", sdk_ver_offset); + if (sdk_ver_len == pubtool_info.npos) { + // If there's no more commas, this is likely the last entry of pubtool info. + // Use string length instead. + sdk_ver_len = pubtool_info.size(); + } + sdk_ver_len -= sdk_ver_offset; + std::string sdk_ver_string = pubtool_info.substr(sdk_ver_offset, sdk_ver_len).data(); + // Number is stored in base 16. + sdk_version = std::stoi(sdk_ver_string, nullptr, 16); + } } + auto& game_info = Common::ElfInfo::Instance(); + game_info.initialized = true; + game_info.game_serial = id; + game_info.title = title; + game_info.app_ver = app_version; + game_info.firmware_ver = fw_version & 0xFFF00000; + game_info.raw_firmware_ver = fw_version; + game_info.sdk_ver = sdk_version; + game_info.psf_attributes = psf_attributes; + + const auto pic1_path = mnt->GetHostPath("/app0/sce_sys/pic1.png"); + if (std::filesystem::exists(pic1_path)) { + game_info.splash_path = pic1_path; + } + + game_info.game_folder = game_folder; + Config::load(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".toml"), true); @@ -196,6 +237,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, if (param_sfo_exists) { LOG_INFO(Loader, "Game id: {} Title: {}", id, title); LOG_INFO(Loader, "Fw: {:#x} App Version: {}", fw_version, app_version); + LOG_INFO(Loader, "Compiled SDK version: {:#x}", sdk_version); LOG_INFO(Loader, "PSVR Supported: {}", (bool)psf_attributes.support_ps_vr.Value()); LOG_INFO(Loader, "PSVR Required: {}", (bool)psf_attributes.require_ps_vr.Value()); } @@ -235,22 +277,6 @@ void Emulator::Run(std::filesystem::path file, std::vector args, } } - auto& game_info = Common::ElfInfo::Instance(); - game_info.initialized = true; - game_info.game_serial = id; - game_info.title = title; - game_info.app_ver = app_version; - game_info.firmware_ver = fw_version & 0xFFF00000; - game_info.raw_firmware_ver = fw_version; - game_info.psf_attributes = psf_attributes; - - const auto pic1_path = mnt->GetHostPath("/app0/sce_sys/pic1.png"); - if (std::filesystem::exists(pic1_path)) { - game_info.splash_path = pic1_path; - } - - game_info.game_folder = game_folder; - std::string game_title = fmt::format("{} - {} <{}>", id, title, app_version); std::string window_title = ""; std::string remote_url(Common::g_scm_remote_url); diff --git a/src/imgui/renderer/imgui_core.cpp b/src/imgui/renderer/imgui_core.cpp index f82612444..452dee013 100644 --- a/src/imgui/renderer/imgui_core.cpp +++ b/src/imgui/renderer/imgui_core.cpp @@ -250,7 +250,7 @@ void Render(const vk::CommandBuffer& cmdbuf, const vk::ImageView& image_view, } bool MustKeepDrawing() { - return layers.size() > 1 || DebugState.IsShowingDebugMenuBar(); + return layers.size() > 1 || change_layers.size() > 1 || DebugState.IsShowingDebugMenuBar(); } } // namespace Core diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index 1cdb1db63..274ba339c 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -957,7 +957,7 @@ bool BufferCache::SynchronizeBuffer(Buffer& buffer, VAddr device_addr, u32 size, .pBufferMemoryBarriers = &post_barrier, }); } - if (is_texel_buffer) { + if (is_texel_buffer && !is_written) { return SynchronizeBufferFromImage(buffer, device_addr, size); } return false; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index be9543737..4706bff24 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -355,13 +355,12 @@ bool PipelineCache::RefreshGraphicsKey() { } // Fill color target information - key.color_buffers[cb] = Shader::PsColorBuffer{ - .data_format = col_buf.GetDataFmt(), - .num_format = col_buf.GetNumberFmt(), - .num_conversion = col_buf.GetNumberConversion(), - .export_format = regs.color_export_format.GetFormat(cb), - .swizzle = col_buf.Swizzle(), - }; + auto& color_buffer = key.color_buffers[cb]; + color_buffer.data_format = col_buf.GetDataFmt(); + color_buffer.num_format = col_buf.GetNumberFmt(); + color_buffer.num_conversion = col_buf.GetNumberConversion(); + color_buffer.export_format = regs.color_export_format.GetFormat(cb); + color_buffer.swizzle = col_buf.Swizzle(); } // Compile and bind shader stages @@ -379,7 +378,7 @@ bool PipelineCache::RefreshGraphicsKey() { continue; } if ((key.mrt_mask & (1u << cb)) == 0) { - key.color_buffers[cb] = {}; + std::memset(&key.color_buffers[cb], 0, sizeof(Shader::PsColorBuffer)); continue; } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 2984581a5..d63ee4fa4 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -380,7 +380,8 @@ void Rasterizer::OnSubmit() { } bool Rasterizer::BindResources(const Pipeline* pipeline) { - if (IsComputeImageCopy(pipeline) || IsComputeMetaClear(pipeline)) { + if (IsComputeImageCopy(pipeline) || IsComputeMetaClear(pipeline) || + IsComputeImageClear(pipeline)) { return false; } @@ -520,6 +521,66 @@ bool Rasterizer::IsComputeImageCopy(const Pipeline* pipeline) { return true; } +bool Rasterizer::IsComputeImageClear(const Pipeline* pipeline) { + if (!pipeline->IsCompute()) { + return false; + } + + // Ensure shader only has 2 bound buffers + const auto& cs_pgm = liverpool->GetCsRegs(); + const auto& info = pipeline->GetStage(Shader::LogicalStage::Compute); + if (cs_pgm.num_thread_x.full != 64 || info.buffers.size() != 2 || !info.images.empty()) { + return false; + } + + // From those 2 buffers, first must hold the clear vector and second the image being cleared + const auto& desc0 = info.buffers[0]; + const auto& desc1 = info.buffers[1]; + if (desc0.is_formatted || !desc1.is_formatted || desc0.is_written || !desc1.is_written) { + return false; + } + + // First buffer must have size of vec4 and second the size of a single layer + const AmdGpu::Buffer buf0 = desc0.GetSharp(info); + const AmdGpu::Buffer buf1 = desc1.GetSharp(info); + const u32 buf1_bpp = AmdGpu::NumBitsPerBlock(buf1.GetDataFmt()); + if (buf0.GetSize() != 16 || (cs_pgm.dim_x * 128ULL * (buf1_bpp / 8)) != buf1.GetSize()) { + return false; + } + + // Find image the buffer alias + const auto image1_id = + texture_cache.FindImageFromRange(buf1.base_address, buf1.GetSize(), false); + if (!image1_id) { + return false; + } + + // Image clear must be valid + VideoCore::Image& image1 = texture_cache.GetImage(image1_id); + if (image1.info.guest_size != buf1.GetSize() || image1.info.num_bits != buf1_bpp || + image1.info.props.is_depth) { + return false; + } + + // Perform image clear + const float* values = reinterpret_cast(buf0.base_address); + const vk::ClearValue clear = { + .color = {.float32 = std::array{values[0], values[1], values[2], values[3]}}, + }; + const VideoCore::SubresourceRange range = { + .base = + { + .level = 0, + .layer = 0, + }, + .extent = image1.info.resources, + }; + image1.Clear(clear, range); + image1.flags |= VideoCore::ImageFlagBits::GpuModified; + image1.flags &= ~VideoCore::ImageFlagBits::Dirty; + return true; +} + void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Bindings& binding, Shader::PushData& push_data) { buffer_bindings.clear(); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index 524a8f06d..96a3c95e8 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -112,6 +112,7 @@ private: bool IsComputeMetaClear(const Pipeline* pipeline); bool IsComputeImageCopy(const Pipeline* pipeline); + bool IsComputeImageClear(const Pipeline* pipeline); private: friend class VideoCore::BufferCache;