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