Merge branch 'shadps4-emu:main' into ime-fixes-again

This commit is contained in:
Valdis Bogdāns 2026-04-08 21:45:42 +03:00 committed by GitHub
commit a8e0495f4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 3027 additions and 1359 deletions

3
.gitignore vendored
View File

@ -418,3 +418,6 @@ FodyWeavers.xsd
# JetBrains
.idea
cmake-build-*
# Nix Result symlink
result

View File

@ -245,7 +245,7 @@ find_package(VulkanMemoryAllocator 3.1.0 CONFIG)
find_package(xbyak 7.07 CONFIG)
find_package(xxHash 0.8.2 MODULE)
find_package(ZLIB 1.3 MODULE)
find_package(Zydis 5.0.0 CONFIG)
find_package(Zydis 5.0.0 MODULE)
find_package(pugixml 1.14 CONFIG)
if (APPLE)
find_package(date 3.0.1 CONFIG)
@ -1093,6 +1093,8 @@ set(IMGUI src/imgui/imgui_config.h
src/imgui/imgui_layer.h
src/imgui/imgui_std.h
src/imgui/imgui_texture.h
src/imgui/imgui_translations.cpp
src/imgui/imgui_translations.h
src/imgui/renderer/imgui_core.cpp
src/imgui/renderer/imgui_core.h
src/imgui/renderer/imgui_impl_sdl3.cpp
@ -1139,7 +1141,14 @@ create_target_directory_groups(shadps4)
target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG)
target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 SDL3_mixer::SDL3_mixer pugixml::pugixml)
target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib)
target_link_libraries(shadps4 PRIVATE stb::headers lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib)
if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
target_link_libraries(shadps4 PRIVATE "/usr/lib/libusb.so")
target_link_libraries(shadps4 PRIVATE "/usr/local/lib/libuuid.so")
else()
target_link_libraries(shadps4 PRIVATE libusb::usb)
endif()
target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h")
target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h")
@ -1185,6 +1194,8 @@ if (APPLE)
# Replacement for std::chrono::time_zone
target_link_libraries(shadps4 PRIVATE date::date-tz epoll-shim)
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
target_link_libraries(shadps4 PRIVATE date::date-tz epoll-shim)
endif()
if (WIN32)
@ -1276,4 +1287,4 @@ install(TARGETS shadps4 BUNDLE DESTINATION .)
else()
enable_testing()
add_subdirectory(tests)
endif()
endif()

View File

@ -150,12 +150,12 @@ The following firmware modules are supported and must be placed in shadPS4's `sy
<div align="center">
| Modules | Modules | Modules | Modules |
|-------------------------|-------------------------|-------------------------|-------------------------|
| libSceCesCs.sprx | libSceFont.sprx | libSceFontFt.sprx | libSceFreeTypeOt.sprx |
| libSceJpegDec.sprx | libSceJpegEnc.sprx | libSceJson.sprx | libSceJson2.sprx |
| libSceLibcInternal.sprx | libSceNgs2.sprx | libScePngEnc.sprx | libSceRtc.sprx |
| libSceUlt.sprx | libSceAudiodec.sprx | | |
| Modules | Modules | Modules | Modules |
|--------------------------|--------------------------|--------------------------|--------------------------|
| libSceAudiodec.sprx | libSceCesCs.sprx | libSceFont.sprx | libSceFontFt.sprx |
| libSceFreeTypeOt.sprx | libSceJpegDec.sprx | libSceJpegEnc.sprx | libSceJson.sprx |
| libSceJson2.sprx | libSceLibcInternal.sprx | libSceNgs2.sprx | libScePngEnc.sprx |
| libSceRtc.sprx | libSceSystemGesture.sprx | libSceUlt.sprx | |
</div>
> [!Caution]

View File

@ -22,6 +22,7 @@ path = [
"documents/Screenshots/Linux/*",
"documents/Screenshots/Windows/*",
"externals/MoltenVK/MoltenVK_icd.json",
"flake.lock",
"scripts/ps4_names.txt",
"src/images/bronze.png",
"src/images/gold.png",
@ -130,4 +131,4 @@ SPDX-License-Identifier = "MIT"
[[annotations]]
path = "src/video_core/host_shaders/fsr/*"
SPDX-FileCopyrightText = "Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved."
SPDX-License-Identifier = "MIT"
SPDX-License-Identifier = "MIT"

View File

@ -53,6 +53,23 @@ sudo zypper install clang git cmake libasound2 libpulse-devel \
nix-shell shell.nix
```
#### Nix Flake Development Shell
```bash
nix develop
cmake -S . -B build/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
ln -s ./build/compile_commands.json .
```
#### Nix Flake Build
```bash
nix build .?submodules=1#linux.debug
```
```bash
nix build .?submodules=1#linux.release
```
```bash
nix build .?submodules=1#linux.releaseWithDebugInfo
```
#### Other Linux distributions
You can try one of two methods:

View File

@ -210,8 +210,15 @@ endif()
# libusb
if (NOT TARGET libusb::usb)
add_subdirectory(ext-libusb)
add_library(libusb::usb ALIAS usb-1.0)
if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
# YOU MUST USE NATIVE LIBUSB
# using anything else will crash instantly, also freebsd will NOT like it
# no you cant vendor this libusb, its builtin on freebsd
find_package(libusb)
else()
add_subdirectory(ext-libusb)
add_library(libusb::usb ALIAS usb-1.0)
endif()
endif()
# Discord RPC
@ -233,25 +240,26 @@ endif()
set(HWINFO_STATIC ON)
add_subdirectory(hwinfo)
# Apple-only dependencies
if (APPLE)
if (APPLE OR ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
# date
if (NOT TARGET date::date-tz)
option(BUILD_TZ_LIB "" ON)
option(USE_SYSTEM_TZ_DB "" ON)
add_subdirectory(date)
endif()
if (NOT TARGET epoll-shim)
add_subdirectory(epoll-shim)
endif()
endif()
# Apple-only dependencies
if (APPLE)
# MoltenVK
if (NOT TARGET MoltenVK)
set(MVK_EXCLUDE_SPIRV_TOOLS ON)
set(MVK_USE_METAL_PRIVATE_API ON)
add_subdirectory(MoltenVK)
endif()
if (NOT TARGET epoll-shim)
add_subdirectory(epoll-shim)
endif()
endif()
#windows only

27
flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1774386573,
"narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

160
flake.nix Normal file
View File

@ -0,0 +1,160 @@
## SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
## SPDX-License-Identifier: GPL-2.0-or-later
{
description = "shadPS4 Nix Flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
outputs =
{ self, nixpkgs }:
let
pkgsLinux = nixpkgs.legacyPackages.x86_64-linux;
in
{
devShells.x86_64-linux.default = pkgsLinux.mkShell.override { stdenv = pkgsLinux.clangStdenv; } {
packages = with pkgsLinux; [
clang-tools
cmake
pkg-config
vulkan-tools
renderdoc
gef
strace
openal
zlib.dev
libedit.dev
vulkan-headers
vulkan-utility-libraries
ffmpeg.dev
fmt.dev
glslang.dev
wayland.dev
stb
libpng.dev
libuuid
sdl3.dev
alsa-lib
hidapi
ibus.dev
jack2.dev
libdecor.dev
libthai.dev
fribidi.dev
libxcb.dev
libGL.dev
libpulseaudio.dev
libusb1.dev
libx11.dev
libxcursor.dev
libxext
libxfixes.dev
libxi.dev
libxinerama.dev
libxkbcommon
libxrandr.dev
libxrender.dev
libxtst
pipewire.dev
libxscrnsaver
sndio
];
shellHook = ''
echo "Entering shadPS4 development shell!"
'';
};
linux =
let
execName = "shadps4";
nativeInputs = with pkgsLinux; [
cmake
ninja
pkg-config
magic-enum
fmt
eudev
];
buildInputs = with pkgsLinux; [
boost
cli11
openal
nlohmann_json
vulkan-loader
vulkan-headers
vulkan-memory-allocator
toml11
zlib
zydis
pugixml
ffmpeg
libpulseaudio
pipewire
vulkan-loader
wayland
wayland-scanner
libX11
libxrandr
libxext
libxcursor
libxi
libxscrnsaver
libxtst
libxcb
libdecor
libxkbcommon
libGL
libuuid
];
defaultFlags = [
"-DCMAKE_INSTALL_PREFIX=$out"
];
in
{
debug = pkgsLinux.stdenv.mkDerivation {
pname = "${execName}";
version = "git";
system = "x86_64-linux";
src = ./.;
dontStrip = true;
nativeBuildInputs = nativeInputs;
buildInputs = buildInputs;
cmakeFlags = [
"-DCMAKE_BUILD_TYPE=Debug"
] ++ [defaultFlags];
};
release = pkgsLinux.stdenv.mkDerivation {
pname = "${execName}";
version = "git";
system = "x86_64-linux";
src = ./.;
nativeBuildInputs = nativeInputs;
buildInputs = buildInputs;
cmakeFlags = [
"-DCMAKE_BUILD_TYPE=Release"
] ++ [defaultFlags];
};
releaseWithDebugInfo = pkgsLinux.stdenv.mkDerivation {
pname = "${execName}";
version = "git";
system = "x86_64-linux";
src = ./.;
dontStrip = true;
nativeBuildInputs = nativeInputs;
buildInputs = buildInputs;
cmakeFlags = [
"-DCMAKE_BUILD_TYPE=Release"
] ++ [defaultFlags];
};
};
};
}

View File

@ -3,7 +3,7 @@
#pragma once
#ifdef __linux__
#if __unix__
#include <pthread.h>
#endif

View File

@ -42,7 +42,8 @@ struct Archive {
}
void Advance(size_t size) {
ASSERT(offset + size <= container.size());
ASSERT_MSG(offset + size <= container.size(),
"Invalid or corrupted deserialization container/shader cache");
offset += size;
}
@ -104,7 +105,8 @@ struct Writer {
struct Reader {
template <typename T>
void Read(T* ptr, size_t size) {
ASSERT(ar.offset + size <= ar.container.size());
ASSERT_MSG(ar.offset + size <= ar.container.size(),
"Invalid or corrupted deserialization container/shader cache");
std::memcpy(reinterpret_cast<void*>(ptr), ar.CurrPtr(), size);
ar.Advance(size);
}

View File

@ -7,6 +7,9 @@
#ifdef _WIN32
#include <windows.h>
#elif defined(__FreeBSD__)
#include <machine/npx.h>
#include <sys/ucontext.h>
#else
#include <sys/ucontext.h>
#endif
@ -22,6 +25,16 @@ void* GetXmmPointer(void* ctx, u8 index) {
#define CASE(index) \
case index: \
return (void*)(&((ucontext_t*)ctx)->uc_mcontext->__fs.__fpu_xmm##index);
#elif defined(__FreeBSD__)
// In mc_fpstate
// See <machine/npx.h> for the internals of mc_fpstate[].
#define CASE(index) \
case index: { \
auto& mctx = ((ucontext_t*)ctx)->uc_mcontext; \
ASSERT(mctx.mc_fpformat == _MC_FPFMT_XMM); \
auto* s_fpu = (struct savefpu*)(&mctx.mc_fpstate[0]); \
return (void*)(&(s_fpu->sv_xmm[0])); \
}
#else
#define CASE(index) \
case index: \
@ -57,6 +70,8 @@ void* GetRip(void* ctx) {
return (void*)((EXCEPTION_POINTERS*)ctx)->ContextRecord->Rip;
#elif defined(__APPLE__)
return (void*)((ucontext_t*)ctx)->uc_mcontext->__ss.__rip;
#elif defined(__FreeBSD__)
return (void*)((ucontext_t*)ctx)->uc_mcontext.mc_rip;
#else
return (void*)((ucontext_t*)ctx)->uc_mcontext.gregs[REG_RIP];
#endif
@ -67,6 +82,8 @@ void IncrementRip(void* ctx, u64 length) {
((EXCEPTION_POINTERS*)ctx)->ContextRecord->Rip += length;
#elif defined(__APPLE__)
((ucontext_t*)ctx)->uc_mcontext->__ss.__rip += length;
#elif defined(__FreeBSD__)
((ucontext_t*)ctx)->uc_mcontext.mc_rip += length;
#else
((ucontext_t*)ctx)->uc_mcontext.gregs[REG_RIP] += length;
#endif
@ -75,18 +92,16 @@ void IncrementRip(void* ctx, u64 length) {
bool IsWriteError(void* ctx) {
#if defined(_WIN32)
return ((EXCEPTION_POINTERS*)ctx)->ExceptionRecord->ExceptionInformation[0] == 1;
#elif defined(__APPLE__)
#if defined(ARCH_X86_64)
#elif defined(__APPLE__) && defined(ARCH_X86_64)
return ((ucontext_t*)ctx)->uc_mcontext->__es.__err & 0x2;
#elif defined(ARCH_ARM64)
#elif defined(__APPLE__) && defined(ARCH_ARM64)
return ((ucontext_t*)ctx)->uc_mcontext->__es.__esr & 0x40;
#endif
#else
#if defined(ARCH_X86_64)
#elif defined(__FreeBSD__) && defined(ARCH_X86_64)
return ((ucontext_t*)ctx)->uc_mcontext.mc_err & 0x2;
#elif defined(ARCH_X86_64)
return ((ucontext_t*)ctx)->uc_mcontext.gregs[REG_ERR] & 0x2;
#else
#error "Unsupported architecture"
#endif
#endif
}
} // namespace Common
} // namespace Common

View File

@ -613,7 +613,11 @@ struct AddressSpace::Impl {
user_size = UserSize;
constexpr int protection_flags = PROT_READ | PROT_WRITE;
constexpr int map_flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED;
int map_flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED; // compiler knows its constexpr
#if !defined(__FreeBSD__)
map_flags |= MAP_NORESERVE;
#endif
#if defined(__APPLE__) && defined(ARCH_X86_64)
// On ARM64 Macs, we run into limitations due to the commpage from 0xFC0000000 - 0xFFFFFFFFF
// and the GPU carveout region from 0x1000000000 - 0x6FFFFFFFFF. Because this creates gaps
@ -628,7 +632,7 @@ struct AddressSpace::Impl {
mmap(reinterpret_cast<void*>(USER_MIN), user_size, protection_flags, map_flags, -1, 0));
#else
const auto virtual_size = system_managed_size + system_reserved_size + user_size;
#if defined(ARCH_X86_64)
#if defined(ARCH_X86_64) && !defined(__FreeBSD__)
const auto virtual_base =
reinterpret_cast<u8*>(mmap(reinterpret_cast<void*>(SYSTEM_MANAGED_MIN), virtual_size,
protection_flags, map_flags, -1, 0));
@ -636,8 +640,10 @@ struct AddressSpace::Impl {
system_reserved_base = reinterpret_cast<u8*>(SYSTEM_RESERVED_MIN);
user_base = reinterpret_cast<u8*>(USER_MIN);
#else
// FreeBSD can't stand MAP_FIXED or it may overwrite mmap() itself!
// Map memory wherever possible and instruction translation can handle offsetting to the
// base.
map_flags &= ~MAP_FIXED;
const auto virtual_base =
reinterpret_cast<u8*>(mmap(nullptr, virtual_size, protection_flags, map_flags, -1, 0));
system_managed_base = virtual_base;
@ -676,8 +682,13 @@ struct AddressSpace::Impl {
}
shm_unlink(shm_path.c_str());
#else
#ifndef __FreeBSD__
madvise(virtual_base, virtual_size, MADV_HUGEPAGE);
#endif
// NOTE: If you add MFD_HUGETLB or whatever, remember that FBSD will break (libc bug)
// so please, do not, add MFD_* whatever unless you ifdef it away (must be 0 for FBSD)
// using sized pages as well causes incessant vm_reclaim calls in kernel, do not use on FBSD
// under any circumstances.
backing_fd = memfd_create("BackingDmem", 0);
if (backing_fd < 0) {
LOG_CRITICAL(Kernel_Vmm, "memfd_create failed: {}", strerror(errno));

View File

@ -12,7 +12,7 @@
#elif defined(__linux__)
#include <filesystem>
#include <fstream>
#elif defined(__APPLE__)
#elif defined(__APPLE__) || defined(__FreeBSD__)
#include <errno.h>
#include <signal.h>
#include <sys/sysctl.h>
@ -48,6 +48,8 @@ bool Core::Debugger::IsDebuggerAttached() {
return (info.kp_proc.p_flag & P_TRACED) != 0;
}
return false;
#elif defined(__FreeBSD__)
return false;
#else
#error "Unsupported platform"
#endif
@ -66,7 +68,7 @@ void Core::Debugger::WaitForDebuggerAttach() {
int Core::Debugger::GetCurrentPid() {
#if defined(_WIN32)
return GetCurrentProcessId();
#elif defined(__APPLE__) || defined(__linux__)
#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
return getpid();
#else
#error "Unsupported platform"
@ -88,7 +90,7 @@ void Core::Debugger::WaitForPid(int pid) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cerr << "Waiting for process " << pid << " to exit..." << std::endl;
}
#elif defined(__APPLE__)
#elif defined(__APPLE__) || defined(__FreeBSD__)
while (kill(pid, 0) == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cerr << "Waiting for process " << pid << " to exit..." << std::endl;

View File

@ -5,7 +5,6 @@
#include "common/key_manager.h"
#include "common/logging/log.h"
#include "common/path_util.h"
#include "core/file_format/npbind.h"
#include "core/file_format/trp.h"
static void DecryptEFSM(std::span<const u8, 16> trophyKey, std::span<const u8, 16> NPcommID,
@ -43,8 +42,10 @@ static void hexToBytes(const char* hex, unsigned char* dst) {
}
}
bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string titleId) {
std::filesystem::path gameSysDir = trophyPath / "sce_sys/trophy/";
bool TRP::Extract(const std::filesystem::path& trophyPath, int index, std::string npCommId,
const std::filesystem::path& outputPath) {
std::filesystem::path gameSysDir =
trophyPath / "sce_sys/trophy/" / std::format("trophy{:02d}.trp", index);
if (!std::filesystem::exists(gameSysDir)) {
LOG_WARNING(Common_Filesystem, "Game trophy directory doesn't exist");
return false;
@ -61,117 +62,82 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit
std::array<u8, 16> user_key{};
std::copy(user_key_vec.begin(), user_key_vec.end(), user_key.begin());
// Load npbind.dat using the new class
std::filesystem::path npbindPath = trophyPath / "sce_sys/npbind.dat";
NPBindFile npbind;
if (!npbind.Load(npbindPath.string())) {
LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file");
}
auto npCommIds = npbind.GetNpCommIds();
if (npCommIds.empty()) {
LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat");
}
bool success = true;
int trpFileIndex = 0;
try {
// Process each TRP file in the trophy directory
for (const auto& it : std::filesystem::directory_iterator(gameSysDir)) {
if (!it.is_regular_file() || it.path().extension() != ".trp") {
continue; // Skip non-TRP files
}
const auto& it = gameSysDir;
if (it.extension() != ".trp") {
return false;
}
Common::FS::IOFile file(it, Common::FS::FileAccessMode::Read);
if (!file.IsOpen()) {
LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.string());
return false;
}
// Get NPCommID for this TRP file (if available)
std::string npCommId;
if (trpFileIndex < static_cast<int>(npCommIds.size())) {
npCommId = npCommIds[trpFileIndex];
LOG_DEBUG(Common_Filesystem, "Using NPCommID: {} for {}", npCommId,
it.path().filename().string());
} else {
LOG_WARNING(Common_Filesystem, "No NPCommID found for TRP file index {}",
trpFileIndex);
}
TrpHeader header;
if (!file.Read(header)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}", it.string());
return false;
}
Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read);
if (!file.IsOpen()) {
LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.path().string());
if (header.magic != TRP_MAGIC) {
LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.string());
return false;
}
s64 seekPos = sizeof(TrpHeader);
// Create output directories
if (!std::filesystem::create_directories(outputPath / "Icons") ||
!std::filesystem::create_directories(outputPath / "Xml")) {
LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", npCommId);
return false;
}
// Process each entry in the TRP file
for (int i = 0; i < header.entry_num; i++) {
if (!file.Seek(seekPos)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset");
success = false;
continue;
break;
}
seekPos += static_cast<s64>(header.entry_size);
TrpHeader header;
if (!file.Read(header)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}",
it.path().string());
TrpEntry entry;
if (!file.Read(entry)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP entry");
success = false;
continue;
break;
}
if (header.magic != TRP_MAGIC) {
LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.path().string());
success = false;
continue;
}
std::string_view name(entry.entry_name);
s64 seekPos = sizeof(TrpHeader);
std::filesystem::path trpFilesPath(
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId /
"TrophyFiles" / it.path().stem());
// Create output directories
if (!std::filesystem::create_directories(trpFilesPath / "Icons") ||
!std::filesystem::create_directories(trpFilesPath / "Xml")) {
LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", titleId);
success = false;
continue;
}
// Process each entry in the TRP file
for (int i = 0; i < header.entry_num; i++) {
if (!file.Seek(seekPos)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset");
if (entry.flag == ENTRY_FLAG_PNG) {
if (!ProcessPngEntry(file, entry, outputPath, name)) {
success = false;
break;
// Continue with next entry
}
seekPos += static_cast<s64>(header.entry_size);
TrpEntry entry;
if (!file.Read(entry)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP entry");
success = false;
break;
}
std::string_view name(entry.entry_name);
if (entry.flag == ENTRY_FLAG_PNG) {
if (!ProcessPngEntry(file, entry, trpFilesPath, name)) {
} else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) {
// Check if we have a valid NPCommID for decryption
if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') {
if (!ProcessEncryptedXmlEntry(file, entry, outputPath, name, user_key,
npCommId)) {
success = false;
// Continue with next entry
}
} else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) {
// Check if we have a valid NPCommID for decryption
if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') {
if (!ProcessEncryptedXmlEntry(file, entry, trpFilesPath, name, user_key,
npCommId)) {
success = false;
// Continue with next entry
}
} else {
LOG_WARNING(Common_Filesystem,
"Skipping encrypted XML entry - invalid NPCommID");
// Skip this entry but continue
}
} else {
LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}",
static_cast<unsigned int>(entry.flag), name);
LOG_WARNING(Common_Filesystem,
"Skipping encrypted XML entry - invalid NPCommID");
// Skip this entry but continue
}
} else {
LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}",
static_cast<unsigned int>(entry.flag), name);
}
trpFileIndex++;
}
} catch (const std::filesystem::filesystem_error& e) {
LOG_CRITICAL(Common_Filesystem, "Filesystem error during trophy extraction: {}", e.what());
return false;
@ -182,7 +148,7 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit
if (success) {
LOG_INFO(Common_Filesystem, "Successfully extracted {} trophy files for {}", trpFileIndex,
titleId);
npCommId);
}
return success;

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -36,7 +36,8 @@ class TRP {
public:
TRP();
~TRP();
bool Extract(const std::filesystem::path& trophyPath, const std::string titleId);
bool Extract(const std::filesystem::path& trophyPath, int index, std::string npCommId,
const std::filesystem::path& outputPath);
private:
bool ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry,
@ -45,9 +46,6 @@ private:
const std::filesystem::path& outputPath, std::string_view name,
const std::array<u8, 16>& user_key, const std::string& npCommId);
std::vector<u8> NPcommID = std::vector<u8>(12);
std::array<u8, 16> np_comm_id{};
std::array<u8, 16> esfmIv{};
std::filesystem::path trpFilesPath;
static constexpr int iv_len = 16;
};

View File

@ -42,7 +42,7 @@ void MntPoints::UnmountAll() {
}
std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_read_only,
bool force_base_path) {
HostPathType path_type) {
// Evil games like Turok2 pass double slashes e.g /app0//game.kpf
std::string corrected_path(path);
size_t pos = corrected_path.find("//");
@ -80,8 +80,24 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea
}
patch_path /= rel_path;
std::filesystem::path mods_path = mount->host_path;
mods_path += "-mods";
mods_path /= rel_path;
if (path_type == HostPathType::Mod) {
return mods_path;
} else if (path_type == HostPathType::Patch) {
return patch_path;
}
if ((corrected_path.starts_with("/app0") || corrected_path.starts_with("/hostapp")) &&
!force_base_path && !ignore_game_patches && std::filesystem::exists(patch_path)) {
path_type != HostPathType::Base && std::filesystem::exists(mods_path)) {
return mods_path;
}
if ((corrected_path.starts_with("/app0") || corrected_path.starts_with("/hostapp")) &&
path_type != HostPathType::Base && !ignore_game_patches &&
std::filesystem::exists(patch_path)) {
return patch_path;
}
@ -95,7 +111,7 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea
std::scoped_lock lk{m_mutex};
path_parts.clear();
auto current_path = host_path;
while (!std::filesystem::exists(current_path)) {
while (!current_path.empty() && !std::filesystem::exists(current_path)) {
// We have probably cached this if it's a folder.
if (auto it = path_cache.find(current_path); it != path_cache.end()) {
current_path = it->second;
@ -104,44 +120,46 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea
path_parts.emplace_back(current_path.filename());
current_path = current_path.parent_path();
}
// We have found an anchor. Traverse parts we recoded and see if they
// exist in filesystem but in different case.
auto guest_path = current_path;
while (!path_parts.empty()) {
const auto part = path_parts.back();
const auto add_match = [&](const auto& host_part) {
current_path /= host_part;
guest_path /= part;
path_cache[guest_path] = current_path;
path_parts.pop_back();
};
// Can happen when the mismatch is in upper folder.
if (std::filesystem::exists(current_path / part)) {
add_match(part);
continue;
}
const auto part_low = Common::ToLower(part.string());
bool found_match = false;
for (const auto& path : std::filesystem::directory_iterator(current_path)) {
const auto candidate = path.path().filename();
const auto filename = Common::ToLower(candidate.string());
// Check if a filename matches in case insensitive manner.
if (filename != part_low) {
if (!current_path.empty()) {
// We have found an anchor. Traverse parts we recoded and see if they
// exist in filesystem but in different case.
auto guest_path = current_path;
while (!path_parts.empty()) {
const auto part = path_parts.back();
const auto add_match = [&](const auto& host_part) {
current_path /= host_part;
guest_path /= part;
path_cache[guest_path] = current_path;
path_parts.pop_back();
};
// Can happen when the mismatch is in upper folder.
if (std::filesystem::exists(current_path / part)) {
add_match(part);
continue;
}
// We found a match, record the actual path in the cache.
add_match(candidate);
found_match = true;
break;
}
if (!found_match) {
return std::optional<std::filesystem::path>({});
const auto part_low = Common::ToLower(part.string());
bool found_match = false;
for (const auto& path : std::filesystem::directory_iterator(current_path)) {
const auto candidate = path.path().filename();
const auto filename = Common::ToLower(candidate.string());
// Check if a filename matches in case insensitive manner.
if (filename != part_low) {
continue;
}
// We found a match, record the actual path in the cache.
add_match(candidate);
found_match = true;
break;
}
if (!found_match) {
return std::optional<std::filesystem::path>({});
}
}
}
return std::optional<std::filesystem::path>(current_path);
};
if (!force_base_path && !ignore_game_patches) {
if (path_type != HostPathType::Base && !ignore_game_patches) {
if (const auto path = search(patch_path)) {
return *path;
}
@ -158,34 +176,54 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea
// TODO: Does not handle mount points inside mount points.
void MntPoints::IterateDirectory(std::string_view guest_directory,
const IterateDirectoryCallback& callback) {
const auto base_path = GetHostPath(guest_directory, nullptr, true);
const auto patch_path = GetHostPath(guest_directory, nullptr, false);
// Only need to consider patch path if it exists and does not resolve to the same as base.
const auto apply_patch = base_path != patch_path && std::filesystem::exists(patch_path);
const auto base_path = GetHostPath(guest_directory, nullptr, HostPathType::Base);
// Forces path types so as not to resolve to base path
const auto patch_path = GetHostPath(guest_directory, nullptr, HostPathType::Patch);
const auto mod_path = GetHostPath(guest_directory, nullptr, HostPathType::Mod);
// Prepend entries for . and .., as both are treated as files on PS4.
callback(base_path / ".", false);
callback(base_path / "..", false);
// Pass 1: Any files that existed in the base directory, using patch directory if needed.
// Pass 1: Any files that existed in the base directory, using mod/patch directory if needed.
if (std::filesystem::exists(base_path)) {
for (const auto& entry : std::filesystem::directory_iterator(base_path)) {
if (apply_patch) {
const auto patch_entry_path = patch_path / entry.path().filename();
if (std::filesystem::exists(patch_entry_path)) {
callback(patch_entry_path, !std::filesystem::is_directory(patch_entry_path));
continue;
}
const auto mod_entry_path = mod_path / entry.path().filename();
const auto patch_entry_path = patch_path / entry.path().filename();
if (std::filesystem::exists(mod_entry_path)) {
callback(mod_entry_path, !std::filesystem::is_directory(mod_entry_path));
continue;
} else if (std::filesystem::exists(patch_entry_path)) {
callback(patch_entry_path, !std::filesystem::is_directory(patch_entry_path));
continue;
}
callback(entry.path(), !entry.is_directory());
}
}
// Pass 2: Any files that exist only in the patch directory.
if (apply_patch) {
if (std::filesystem::exists(patch_path)) {
for (const auto& entry : std::filesystem::directory_iterator(patch_path)) {
const auto base_entry_path = base_path / entry.path().filename();
if (!std::filesystem::exists(base_entry_path)) {
const auto mod_entry_path = mod_path / entry.path().filename();
if (std::filesystem::exists(mod_entry_path)) {
callback(mod_entry_path, !std::filesystem::is_directory(mod_entry_path));
continue;
}
callback(entry.path(), !entry.is_directory());
}
}
}
// Pass 3: Any files that exist only in the mod directory (confirmed this can be valid)
if (std::filesystem::exists(mod_path)) {
for (const auto& entry : std::filesystem::directory_iterator(mod_path)) {
const auto base_entry_path = base_path / entry.path().filename();
const auto patch_entry_path = patch_path / entry.path().filename();
if (!std::filesystem::exists(base_entry_path) &&
!std::filesystem::exists(patch_entry_path)) {
callback(entry.path(), !entry.is_directory());
}
}

View File

@ -36,6 +36,13 @@ public:
bool read_only;
};
enum class HostPathType {
Default, // Prioritizes Mod, then patch, then base
Base,
Patch,
Mod
};
explicit MntPoints() = default;
~MntPoints() = default;
@ -45,7 +52,8 @@ public:
void UnmountAll();
std::filesystem::path GetHostPath(std::string_view guest_directory,
bool* is_read_only = nullptr, bool force_base_path = false);
bool* is_read_only = nullptr,
HostPathType host_path = HostPathType::Default);
using IterateDirectoryCallback =
std::function<void(const std::filesystem::path& host_path, bool is_file)>;
void IterateDirectory(std::string_view guest_directory,

View File

@ -211,13 +211,6 @@ void IPC::InputLoop() {
} else if (cmd == "RELOAD_INPUTS") {
std::string config = next_str();
Input::ParseInputConfig(config);
} else if (cmd == "SET_ACTIVE_CONTROLLER") {
std::string active_controller = next_str();
GamepadSelect::SetSelectedGamepad(active_controller);
SDL_Event checkGamepad;
SDL_memset(&checkGamepad, 0, sizeof(checkGamepad));
checkGamepad.type = SDL_EVENT_CHANGE_CONTROLLER;
SDL_PushEvent(&checkGamepad);
} else {
std::cerr << ";UNKNOWN CMD: " << cmd << std::endl;
}

View File

@ -298,6 +298,126 @@ s32 PS4_SYSV_ABI sceKernelGetAppInfo(s32 pid, OrbisKernelAppInfo* app_info) {
return ORBIS_OK;
}
// Nominally: long sysconf(int name);
u64 PS4_SYSV_ABI posix_sysconf(s32 name) {
switch (name) {
case 0:
return 0x20000;
case POSIX_SC_ARG_MAX:
return 0x588bc000;
case POSIX_SC_CHILD_MAX:
return 0x64;
case POSIX_SC_CLK_TCK:
return 0x20;
case POSIX_SC_NGROUPS_MAX:
return 0x644;
case POSIX_SC_OPEN_MAX:
return -0x1;
case POSIX_SC_JOB_CONTROL:
return 0x6;
case POSIX_SC_SAVED_IDS:
return 0x1;
case POSIX_SC_VERSION:
return 0x1;
case POSIX_SC_BC_BASE_MAX:
return 0x31069;
case POSIX_SC_BC_DIM_MAX:
return -0x1;
case POSIX_SC_BC_SCALE_MAX:
return 0x31069;
case POSIX_SC_BC_STRING_MAX:
return 0x31069;
case POSIX_SC_COLL_WEIGHTS_MAX:
return -0x1;
case POSIX_SC_EXPR_NEST_MAX:
return -0x1;
case POSIX_SC_LINE_MAX:
return 0x31069;
case POSIX_SC_RE_DUP_MAX:
return 0x31069;
case POSIX_SC_2_VERSION:
return 0x31069;
case POSIX_SC_2_C_BIND:
return 0x31069;
case POSIX_SC_2_C_DEV:
return 0x31069;
case POSIX_SC_2_CHAR_TERM:
return 0x31069;
case POSIX_SC_2_FORT_DEV:
return 0x31069;
case POSIX_SC_2_FORT_RUN:
return 0x31069;
case POSIX_SC_2_LOCALEDEF:
return -0x1;
case POSIX_SC_2_SW_DEV:
return -0x1;
case POSIX_SC_2_UPE:
return 0x0;
case POSIX_SC_STREAM_MAX:
return 0x7fffffff;
case POSIX_SC_TZNAME_MAX:
return -0x1;
case POSIX_SC_ASYNCHRONOUS_IO:
return 0x8000;
case POSIX_SC_MAPPED_FILES:
return 0x31069;
case POSIX_SC_MEMLOCK:
return 0x4000;
case POSIX_SC_MEMLOCK_RANGE:
return 0x1e;
case POSIX_SC_MEMORY_PROTECTION:
return 0x100;
case POSIX_SC_MESSAGE_PASSING:
return 0x7fffffff;
case POSIX_SC_PRIORITIZED_IO:
return -0x1;
case POSIX_SC_PRIORITY_SCHEDULING:
return -0x1;
case POSIX_SC_REALTIME_SIGNALS:
return 0x63;
case POSIX_SC_SEMAPHORES:
return 0x800;
case POSIX_SC_FSYNC:
return 0x63;
case POSIX_SC_SHARED_MEMORY_OBJECTS:
return 0x3e8;
case POSIX_SC_SYNCHRONIZED_IO:
return 0x2;
case POSIX_SC_THREAD_ATTR_STACKSIZE:
return 0x1;
case POSIX_SC_THREAD_CPUTIME:
return 0x1;
case POSIX_SC_THREAD_DESTRUCTOR_ITERATIONS:
return 0x48000;
case POSIX_SC_THREAD_KEYS_MAX:
return 0x1a078630b2dd7;
case POSIX_SC_THREAD_PRIO_INHERIT:
return -0x1;
case POSIX_SC_THREAD_PRIO_PROTECT:
return -0x1;
case POSIX_SC_THREAD_PRIORITY_SCHEDULING:
return 0x2bc;
case POSIX_SC_THREAD_PROCESS_SHARED:
return 0x2bc;
case POSIX_SC_THREAD_SAFE_FUNCTIONS:
return 0x1;
case POSIX_SC_THREAD_SPORADIC_SERVER:
return -0x1;
case POSIX_SC_THREAD_STACK_MIN:
return 0x1;
case POSIX_SC_THREAD_THREADS_MAX:
return 0x1;
case POSIX_SC_TIMEOUTS:
return -0x1;
// Manually specified
case POSIX_SC_PAGESIZE:
return posix_getpagesize();
default:
LOG_ERROR(Lib_Kernel, "unhandled {}", name);
return 0;
}
}
void RegisterLib(Core::Loader::SymbolsResolver* sym) {
service_thread = std::jthread{KernelServiceThread};
@ -327,6 +447,10 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("9BcDykPmo1I", "libkernel", 1, "libkernel", __Error);
LIB_FUNCTION("k+AXqu2-eBc", "libkernel", 1, "libkernel", posix_getpagesize);
LIB_FUNCTION("k+AXqu2-eBc", "libScePosix", 1, "libkernel", posix_getpagesize);
LIB_FUNCTION("mkawd0NA9ts", "libkernel", 1, "libkernel", posix_sysconf);
LIB_FUNCTION("mkawd0NA9ts", "libScePosix", 1, "libkernel", posix_sysconf);
LIB_FUNCTION("NWtTN10cJzE", "libSceLibcInternalExt", 1, "libSceLibcInternal",
sceLibcHeapGetTraceInfo);

View File

@ -82,4 +82,126 @@ struct OrbisKernelAppInfo {
void RegisterLib(Core::Loader::SymbolsResolver* sym);
constexpr u32 POSIX_SC_ARG_MAX = 1;
constexpr u32 POSIX_SC_CHILD_MAX = 2;
constexpr u32 POSIX_SC_CLK_TCK = 3;
constexpr u32 POSIX_SC_NGROUPS_MAX = 4;
constexpr u32 POSIX_SC_OPEN_MAX = 5;
constexpr u32 POSIX_SC_JOB_CONTROL = 6;
constexpr u32 POSIX_SC_SAVED_IDS = 7;
constexpr u32 POSIX_SC_VERSION = 8;
constexpr u32 POSIX_SC_BC_BASE_MAX = 9;
constexpr u32 POSIX_SC_BC_DIM_MAX = 10;
constexpr u32 POSIX_SC_BC_SCALE_MAX = 11;
constexpr u32 POSIX_SC_BC_STRING_MAX = 12;
constexpr u32 POSIX_SC_COLL_WEIGHTS_MAX = 13;
constexpr u32 POSIX_SC_EXPR_NEST_MAX = 14;
constexpr u32 POSIX_SC_LINE_MAX = 15;
constexpr u32 POSIX_SC_RE_DUP_MAX = 16;
constexpr u32 POSIX_SC_2_VERSION = 17;
constexpr u32 POSIX_SC_2_C_BIND = 18;
constexpr u32 POSIX_SC_2_C_DEV = 19;
constexpr u32 POSIX_SC_2_CHAR_TERM = 20;
constexpr u32 POSIX_SC_2_FORT_DEV = 21;
constexpr u32 POSIX_SC_2_FORT_RUN = 22;
constexpr u32 POSIX_SC_2_LOCALEDEF = 23;
constexpr u32 POSIX_SC_2_SW_DEV = 24;
constexpr u32 POSIX_SC_2_UPE = 25;
constexpr u32 POSIX_SC_STREAM_MAX = 26;
constexpr u32 POSIX_SC_TZNAME_MAX = 27;
constexpr u32 POSIX_SC_ASYNCHRONOUS_IO = 28;
constexpr u32 POSIX_SC_MAPPED_FILES = 29;
constexpr u32 POSIX_SC_MEMLOCK = 30;
constexpr u32 POSIX_SC_MEMLOCK_RANGE = 31;
constexpr u32 POSIX_SC_MEMORY_PROTECTION = 32;
constexpr u32 POSIX_SC_MESSAGE_PASSING = 33;
constexpr u32 POSIX_SC_PRIORITIZED_IO = 34;
constexpr u32 POSIX_SC_PRIORITY_SCHEDULING = 35;
constexpr u32 POSIX_SC_REALTIME_SIGNALS = 36;
constexpr u32 POSIX_SC_SEMAPHORES = 37;
constexpr u32 POSIX_SC_FSYNC = 38;
constexpr u32 POSIX_SC_SHARED_MEMORY_OBJECTS = 39;
constexpr u32 POSIX_SC_SYNCHRONIZED_IO = 40;
constexpr u32 POSIX_SC_TIMERS = 41;
constexpr u32 POSIX_SC_AIO_LISTIO_MAX = 42;
constexpr u32 POSIX_SC_AIO_MAX = 43;
constexpr u32 POSIX_SC_AIO_PRIO_DELTA_MAX = 44;
constexpr u32 POSIX_SC_DELAYTIMER_MAX = 45;
constexpr u32 POSIX_SC_MQ_OPEN_MAX = 46;
constexpr u32 POSIX_SC_PAGESIZE = 47;
constexpr u32 POSIX_SC_RTSIG_MAX = 48;
constexpr u32 POSIX_SC_SEM_NSEMS_MAX = 49;
constexpr u32 POSIX_SC_SEM_VALUE_MAX = 50;
constexpr u32 POSIX_SC_SIGQUEUE_MAX = 51;
constexpr u32 POSIX_SC_TIMER_MAX = 52;
constexpr u32 POSIX_SC_2_PBS = 59;
constexpr u32 POSIX_SC_2_PBS_ACCOUNTING = 60;
constexpr u32 POSIX_SC_2_PBS_CHECKPOINT = 61;
constexpr u32 POSIX_SC_2_PBS_LOCATE = 62;
constexpr u32 POSIX_SC_2_PBS_MESSAGE = 63;
constexpr u32 POSIX_SC_2_PBS_TRACK = 64;
constexpr u32 POSIX_SC_ADVISORY_INFO = 65;
constexpr u32 POSIX_SC_BARRIERS = 66;
constexpr u32 POSIX_SC_CLOCK_SELECTION = 67;
constexpr u32 POSIX_SC_CPUTIME = 68;
constexpr u32 POSIX_SC_FILE_LOCKING = 69;
constexpr u32 POSIX_SC_GETGR_R_SIZE_MAX = 70;
constexpr u32 POSIX_SC_GETPW_R_SIZE_MAX = 71;
constexpr u32 POSIX_SC_HOST_NAME_MAX = 72;
constexpr u32 POSIX_SC_LOGIN_NAME_MAX = 73;
constexpr u32 POSIX_SC_MONOTONIC_CLOCK = 74;
constexpr u32 POSIX_SC_MQ_PRIO_MAX = 75;
constexpr u32 POSIX_SC_READER_WRITER_LOCKS = 76;
constexpr u32 POSIX_SC_REGEXP = 77;
constexpr u32 POSIX_SC_SHELL = 78;
constexpr u32 POSIX_SC_SPAWN = 79;
constexpr u32 POSIX_SC_SPIN_LOCKS = 80;
constexpr u32 POSIX_SC_SPORADIC_SERVER = 81;
constexpr u32 POSIX_SC_THREAD_ATTR_STACKADDR = 82;
constexpr u32 POSIX_SC_THREAD_ATTR_STACKSIZE = 83;
constexpr u32 POSIX_SC_THREAD_CPUTIME = 84;
constexpr u32 POSIX_SC_THREAD_DESTRUCTOR_ITERATIONS = 85;
constexpr u32 POSIX_SC_THREAD_KEYS_MAX = 86;
constexpr u32 POSIX_SC_THREAD_PRIO_INHERIT = 87;
constexpr u32 POSIX_SC_THREAD_PRIO_PROTECT = 88;
constexpr u32 POSIX_SC_THREAD_PRIORITY_SCHEDULING = 89;
constexpr u32 POSIX_SC_THREAD_PROCESS_SHARED = 90;
constexpr u32 POSIX_SC_THREAD_SAFE_FUNCTIONS = 91;
constexpr u32 POSIX_SC_THREAD_SPORADIC_SERVER = 92;
constexpr u32 POSIX_SC_THREAD_STACK_MIN = 93;
constexpr u32 POSIX_SC_THREAD_THREADS_MAX = 94;
constexpr u32 POSIX_SC_TIMEOUTS = 95;
constexpr u32 POSIX_SC_THREADS = 96;
constexpr u32 POSIX_SC_TRACE = 97;
constexpr u32 POSIX_SC_TRACE_EVENT_FILTER = 98;
constexpr u32 POSIX_SC_TRACE_INHERIT = 99;
constexpr u32 POSIX_SC_TRACE_LOG = 100;
constexpr u32 POSIX_SC_TTY_NAME_MAX = 101;
constexpr u32 POSIX_SC_TYPED_MEMORY_OBJECTS = 102;
constexpr u32 POSIX_SC_V6_ILP32_OFF32 = 103;
constexpr u32 POSIX_SC_V6_ILP32_OFFBIG = 104;
constexpr u32 POSIX_SC_V6_LP64_OFF64 = 105;
constexpr u32 POSIX_SC_V6_LPBIG_OFFBIG = 106;
constexpr u32 POSIX_SC_IPV6 = 118;
constexpr u32 POSIX_SC_RAW_SOCKETS = 119;
constexpr u32 POSIX_SC_SYMLOOP_MAX = 120;
constexpr u32 POSIX_SC_ATEXIT_MAX = 107;
constexpr u32 POSIX_SC_IOV_MAX = 56;
constexpr u32 POSIX_SC_XOPEN_CRYPT = 108;
constexpr u32 POSIX_SC_XOPEN_ENH_I18N = 109;
constexpr u32 POSIX_SC_XOPEN_LEGACY = 110;
constexpr u32 POSIX_SC_XOPEN_REALTIME = 111;
constexpr u32 POSIX_SC_XOPEN_REALTIME_THREADS = 112;
constexpr u32 POSIX_SC_XOPEN_SHM = 113;
constexpr u32 POSIX_SC_XOPEN_STREAMS = 114;
constexpr u32 POSIX_SC_XOPEN_UNIX = 115;
constexpr u32 POSIX_SC_XOPEN_VERSION = 116;
constexpr u32 POSIX_SC_XOPEN_XCU_VERSION = 117;
constexpr u32 POSIX_SC_NPROCESSORS_CONF = 57;
constexpr u32 POSIX_SC_NPROCESSORS_ONLN = 58;
constexpr u32 POSIX_SC_CPUSET_SIZE = 122;
constexpr u32 POSIX_SC_UEXTERR_MAXLEN = 123;
constexpr u32 POSIX_SC_NSIG = 124;
constexpr u32 POSIX_SC_PHYS_PAGES = 121;
} // namespace Libraries::Kernel

View File

@ -339,7 +339,11 @@ s32 PS4_SYSV_ABI sceKernelMprotect(const void* addr, u64 size, s32 prot) {
Core::MemoryManager* memory_manager = Core::Memory::Instance();
Core::MemoryProt protection_flags = static_cast<Core::MemoryProt>(prot);
return memory_manager->Protect(aligned_addr, aligned_size, protection_flags);
s32 result = memory_manager->Protect(aligned_addr, aligned_size, protection_flags);
if (result == ORBIS_OK) {
memory_manager->InvalidateMemory(aligned_addr, aligned_size);
}
return result;
}
s32 PS4_SYSV_ABI posix_mprotect(const void* addr, u64 size, s32 prot) {
@ -370,6 +374,7 @@ s32 PS4_SYSV_ABI sceKernelMtypeprotect(const void* addr, u64 size, s32 mtype, s3
s32 result = memory_manager->Protect(aligned_addr, aligned_size, protection_flags);
if (result == ORBIS_OK) {
memory_manager->SetDirectMemoryType(aligned_addr, aligned_size, mtype);
memory_manager->InvalidateMemory(aligned_addr, aligned_size);
}
return result;
}

View File

@ -215,6 +215,28 @@ void SigactionHandler(int native_signum, siginfo_t* inf, ucontext_t* raw_context
ctx.uc_mcontext.mc_gs = regs.__gs;
ctx.uc_mcontext.mc_rip = regs.__rip;
ctx.uc_mcontext.mc_addr = reinterpret_cast<uint64_t>(inf->si_addr);
#elif defined(__FreeBSD__)
const auto& regs = raw_context->uc_mcontext;
ctx.uc_mcontext.mc_r8 = regs.mc_r8;
ctx.uc_mcontext.mc_r9 = regs.mc_r9;
ctx.uc_mcontext.mc_r10 = regs.mc_r10;
ctx.uc_mcontext.mc_r11 = regs.mc_r11;
ctx.uc_mcontext.mc_r12 = regs.mc_r12;
ctx.uc_mcontext.mc_r13 = regs.mc_r13;
ctx.uc_mcontext.mc_r14 = regs.mc_r14;
ctx.uc_mcontext.mc_r15 = regs.mc_r15;
ctx.uc_mcontext.mc_rdi = regs.mc_rdi;
ctx.uc_mcontext.mc_rsi = regs.mc_rsi;
ctx.uc_mcontext.mc_rbp = regs.mc_rbp;
ctx.uc_mcontext.mc_rbx = regs.mc_rbx;
ctx.uc_mcontext.mc_rdx = regs.mc_rdx;
ctx.uc_mcontext.mc_rax = regs.mc_rax;
ctx.uc_mcontext.mc_rcx = regs.mc_rcx;
ctx.uc_mcontext.mc_rsp = regs.mc_rsp;
ctx.uc_mcontext.mc_fs = regs.mc_fs;
ctx.uc_mcontext.mc_gs = regs.mc_gs;
ctx.uc_mcontext.mc_rip = regs.mc_rip;
ctx.uc_mcontext.mc_addr = uint64_t(regs.mc_addr);
#else
const auto& regs = raw_context->uc_mcontext.gregs;
ctx.uc_mcontext.mc_r8 = regs[REG_R8];
@ -286,6 +308,28 @@ bool PS4_SYSV_ABI posix_sigisemptyset(Sigset* s) {
return s->bits[0] == 0 && s->bits[1] == 0;
}
s32 PS4_SYSV_ABI posix_sigalstack(const OrbisKernelExceptionHandlerStack* ss,
OrbisKernelExceptionHandlerStack* old_ss) {
#ifdef __unix__
stack_t native_ss{};
if (ss) {
native_ss.ss_sp = ss->ss_sp;
native_ss.ss_flags = ss->ss_flags;
native_ss.ss_size = ss->ss_size;
}
stack_t native_old_ss{};
sigaltstack(&native_ss, &native_old_ss);
if (old_ss) {
old_ss->ss_sp = native_old_ss.ss_sp;
old_ss->ss_flags = native_old_ss.ss_flags;
old_ss->ss_size = native_old_ss.ss_size;
}
#else
LOG_ERROR(Lib_Kernel, "(stubbed)");
#endif
return ORBIS_OK;
}
s32 PS4_SYSV_ABI posix_sigaction(s32 sig, Sigaction* act, Sigaction* oact) {
if (sig < 1 || sig > 128 || sig == POSIX_SIGTHR || sig == POSIX_SIGKILL ||
sig == POSIX_SIGSTOP) {
@ -303,7 +347,7 @@ s32 PS4_SYSV_ABI posix_sigaction(s32 sig, Sigaction* act, Sigaction* oact) {
*__Error() = POSIX_EINVAL;
return ORBIS_FAIL;
}
#ifndef __APPLE__
#if !defined(__APPLE__) && !defined(__FreeBSD__)
if (native_sig >= __SIGRTMIN && native_sig < SIGRTMIN) {
LOG_ERROR(Lib_Kernel, "Guest is attempting to use the HLE libc-reserved signal {}!", sig);
*__Error() = POSIX_EINVAL;
@ -473,9 +517,12 @@ void RegisterException(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("KiJEPEWRyUY", "libkernel", 1, "libkernel", posix_sigaction);
LIB_FUNCTION("+F7C-hdk7+E", "libkernel", 1, "libkernel", posix_sigemptyset);
LIB_FUNCTION("yH-uQW3LbX0", "libkernel", 1, "libkernel", posix_pthread_kill);
LIB_FUNCTION("sHziAegVp74", "libkernel", 1, "libkernel", posix_sigalstack);
LIB_FUNCTION("KiJEPEWRyUY", "libScePosix", 1, "libkernel", posix_sigaction);
LIB_FUNCTION("+F7C-hdk7+E", "libScePosix", 1, "libkernel", posix_sigemptyset);
LIB_FUNCTION("yH-uQW3LbX0", "libScePosix", 1, "libkernel", posix_pthread_kill);
LIB_FUNCTION("sHziAegVp74", "libScePosix", 1, "libkernel", posix_sigalstack);
}
} // namespace Libraries::Kernel

View File

@ -12,6 +12,11 @@ class SymbolsResolver;
namespace Libraries::Kernel {
using OrbisKernelExceptionHandler = PS4_SYSV_ABI void (*)(int, void*);
struct OrbisKernelExceptionHandlerStack {
void* ss_sp;
int ss_flags;
size_t ss_size;
};
constexpr s32 POSIX_SIGHUP = 1;
constexpr s32 POSIX_SIGINT = 2;
@ -47,7 +52,7 @@ constexpr s32 POSIX_SIGUSR2 = 31;
constexpr s32 POSIX_SIGTHR = 32;
constexpr s32 POSIX_SIGLIBRT = 33;
#ifdef __linux__
#if defined(__linux__) || defined(__FreeBSD__)
constexpr s32 _SIGEMT = 128;
constexpr s32 _SIGINFO = 129;
#elif !defined(_WIN32)

View File

@ -17,7 +17,7 @@
#include <windows.h>
#include "common/ntapi.h"
#else
#ifdef __APPLE__
#if defined(__APPLE__) || defined(__FreeBSD__)
#include <date/tz.h>
#endif
#include <ctime>
@ -501,7 +501,7 @@ s32 PS4_SYSV_ABI sceKernelConvertUtcToLocaltime(time_t time, time_t* local_time,
*dst_sec = res == TIME_ZONE_ID_DAYLIGHT ? -_dstbias : 0;
}
#else
#ifdef __APPLE__
#if defined(__APPLE__) || defined(__FreeBSD__)
// std::chrono::current_zone() not available yet.
const auto* time_zone = date::current_zone();
#else

View File

@ -63,7 +63,6 @@
#include "core/libraries/system/posix.h"
#include "core/libraries/system/systemservice.h"
#include "core/libraries/system/userservice.h"
#include "core/libraries/system_gesture/system_gesture.h"
#include "core/libraries/ulobjmgr/ulobjmgr.h"
#include "core/libraries/usbd/usbd.h"
#include "core/libraries/videodec/videodec.h"
@ -120,7 +119,6 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) {
Libraries::Random::RegisterLib(sym);
Libraries::Usbd::RegisterLib(sym);
Libraries::Pad::RegisterLib(sym);
Libraries::SystemGesture::RegisterLib(sym);
Libraries::Ajm::RegisterLib(sym);
Libraries::ErrorDialog::RegisterLib(sym);
Libraries::ImeDialog::RegisterLib(sym);

View File

@ -663,10 +663,12 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or
return ORBIS_NET_ERROR_EBADF;
}
#ifndef __FreeBSD__
epoll_event native_event = {.events = ConvertEpollEventsIn(event->events),
.data = {.fd = id}};
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_ADD, *native_handle, &native_event) == 0);
epoll->events.emplace_back(id, *event);
#endif
break;
}
case Core::FileSys::FileType::Resolver: {
@ -711,10 +713,12 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or
return ORBIS_NET_ERROR_EBADF;
}
#ifndef __FreeBSD__
epoll_event native_event = {.events = ConvertEpollEventsIn(event->events),
.data = {.fd = id}};
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_MOD, *native_handle, &native_event) == 0);
*it = {id, *event};
#endif
break;
}
default:
@ -752,9 +756,10 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or
*sceNetErrnoLoc() = ORBIS_NET_EBADF;
return ORBIS_NET_ERROR_EBADF;
}
#ifndef __FreeBSD__
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_DEL, *native_handle, nullptr) == 0);
epoll->events.erase(it);
#endif
break;
}
case Core::FileSys::FileType::Resolver: {
@ -810,6 +815,9 @@ int PS4_SYSV_ABI sceNetEpollDestroy(OrbisNetId epollid) {
int PS4_SYSV_ABI sceNetEpollWait(OrbisNetId epollid, OrbisNetEpollEvent* events, int maxevents,
int timeout) {
#ifdef __FreeBSD__
return 0;
#else
auto file = FDTable::Instance()->GetEpoll(epollid);
if (!file) {
*sceNetErrnoLoc() = ORBIS_NET_EBADF;
@ -836,7 +844,6 @@ int PS4_SYSV_ABI sceNetEpollWait(OrbisNetId epollid, OrbisNetEpollEvent* events,
}
int i = 0;
if (result < 0) {
LOG_ERROR(Lib_Net, "epoll_wait failed with {}", Common::GetLastErrorMsg());
switch (errno) {
@ -905,8 +912,8 @@ int PS4_SYSV_ABI sceNetEpollWait(OrbisNetId epollid, OrbisNetEpollEvent* events,
++i;
}
}
return i;
#endif
}
int* PS4_SYSV_ABI sceNetErrnoLoc() {

View File

@ -10,12 +10,14 @@ namespace Libraries::Net {
u32 ConvertEpollEventsIn(u32 orbis_events) {
u32 ret = 0;
#ifndef __FreeBSD__
if ((orbis_events & ORBIS_NET_EPOLLIN) != 0) {
ret |= EPOLLIN;
}
if ((orbis_events & ORBIS_NET_EPOLLOUT) != 0) {
ret |= EPOLLOUT;
}
#endif
return ret;
}
@ -23,6 +25,7 @@ u32 ConvertEpollEventsIn(u32 orbis_events) {
u32 ConvertEpollEventsOut(u32 epoll_events) {
u32 ret = 0;
#ifndef __FreeBSD__
if ((epoll_events & EPOLLIN) != 0) {
ret |= ORBIS_NET_EPOLLIN;
}
@ -35,6 +38,7 @@ u32 ConvertEpollEventsOut(u32 epoll_events) {
if ((epoll_events & EPOLLHUP) != 0) {
ret |= ORBIS_NET_EPOLLHUP;
}
#endif
return ret;
}

View File

@ -14,7 +14,8 @@
#include <wepoll.h>
#endif
#if defined(__linux__) || defined(__APPLE__)
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
// ADD libepoll-shim if using freebsd!
#include <sys/epoll.h>
#include <unistd.h>
#endif
@ -82,4 +83,4 @@ private:
std::mutex m_mutex;
};
} // namespace Libraries::Net
} // namespace Libraries::Net

View File

@ -25,7 +25,7 @@ typedef int net_socket;
#include <net/if_dl.h>
#include <net/route.h>
#endif
#if __linux__
#if defined(__linux__) || defined(__FreeBSD__)
#include <fstream>
#include <iostream>
#include <sstream>
@ -81,6 +81,8 @@ bool NetUtilInternal::RetrieveEthernetAddr() {
}
freeifaddrs(ifap);
}
#elif defined(__FreeBSD__)
// todo
#else
ifreq ifr;
ifconf ifc;
@ -226,7 +228,8 @@ bool NetUtilInternal::RetrieveDefaultGateway() {
inet_ntop(AF_INET, gateAddr, str, sizeof(str));
this->default_gateway = str;
return true;
#elif defined(__FreeBSD__)
return true;
#else
std::ifstream route{"/proc/net/route"};
std::string line;
@ -398,4 +401,4 @@ int NetUtilInternal::ResolveHostname(const char* hostname, Libraries::Net::Orbis
return ret;
}
} // namespace NetUtil
} // namespace NetUtil

View File

@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <mutex>
#include <variant>
#include <core/emulator_settings.h>
#include "common/config.h"
#include <core/user_settings.h>
#include "common/logging/log.h"
#include "core/emulator_settings.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/libs.h"
#include "core/libraries/np/np_error.h"
@ -632,7 +632,8 @@ s32 PS4_SYSV_ABI sceNpGetNpId(Libraries::UserService::OrbisUserServiceUserId use
return ORBIS_NP_ERROR_SIGNED_OUT;
}
memset(np_id, 0, sizeof(OrbisNpId));
strncpy(np_id->handle.data, Config::getUserName().c_str(), sizeof(np_id->handle.data));
strncpy(np_id->handle.data, UserManagement.GetDefaultUser().user_name.c_str(),
sizeof(np_id->handle.data));
return ORBIS_OK;
}
@ -646,7 +647,8 @@ s32 PS4_SYSV_ABI sceNpGetOnlineId(Libraries::UserService::OrbisUserServiceUserId
return ORBIS_NP_ERROR_SIGNED_OUT;
}
memset(online_id, 0, sizeof(OrbisNpOnlineId));
strncpy(online_id->data, Config::getUserName().c_str(), sizeof(online_id->data));
strncpy(online_id->data, UserManagement.GetDefaultUser().user_name.c_str(),
sizeof(online_id->data));
return ORBIS_OK;
}

View File

@ -14,10 +14,7 @@ namespace Libraries::Np::NpPartner {
static bool g_library_init = false;
std::mutex g_library_mutex{};
/**
* Terminates the library
*/
s32 PS4_SYSV_ABI Func_A4CC5784DA33517F() {
s32 PS4_SYSV_ABI sceNpEAAccessTerminate() {
LOG_ERROR(Lib_NpPartner, "(STUBBED) called");
if (!g_library_init) {
return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED;
@ -27,10 +24,7 @@ s32 PS4_SYSV_ABI Func_A4CC5784DA33517F() {
return ORBIS_OK;
}
/**
* Aborts requests started by Func_F8E9DB52CD425743
*/
s32 PS4_SYSV_ABI Func_A507D84D91F39CC7() {
s32 PS4_SYSV_ABI sceNpHasEAAccessSubscriptionAbortRequest() {
LOG_ERROR(Lib_NpPartner, "(STUBBED) called");
if (!g_library_init) {
return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED;
@ -39,20 +33,15 @@ s32 PS4_SYSV_ABI Func_A507D84D91F39CC7() {
return ORBIS_OK;
}
/**
* Initializes the library
*/
s32 PS4_SYSV_ABI Func_EC2C48E74FF19429() {
s32 PS4_SYSV_ABI sceNpEAAccessInitialize() {
LOG_ERROR(Lib_NpPartner, "(STUBBED) called");
g_library_init = true;
// Also retrieves and sends compiled SDK version to server.
return ORBIS_OK;
}
/**
* Creates an NP request to determine if the user has a subscription to EA's services.
*/
s32 PS4_SYSV_ABI Func_F8E9DB52CD425743(UserService::OrbisUserServiceUserId user_id, bool* result) {
s32 PS4_SYSV_ABI sceNpHasEAAccessSubscription(UserService::OrbisUserServiceUserId user_id,
bool* result) {
LOG_ERROR(Lib_NpPartner, "(STUBBED) called");
if (!g_library_init) {
return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED;
@ -71,13 +60,13 @@ s32 PS4_SYSV_ABI Func_F8E9DB52CD425743(UserService::OrbisUserServiceUserId user_
void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("pMxXhNozUX8", "libSceNpPartner001", 1, "libSceNpPartner001",
Func_A4CC5784DA33517F);
sceNpEAAccessTerminate);
LIB_FUNCTION("pQfYTZHznMc", "libSceNpPartner001", 1, "libSceNpPartner001",
Func_A507D84D91F39CC7);
sceNpHasEAAccessSubscriptionAbortRequest);
LIB_FUNCTION("7CxI50-xlCk", "libSceNpPartner001", 1, "libSceNpPartner001",
Func_EC2C48E74FF19429);
sceNpEAAccessInitialize);
LIB_FUNCTION("+OnbUs1CV0M", "libSceNpPartner001", 1, "libSceNpPartner001",
Func_F8E9DB52CD425743);
sceNpHasEAAccessSubscription);
};
} // namespace Libraries::Np::NpPartner

View File

@ -1,22 +1,118 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <unordered_map>
#include <pugixml.hpp>
#include "common/elf_info.h"
#include "common/logging/log.h"
#include "common/path_util.h"
#include "common/slot_vector.h"
#include "core/emulator_settings.h"
#include "core/libraries/libs.h"
#include "core/libraries/np/np_error.h"
#include "core/libraries/np/np_trophy.h"
#include "core/libraries/np/np_trophy_error.h"
#include "core/libraries/np/trophy_ui.h"
#include "core/libraries/system/userservice.h"
#include "core/memory.h"
namespace Libraries::Np::NpTrophy {
std::string game_serial;
// PS4 system language IDs map directly to TROP00.XML .. TROP30.XML.
// Index = OrbisSystemServiceParamId language value reported by the system.
// clang-format off
static constexpr std::array<std::string_view, 31> s_language_xml_names = {
"TROP_00.XML", // 00 Japanese
"TROP_01.XML", // 01 English (US)
"TROP_02.XML", // 02 French
"TROP_03.XML", // 03 Spanish (ES)
"TROP_04.XML", // 04 German
"TROP_05.XML", // 05 Italian
"TROP_06.XML", // 06 Dutch
"TROP_07.XML", // 07 Portuguese (PT)
"TROP_08.XML", // 08 Russian
"TROP_09.XML", // 09 Korean
"TROP_10.XML", // 10 Traditional Chinese
"TROP_11.XML", // 11 Simplified Chinese
"TROP_12.XML", // 12 Finnish
"TROP_13.XML", // 13 Swedish
"TROP_14.XML", // 14 Danish
"TROP_15.XML", // 15 Norwegian
"TROP_16.XML", // 16 Polish
"TROP_17.XML", // 17 Portuguese (BR)
"TROP_18.XML", // 18 English (GB)
"TROP_19.XML", // 19 Turkish
"TROP_20.XML", // 20 Spanish (LA)
"TROP_21.XML", // 21 Arabic
"TROP_22.XML", // 22 French (CA)
"TROP_23.XML", // 23 Czech
"TROP_24.XML", // 24 Hungarian
"TROP_25.XML", // 25 Greek
"TROP_26.XML", // 26 Romanian
"TROP_27.XML", // 27 Thai
"TROP_28.XML", // 28 Vietnamese
"TROP_29.XML", // 29 Indonesian
"TROP_30.XML", // 30 Unkrainian
};
// clang-format on
// Returns the best available trophy XML path for the current system language.
// Resolution order:
// 1. TROP_XX.XML for the active system language (e.g. TROP01.XML for English)
// 2. TROP.XML (master / language-neutral fallback)
static std::filesystem::path GetTrophyXmlPath(const std::filesystem::path& xml_dir,
int system_language) {
// Try the exact language file first.
if (system_language >= 0 && system_language < static_cast<int>(s_language_xml_names.size())) {
auto lang_path = xml_dir / s_language_xml_names[system_language];
if (std::filesystem::exists(lang_path)) {
return lang_path;
}
}
// Final fallback: master TROP.XML (always present).
return xml_dir / "TROP.XML";
}
static void ApplyUnlockToXmlFile(const std::filesystem::path& xml_path, OrbisNpTrophyId trophyId,
u64 trophyTimestamp, bool unlock_platinum,
OrbisNpTrophyId platinumId, u64 platinumTimestamp) {
pugi::xml_document doc;
if (!doc.load_file(xml_path.native().c_str())) {
LOG_WARNING(Lib_NpTrophy, "ApplyUnlock: failed to load {}", xml_path.string());
return;
}
auto trophyconf = doc.child("trophyconf");
for (pugi::xml_node& node : trophyconf.children()) {
if (std::string_view(node.name()) != "trophy") {
continue;
}
int id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
auto set_unlock = [&](u64 ts) {
if (node.attribute("unlockstate").empty()) {
node.append_attribute("unlockstate") = "true";
} else {
node.attribute("unlockstate").set_value("true");
}
const auto ts_str = std::to_string(ts);
if (node.attribute("timestamp").empty()) {
node.append_attribute("timestamp") = ts_str.c_str();
} else {
node.attribute("timestamp").set_value(ts_str.c_str());
}
};
if (id == trophyId) {
set_unlock(trophyTimestamp);
} else if (unlock_platinum && id == platinumId) {
set_unlock(platinumTimestamp);
}
}
doc.save_file(xml_path.native().c_str());
}
static constexpr auto MaxTrophyHandles = 4u;
static constexpr auto MaxTrophyContexts = 8u;
@ -30,6 +126,11 @@ struct ContextKeyHash {
struct TrophyContext {
u32 context_id;
bool registered = false;
std::filesystem::path trophy_xml_path; // resolved once at CreateContext
std::filesystem::path xml_dir; // .../Xml/
std::filesystem::path xml_save_file; // The actual file for tracking progress per-user.
std::filesystem::path icons_dir; // .../Icons/
};
static Common::SlotVector<OrbisNpTrophyHandle> trophy_handles{};
static Common::SlotVector<ContextKey> trophy_contexts{};
@ -94,66 +195,10 @@ OrbisNpTrophyGrade GetTrophyGradeFromChar(char trophyType) {
}
}
int PS4_SYSV_ABI sceNpTrophyAbortHandle(OrbisNpTrophyHandle handle) {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyCaptureScreenshot() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyDetails() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyFlagArray() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupArray() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupDetails() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfo() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context,
Libraries::UserService::OrbisUserServiceUserId user_id,
uint32_t service_label, u64 options) {
ASSERT(options == 0ull);
if (!context) {
if (!context || options != 0ull) {
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
}
@ -169,7 +214,20 @@ s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context,
const auto ctx_id = trophy_contexts.insert(user_id, service_label);
*context = ctx_id.index + 1;
contexts_internal[key].context_id = *context;
auto& ctx = contexts_internal[key];
ctx.context_id = *context;
// Resolve and cache all paths once so callers never recompute them.
const std::string np_comm_id = Common::ElfInfo::Instance().GetNpCommIds()[service_label];
const auto trophy_base =
Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "trophy" / np_comm_id;
ctx.xml_save_file =
EmulatorSettings.GetHomeDir() / std::to_string(user_id) / "trophy" / (np_comm_id + ".xml");
ctx.xml_dir = trophy_base / "Xml";
ctx.icons_dir = trophy_base / "Icons";
ctx.trophy_xml_path = GetTrophyXmlPath(ctx.xml_dir, EmulatorSettings.GetConsoleLanguage());
LOG_INFO(Lib_NpTrophy, "New context = {}, user_id = {} service label = {}", *context, user_id,
service_label);
@ -206,6 +264,10 @@ int PS4_SYSV_ABI sceNpTrophyDestroyContext(OrbisNpTrophyContext context) {
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
if (!trophy_contexts.is_allocated(contextId)) {
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
ContextKey contextkey = trophy_contexts[contextId];
trophy_contexts.erase(contextId);
contexts_internal.erase(contextkey);
@ -251,12 +313,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGameIcon(OrbisNpTrophyContext context, OrbisNpTro
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
ContextKey contextkey = trophy_contexts[contextId];
char trophy_folder[9];
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
const auto trophy_dir =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
auto icon_file = trophy_dir / trophy_folder / "Icons" / "ICON0.PNG";
const auto& ctx = contexts_internal[contextkey];
auto icon_file = ctx.icons_dir / "ICON0.PNG";
Common::FS::IOFile icon(icon_file, Common::FS::FileAccessMode::Read);
if (!icon.IsOpen()) {
@ -304,12 +364,11 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
ContextKey contextkey = trophy_contexts[contextId];
char trophy_folder[9];
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
const auto trophy_dir =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
const auto& ctx = contexts_internal[contextkey];
if (!ctx.registered)
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
const auto& trophy_file = ctx.trophy_xml_path;
const auto& trophy_save_file = ctx.xml_save_file;
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
@ -336,7 +395,18 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro
if (node_name == "group")
game_info.num_groups++;
}
pugi::xml_document save_doc;
pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str());
if (!save_result) {
LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description());
return ORBIS_OK;
}
auto save_trophyconf = save_doc.child("trophyconf");
for (const pugi::xml_node& node : save_trophyconf.children()) {
std::string_view node_name = node.name();
if (node_name == "trophy") {
bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool();
std::string_view current_trophy_grade = node.attribute("ttype").value();
@ -368,8 +438,9 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro
data->unlocked_silver = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER];
data->unlocked_bronze = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE];
// maybe this should be 1 instead of 100?
data->progress_percentage = 100;
data->progress_percentage = (game_info.num_trophies > 0)
? (game_info.unlocked_trophies * 100u) / game_info.num_trophies
: 0;
return ORBIS_OK;
}
@ -411,12 +482,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
ContextKey contextkey = trophy_contexts[contextId];
char trophy_folder[9];
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
const auto trophy_dir =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
const auto& ctx = contexts_internal[contextkey];
if (!ctx.registered)
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
const auto& trophy_file = ctx.trophy_xml_path;
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
@ -450,7 +519,18 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr
details->group_id = groupId;
data->group_id = groupId;
}
pugi::xml_document save_doc;
pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str());
if (!save_result) {
LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description());
return ORBIS_OK;
}
auto save_trophyconf = save_doc.child("trophyconf");
for (const pugi::xml_node& node : save_trophyconf.children()) {
std::string_view node_name = node.name();
if (node_name == "trophy") {
bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool();
std::string_view current_trophy_grade = node.attribute("ttype").value();
@ -484,15 +564,84 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr
data->unlocked_silver = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER];
data->unlocked_bronze = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE];
// maybe this should be 1 instead of 100?
data->progress_percentage = 100;
data->progress_percentage =
(group_info.num_trophies > 0)
? (group_info.unlocked_trophies * 100u) / group_info.num_trophies
: 0;
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyGetTrophyIcon(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle,
OrbisNpTrophyId trophyId, void* buffer, u64* size) {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
if (size == nullptr)
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
if (trophyId < 0 || trophyId >= ORBIS_NP_TROPHY_NUM_MAX)
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
Common::SlotId contextId;
contextId.index = context - 1;
if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) {
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
s32 handle_index = handle - 1;
if (handle_index >= trophy_handles.size() ||
!trophy_handles.is_allocated({static_cast<u32>(handle_index)})) {
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
}
ContextKey contextkey = trophy_contexts[contextId];
const auto& ctx = contexts_internal[contextkey];
if (!ctx.registered)
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
// Check that the trophy is unlocked and icons are only available for earned trophies.
pugi::xml_document doc;
if (!doc.load_file(ctx.xml_save_file.native().c_str())) {
LOG_ERROR(Lib_NpTrophy, "Failed to open trophy XML: {}", ctx.xml_save_file.string());
return ORBIS_NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND;
}
bool unlocked = false;
bool found = false;
for (const pugi::xml_node& node : doc.child("trophyconf").children()) {
if (std::string_view(node.name()) != "trophy")
continue;
if (node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID) == trophyId) {
found = true;
unlocked = node.attribute("unlockstate").as_bool();
break;
}
}
if (!found)
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
if (!unlocked)
return ORBIS_NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED;
const std::string icon_name = fmt::format("TROP{:03d}.PNG", trophyId);
const auto icon_path = ctx.icons_dir / icon_name;
Common::FS::IOFile icon(icon_path, Common::FS::FileAccessMode::Read);
if (!icon.IsOpen()) {
LOG_ERROR(Lib_NpTrophy, "Failed to open trophy icon: {}", icon_path.string());
return ORBIS_NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND;
}
if (buffer != nullptr) {
ReadFile(icon, buffer, *size);
} else {
*size = icon.GetSize();
}
return ORBIS_OK;
}
@ -507,7 +656,7 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
if (trophyId >= 127)
if (trophyId >= ORBIS_NP_TROPHY_NUM_MAX)
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
if (details == nullptr || data == nullptr)
@ -522,12 +671,10 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
ContextKey contextkey = trophy_contexts[contextId];
char trophy_folder[9];
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
const auto trophy_dir =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
const auto& ctx = contexts_internal[contextkey];
if (!ctx.registered)
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
const auto& trophy_file = ctx.trophy_xml_path;
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
@ -545,12 +692,34 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT
if (node_name == "trophy") {
int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
if (current_trophy_id == trophyId) {
bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool();
std::string_view current_trophy_grade = node.attribute("ttype").value();
std::string_view current_trophy_name = node.child("name").text().as_string();
std::string_view current_trophy_description =
node.child("detail").text().as_string();
strncpy(details->name, current_trophy_name.data(), ORBIS_NP_TROPHY_NAME_MAX_SIZE);
strncpy(details->description, current_trophy_description.data(),
ORBIS_NP_TROPHY_DESCR_MAX_SIZE);
}
}
}
pugi::xml_document save_doc;
pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str());
if (!save_result) {
LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description());
return ORBIS_OK;
}
auto save_trophyconf = save_doc.child("trophyconf");
for (const pugi::xml_node& node : save_trophyconf.children()) {
std::string_view node_name = node.name();
if (node_name == "trophy") {
int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
if (current_trophy_id == trophyId) {
bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool();
std::string_view current_trophy_grade = node.attribute("ttype").value();
uint64_t current_trophy_timestamp = node.attribute("timestamp").as_ullong();
int current_trophy_groupid = node.attribute("gid").as_int(-1);
bool current_trophy_hidden = node.attribute("hidden").as_bool();
@ -560,10 +729,6 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT
details->group_id = current_trophy_groupid;
details->hidden = current_trophy_hidden;
strncpy(details->name, current_trophy_name.data(), ORBIS_NP_TROPHY_NAME_MAX_SIZE);
strncpy(details->description, current_trophy_description.data(),
ORBIS_NP_TROPHY_DESCR_MAX_SIZE);
data->trophy_id = trophyId;
data->unlocked = current_trophy_unlockstate;
data->timestamp.tick = current_trophy_timestamp;
@ -579,29 +744,34 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context,
OrbisNpTrophyFlagArray* flags, u32* count) {
LOG_INFO(Lib_NpTrophy, "called");
if (flags == nullptr || count == nullptr)
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
if (flags == nullptr || count == nullptr)
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
ORBIS_NP_TROPHY_FLAG_ZERO(flags);
Common::SlotId contextId;
contextId.index = context - 1;
if (contextId.index >= trophy_contexts.size()) {
if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) {
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
ContextKey contextkey = trophy_contexts[contextId];
char trophy_folder[9];
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
const auto trophy_dir =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
s32 handle_index = handle - 1;
if (handle_index >= trophy_handles.size() ||
!trophy_handles.is_allocated({static_cast<u32>(handle_index)})) {
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
}
ContextKey contextkey = trophy_contexts[contextId];
const auto& ctx = contexts_internal[contextkey];
if (!ctx.registered)
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
const auto& trophy_file = ctx.xml_save_file;
ORBIS_NP_TROPHY_FLAG_ZERO(flags);
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
@ -622,10 +792,9 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context,
if (node_name == "trophy") {
num_trophies++;
}
if (current_trophy_unlockstate) {
ORBIS_NP_TROPHY_FLAG_SET(current_trophy_id, flags);
if (current_trophy_unlockstate) {
ORBIS_NP_TROPHY_FLAG_SET(current_trophy_id, flags);
}
}
}
@ -633,6 +802,200 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context,
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyRegisterContext(OrbisNpTrophyContext context,
OrbisNpTrophyHandle handle, uint64_t options) {
if (options != 0ull)
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
Common::SlotId contextId;
contextId.index = context - 1;
if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) {
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
s32 handle_index = handle - 1;
if (handle_index >= trophy_handles.size() ||
!trophy_handles.is_allocated({static_cast<u32>(handle_index)})) {
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
}
ContextKey contextkey = trophy_contexts[contextId];
auto& ctx = contexts_internal[contextkey];
if (ctx.registered)
return ORBIS_NP_TROPHY_ERROR_ALREADY_REGISTERED;
if (!std::filesystem::exists(ctx.trophy_xml_path))
return ORBIS_NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED;
ctx.registered = true;
LOG_INFO(Lib_NpTrophy, "Context {} registered", context);
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle,
OrbisNpTrophyId trophyId, OrbisNpTrophyId* platinumId) {
LOG_INFO(Lib_NpTrophy, "Unlocking trophy id {}", trophyId);
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
if (trophyId >= ORBIS_NP_TROPHY_NUM_MAX)
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
if (platinumId == nullptr)
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
Common::SlotId contextId;
contextId.index = context - 1;
if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) {
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
s32 handle_index = handle - 1;
if (handle_index >= trophy_handles.size() ||
!trophy_handles.is_allocated({static_cast<u32>(handle_index)})) {
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
}
ContextKey contextkey = trophy_contexts[contextId];
const auto& ctx = contexts_internal[contextkey];
if (!ctx.registered)
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
const auto& xml_dir = ctx.xml_dir;
const auto& trophy_file = ctx.trophy_xml_path;
pugi::xml_document save_doc;
pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str());
if (!save_result) {
LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", save_result.description());
return ORBIS_OK;
}
auto save_trophyconf = save_doc.child("trophyconf");
for (const pugi::xml_node& node : save_trophyconf.children()) {
std::string_view node_name = node.name();
if (std::string_view(node.name()) != "trophy")
continue;
int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool();
if (current_trophy_id == trophyId) {
if (current_trophy_unlockstate) {
LOG_INFO(Lib_NpTrophy, "Trophy already unlocked");
return ORBIS_NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED;
}
}
}
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
if (!result) {
LOG_ERROR(Lib_NpTrophy, "Failed to parse trophy xml : {}", result.description());
return ORBIS_NP_TROPHY_ERROR_TITLE_NOT_FOUND;
}
*platinumId = ORBIS_NP_TROPHY_INVALID_TROPHY_ID;
int num_trophies = 0;
int num_trophies_unlocked = 0;
pugi::xml_node platinum_node;
// Outputs filled during the scan.
bool trophy_found = false;
const char* trophy_name = "";
std::string_view trophy_type;
std::filesystem::path trophy_icon_path;
auto trophyconf = doc.child("trophyconf");
for (pugi::xml_node& node : trophyconf.children()) {
if (std::string_view(node.name()) != "trophy")
continue;
int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool();
std::string_view current_trophy_type = node.attribute("ttype").value();
if (current_trophy_type == "P") {
platinum_node = node;
if (trophyId == current_trophy_id) {
return ORBIS_NP_TROPHY_ERROR_PLATINUM_CANNOT_UNLOCK;
}
}
if (node.attribute("pid").as_int(-1) != ORBIS_NP_TROPHY_INVALID_TROPHY_ID) {
num_trophies++;
if (current_trophy_unlockstate) {
num_trophies_unlocked++;
}
}
if (current_trophy_id == trophyId) {
trophy_found = true;
trophy_name = node.child("name").text().as_string();
trophy_type = current_trophy_type;
const std::string icon_file = fmt::format("TROP{:03d}.PNG", current_trophy_id);
trophy_icon_path = ctx.icons_dir / icon_file;
}
}
if (!trophy_found)
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
// Capture timestamps once so every file gets the exact same value.
const auto now_secs = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
const u64 trophy_timestamp = static_cast<u64>(now_secs);
// Decide platinum.
bool unlock_platinum = false;
OrbisNpTrophyId platinum_id = ORBIS_NP_TROPHY_INVALID_TROPHY_ID;
u64 platinum_timestamp = 0;
const char* platinum_name = "";
std::filesystem::path platinum_icon_path;
if (!platinum_node.attribute("unlockstate").as_bool()) {
if ((num_trophies - 1) == num_trophies_unlocked) {
unlock_platinum = true;
platinum_id = platinum_node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
platinum_timestamp = trophy_timestamp; // same second is fine
platinum_name = platinum_node.child("name").text().as_string();
const std::string plat_icon_file = fmt::format("TROP{:03d}.PNG", platinum_id);
platinum_icon_path = ctx.icons_dir / plat_icon_file;
*platinumId = platinum_id;
}
}
// Queue UI notifications (only once, using the primary XML's strings).
AddTrophyToQueue(trophy_icon_path, trophy_name, trophy_type);
if (unlock_platinum) {
AddTrophyToQueue(platinum_icon_path, platinum_name, "P");
}
ApplyUnlockToXmlFile(ctx.xml_save_file, trophyId, trophy_timestamp, unlock_platinum,
platinum_id, platinum_timestamp);
LOG_INFO(Lib_NpTrophy, "Trophy {} successfully saved.", trophyId);
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyGroupArrayGetNum() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
@ -698,19 +1061,6 @@ int PS4_SYSV_ABI sceNpTrophyNumInfoGetTotal() {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyRegisterContext(OrbisNpTrophyContext context,
OrbisNpTrophyHandle handle, uint64_t options) {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophySetInfoGetTrophyFlagArray() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
@ -942,147 +1292,58 @@ int PS4_SYSV_ABI sceNpTrophySystemSetDbgParamInt() {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle,
OrbisNpTrophyId trophyId, OrbisNpTrophyId* platinumId) {
LOG_INFO(Lib_NpTrophy, "Unlocking trophy id {}", trophyId);
int PS4_SYSV_ABI sceNpTrophyAbortHandle(OrbisNpTrophyHandle handle) {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
int PS4_SYSV_ABI sceNpTrophyCaptureScreenshot() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyDetails() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
if (trophyId >= 127)
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyFlagArray() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
if (platinumId == nullptr)
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupArray() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
Common::SlotId contextId;
contextId.index = context - 1;
if (contextId.index >= trophy_contexts.size()) {
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
}
ContextKey contextkey = trophy_contexts[contextId];
char trophy_folder[9];
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupDetails() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
const auto trophy_dir =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfo() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
if (!result) {
LOG_ERROR(Lib_NpTrophy, "Failed to parse trophy xml : {}", result.description());
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
*platinumId = ORBIS_NP_TROPHY_INVALID_TROPHY_ID;
int num_trophies = 0;
int num_trophies_unlocked = 0;
pugi::xml_node platinum_node;
auto trophyconf = doc.child("trophyconf");
for (pugi::xml_node& node : trophyconf.children()) {
int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool();
const char* current_trophy_name = node.child("name").text().as_string();
std::string_view current_trophy_description = node.child("detail").text().as_string();
std::string_view current_trophy_type = node.attribute("ttype").value();
if (current_trophy_type == "P") {
platinum_node = node;
if (trophyId == current_trophy_id) {
return ORBIS_NP_TROPHY_ERROR_PLATINUM_CANNOT_UNLOCK;
}
}
if (std::string_view(node.name()) == "trophy") {
if (node.attribute("pid").as_int(-1) != ORBIS_NP_TROPHY_INVALID_TROPHY_ID) {
num_trophies++;
if (current_trophy_unlockstate) {
num_trophies_unlocked++;
}
}
if (current_trophy_id == trophyId) {
if (current_trophy_unlockstate) {
LOG_INFO(Lib_NpTrophy, "Trophy already unlocked");
return ORBIS_NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED;
} else {
if (node.attribute("unlockstate").empty()) {
node.append_attribute("unlockstate") = "true";
} else {
node.attribute("unlockstate").set_value("true");
}
auto trophyTimestamp = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
if (node.attribute("timestamp").empty()) {
node.append_attribute("timestamp") =
std::to_string(trophyTimestamp).c_str();
} else {
node.attribute("timestamp")
.set_value(std::to_string(trophyTimestamp).c_str());
}
std::string trophy_icon_file = "TROP";
trophy_icon_file.append(node.attribute("id").value());
trophy_icon_file.append(".PNG");
std::filesystem::path current_icon_path =
trophy_dir / trophy_folder / "Icons" / trophy_icon_file;
AddTrophyToQueue(current_icon_path, current_trophy_name, current_trophy_type);
}
}
}
}
if (!platinum_node.attribute("unlockstate").as_bool()) {
if ((num_trophies - 1) == num_trophies_unlocked) {
if (platinum_node.attribute("unlockstate").empty()) {
platinum_node.append_attribute("unlockstate") = "true";
} else {
platinum_node.attribute("unlockstate").set_value("true");
}
auto trophyTimestamp = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
if (platinum_node.attribute("timestamp").empty()) {
platinum_node.append_attribute("timestamp") =
std::to_string(trophyTimestamp).c_str();
} else {
platinum_node.attribute("timestamp")
.set_value(std::to_string(trophyTimestamp).c_str());
}
int platinum_trophy_id =
platinum_node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
const char* platinum_trophy_name = platinum_node.child("name").text().as_string();
std::string platinum_icon_file = "TROP";
platinum_icon_file.append(platinum_node.attribute("id").value());
platinum_icon_file.append(".PNG");
std::filesystem::path platinum_icon_path =
trophy_dir / trophy_folder / "Icons" / platinum_icon_file;
*platinumId = platinum_trophy_id;
AddTrophyToQueue(platinum_icon_path, platinum_trophy_name, "P");
}
}
doc.save_file((trophy_dir / trophy_folder / "Xml" / "TROP.XML").native().c_str());
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() {
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
return ORBIS_OK;
}

View File

@ -13,8 +13,6 @@ class SymbolsResolver;
namespace Libraries::Np::NpTrophy {
extern std::string game_serial;
constexpr int ORBIS_NP_TROPHY_FLAG_SETSIZE = 128;
constexpr int ORBIS_NP_TROPHY_FLAG_BITS_SHIFT = 5;

View File

@ -1,20 +1,24 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/config.h"
#include "common/logging/log.h"
#include "common/singleton.h"
#include "core/emulator_settings.h"
#include "core/libraries/libs.h"
#include "core/libraries/pad/pad_errors.h"
#include "core/user_settings.h"
#include "input/controller.h"
#include "pad.h"
namespace Libraries::Pad {
using Input::GameController;
using Input::GameControllers;
using namespace Libraries::UserService;
static bool g_initialized = false;
static bool g_opened = false;
static std::unordered_map<OrbisUserServiceUserId, s32> user_id_pad_handle_map{};
static constexpr s32 tv_remote_handle = 5;
int PS4_SYSV_ABI scePadClose(s32 handle) {
LOG_ERROR(Lib_Pad, "(STUBBED) called");
@ -30,8 +34,8 @@ int PS4_SYSV_ABI scePadDeviceClassGetExtendedInformation(
s32 handle, OrbisPadDeviceClassExtendedInformation* pExtInfo) {
LOG_ERROR(Lib_Pad, "(STUBBED) called");
std::memset(pExtInfo, 0, sizeof(OrbisPadDeviceClassExtendedInformation));
if (Config::getUseSpecialPad()) {
pExtInfo->deviceClass = (OrbisPadDeviceClass)Config::getSpecialPadClass();
if (EmulatorSettings.IsUsingSpecialPad()) {
pExtInfo->deviceClass = (OrbisPadDeviceClass)EmulatorSettings.GetSpecialPadClass();
}
return ORBIS_OK;
}
@ -107,9 +111,9 @@ int PS4_SYSV_ABI scePadGetControllerInformation(s32 handle, OrbisPadControllerIn
return ORBIS_OK;
}
pInfo->connected = true;
if (Config::getUseSpecialPad()) {
if (EmulatorSettings.IsUsingSpecialPad()) {
pInfo->connectionType = ORBIS_PAD_PORT_TYPE_SPECIAL;
pInfo->deviceClass = (OrbisPadDeviceClass)Config::getSpecialPadClass();
pInfo->deviceClass = (OrbisPadDeviceClass)EmulatorSettings.GetSpecialPadClass();
}
return ORBIS_OK;
}
@ -156,11 +160,16 @@ int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId
if (!g_initialized) {
return ORBIS_PAD_ERROR_NOT_INITIALIZED;
}
if (userId == -1 || !g_opened) {
if (userId == -1) {
return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE;
}
LOG_DEBUG(Lib_Pad, "(DUMMY) called");
return 1;
auto it = user_id_pad_handle_map.find(userId);
if (it == user_id_pad_handle_map.end()) {
return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE;
}
s32 pad_handle = it->second;
LOG_DEBUG(Lib_Pad, "called, userid: {}, out pad handle: {}", userId, pad_handle);
return pad_handle;
}
int PS4_SYSV_ABI scePadGetIdleCount() {
@ -168,8 +177,18 @@ int PS4_SYSV_ABI scePadGetIdleCount() {
return ORBIS_OK;
}
int PS4_SYSV_ABI scePadGetInfo() {
LOG_ERROR(Lib_Pad, "(STUBBED) called");
int PS4_SYSV_ABI scePadGetInfo(OrbisPadInfo* data) {
LOG_WARNING(Lib_Pad, "(DUMMY) called");
if (!data) {
return ORBIS_PAD_ERROR_INVALID_ARG;
}
auto& controllers = *Common::Singleton<GameControllers>::Instance();
auto col = controllers[0]->GetLightBarRGB();
std::memset(data, 0, sizeof(OrbisPadInfo));
data->unk1 = 0x1;
data->pad_handle = 1;
data->unk3 = 0x00000101;
data->colour = col.r + (col.g << 8) + (col.b << 16);
return ORBIS_OK;
}
@ -254,34 +273,61 @@ int PS4_SYSV_ABI scePadOpen(Libraries::UserService::OrbisUserServiceUserId userI
if (!g_initialized) {
return ORBIS_PAD_ERROR_NOT_INITIALIZED;
}
if (userId == -1) {
return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE;
if (userId < 0) {
return ORBIS_DEVICE_SERVICE_ERROR_INVALID_USER;
}
if (Config::getUseSpecialPad()) {
if (userId == ORBIS_USER_SERVICE_USER_ID_SYSTEM) {
if (type == ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) {
LOG_INFO(Lib_Pad, "Opened a TV remote device");
user_id_pad_handle_map[ORBIS_USER_SERVICE_USER_ID_SYSTEM] = tv_remote_handle;
return tv_remote_handle;
}
return ORBIS_DEVICE_SERVICE_ERROR_INVALID_USER;
}
if (type == ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) {
return ORBIS_PAD_ERROR_INVALID_ARG;
}
if (EmulatorSettings.IsUsingSpecialPad()) {
if (type != ORBIS_PAD_PORT_TYPE_SPECIAL)
return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED;
} else {
if (type != ORBIS_PAD_PORT_TYPE_STANDARD && type != ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL)
if (type != ORBIS_PAD_PORT_TYPE_STANDARD)
return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED;
}
LOG_INFO(Lib_Pad, "(DUMMY) called user_id = {} type = {} index = {}", userId, type, index);
g_opened = true;
scePadResetLightBar(userId);
scePadResetOrientation(userId);
return 1; // dummy
auto u = UserManagement.GetUserByID(userId);
if (!u) {
return ORBIS_DEVICE_SERVICE_ERROR_USER_NOT_LOGIN;
}
s32 pad_handle = u->player_index;
LOG_INFO(Lib_Pad, "called user_id = {} type = {} index = {}, pad_handle = {}", userId, type,
index, pad_handle);
scePadResetLightBar(pad_handle);
scePadResetOrientation(pad_handle);
user_id_pad_handle_map[userId] = pad_handle;
return pad_handle;
}
int PS4_SYSV_ABI scePadOpenExt(Libraries::UserService::OrbisUserServiceUserId userId, s32 type,
s32 index, const OrbisPadOpenExtParam* pParam) {
LOG_ERROR(Lib_Pad, "(STUBBED) called");
if (Config::getUseSpecialPad()) {
if (EmulatorSettings.IsUsingSpecialPad()) {
if (type != ORBIS_PAD_PORT_TYPE_SPECIAL)
return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED;
} else {
if (type != ORBIS_PAD_PORT_TYPE_STANDARD && type != ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL)
return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED;
}
return 1; // dummy
auto u = UserManagement.GetUserByID(userId);
if (!u) {
return ORBIS_DEVICE_SERVICE_ERROR_USER_NOT_LOGIN;
}
s32 pad_handle = u->player_index;
LOG_INFO(Lib_Pad, "called user_id = {} type = {} index = {}, pad_handle = {}", userId, type,
index, pad_handle);
scePadResetLightBar(pad_handle);
scePadResetOrientation(pad_handle);
user_id_pad_handle_map[userId] = pad_handle;
return pad_handle;
}
int PS4_SYSV_ABI scePadOpenExt2() {
@ -294,8 +340,8 @@ int PS4_SYSV_ABI scePadOutputReport() {
return ORBIS_OK;
}
int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num, bool connected,
u32 connected_count) {
int ProcessStates(s32 handle, OrbisPadData* pData, Input::GameController& controller,
Input::State* states, s32 num, bool connected, u32 connected_count) {
if (!connected) {
pData[0] = {};
pData[0].orientation = {0.0f, 0.0f, 0.0f, 1.0f};
@ -319,73 +365,75 @@ int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num
pData[i].angularVelocity.z = states[i].angularVelocity.z;
pData[i].orientation = {0.0f, 0.0f, 0.0f, 1.0f};
auto* controller = Common::Singleton<GameController>::Instance();
const auto* engine = controller->GetEngine();
if (engine && handle == 1) {
const auto gyro_poll_rate = engine->GetAccelPollRate();
if (gyro_poll_rate != 0.0f) {
auto now = std::chrono::steady_clock::now();
float deltaTime = std::chrono::duration_cast<std::chrono::microseconds>(
now - controller->GetLastUpdate())
.count() /
1000000.0f;
controller->SetLastUpdate(now);
Libraries::Pad::OrbisFQuaternion lastOrientation = controller->GetLastOrientation();
Libraries::Pad::OrbisFQuaternion outputOrientation = {0.0f, 0.0f, 0.0f, 1.0f};
GameController::CalculateOrientation(pData->acceleration, pData->angularVelocity,
deltaTime, lastOrientation, outputOrientation);
pData[i].orientation = outputOrientation;
controller->SetLastOrientation(outputOrientation);
}
const auto gyro_poll_rate = controller.accel_poll_rate;
if (gyro_poll_rate != 0.0f) {
auto now = std::chrono::steady_clock::now();
float deltaTime = std::chrono::duration_cast<std::chrono::microseconds>(
now - controller.GetLastUpdate())
.count() /
1000000.0f;
controller.SetLastUpdate(now);
Libraries::Pad::OrbisFQuaternion lastOrientation = controller.GetLastOrientation();
Libraries::Pad::OrbisFQuaternion outputOrientation = {0.0f, 0.0f, 0.0f, 1.0f};
GameControllers::CalculateOrientation(pData->acceleration, pData->angularVelocity,
deltaTime, lastOrientation, outputOrientation);
pData[i].orientation = outputOrientation;
controller.SetLastOrientation(outputOrientation);
}
pData[i].touchData.touchNum =
(states[i].touchpad[0].state ? 1 : 0) + (states[i].touchpad[1].state ? 1 : 0);
if (handle == 1) {
if (controller->GetTouchCount() >= 127) {
controller->SetTouchCount(0);
if (controller.GetTouchCount() >= 127) {
controller.SetTouchCount(0);
}
if (controller->GetSecondaryTouchCount() >= 127) {
controller->SetSecondaryTouchCount(0);
if (controller.GetSecondaryTouchCount() >= 127) {
controller.SetSecondaryTouchCount(0);
}
if (pData->touchData.touchNum == 1 && controller->GetPreviousTouchNum() == 0) {
controller->SetTouchCount(controller->GetTouchCount() + 1);
controller->SetSecondaryTouchCount(controller->GetTouchCount());
} else if (pData->touchData.touchNum == 2 && controller->GetPreviousTouchNum() == 1) {
controller->SetSecondaryTouchCount(controller->GetSecondaryTouchCount() + 1);
} else if (pData->touchData.touchNum == 0 && controller->GetPreviousTouchNum() > 0) {
if (controller->GetTouchCount() < controller->GetSecondaryTouchCount()) {
controller->SetTouchCount(controller->GetSecondaryTouchCount());
if (pData->touchData.touchNum == 1 && controller.GetPreviousTouchNum() == 0) {
controller.SetTouchCount(controller.GetTouchCount() + 1);
controller.SetSecondaryTouchCount(controller.GetTouchCount());
} else if (pData->touchData.touchNum == 2 && controller.GetPreviousTouchNum() == 1) {
controller.SetSecondaryTouchCount(controller.GetSecondaryTouchCount() + 1);
} else if (pData->touchData.touchNum == 0 && controller.GetPreviousTouchNum() > 0) {
if (controller.GetTouchCount() < controller.GetSecondaryTouchCount()) {
controller.SetTouchCount(controller.GetSecondaryTouchCount());
} else {
if (controller->WasSecondaryTouchReset()) {
controller->SetTouchCount(controller->GetSecondaryTouchCount());
controller->UnsetSecondaryTouchResetBool();
if (controller.WasSecondaryTouchReset()) {
controller.SetTouchCount(controller.GetSecondaryTouchCount());
controller.UnsetSecondaryTouchResetBool();
}
}
}
controller->SetPreviousTouchNum(pData->touchData.touchNum);
controller.SetPreviousTouchNum(pData->touchData.touchNum);
if (pData->touchData.touchNum == 1) {
states[i].touchpad[0].ID = controller->GetTouchCount();
states[i].touchpad[0].ID = controller.GetTouchCount();
states[i].touchpad[1].ID = 0;
} else if (pData->touchData.touchNum == 2) {
states[i].touchpad[0].ID = controller->GetTouchCount();
states[i].touchpad[1].ID = controller->GetSecondaryTouchCount();
states[i].touchpad[0].ID = controller.GetTouchCount();
states[i].touchpad[1].ID = controller.GetSecondaryTouchCount();
}
} else {
states[i].touchpad[0].ID = 1;
states[i].touchpad[1].ID = 2;
}
pData[i].touchData.touch[0].x = states[i].touchpad[0].x;
pData[i].touchData.touch[0].y = states[i].touchpad[0].y;
pData[i].touchData.touch[0].id = states[i].touchpad[0].ID;
pData[i].touchData.touch[1].x = states[i].touchpad[1].x;
pData[i].touchData.touch[1].y = states[i].touchpad[1].y;
pData[i].touchData.touch[1].id = states[i].touchpad[1].ID;
if (!states[i].touchpad[0].state && states[i].touchpad[1].state) {
pData[i].touchData.touch[0].x = states[i].touchpad[1].x;
pData[i].touchData.touch[0].y = states[i].touchpad[1].y;
pData[i].touchData.touch[0].id = states[i].touchpad[1].ID;
} else {
pData[i].touchData.touch[0].x = states[i].touchpad[0].x;
pData[i].touchData.touch[0].y = states[i].touchpad[0].y;
pData[i].touchData.touch[0].id = states[i].touchpad[0].ID;
pData[i].touchData.touch[1].x = states[i].touchpad[1].x;
pData[i].touchData.touch[1].y = states[i].touchpad[1].y;
pData[i].touchData.touch[1].id = states[i].touchpad[1].ID;
}
pData[i].connected = connected;
pData[i].timestamp = states[i].time;
pData[i].connectedCount = connected_count;
@ -397,16 +445,18 @@ int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num
int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num) {
LOG_TRACE(Lib_Pad, "called");
if (handle < 1) {
return ORBIS_PAD_ERROR_INVALID_HANDLE;
}
int connected_count = 0;
bool connected = false;
std::vector<Input::State> states(64);
auto* controller = Common::Singleton<GameController>::Instance();
const auto* engine = controller->GetEngine();
int ret_num = controller->ReadStates(states.data(), num, &connected, &connected_count);
return ProcessStates(handle, pData, states.data(), ret_num, connected, connected_count);
auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle);
if (!controller_id) {
return ORBIS_PAD_ERROR_INVALID_HANDLE;
}
auto& controllers = *Common::Singleton<GameControllers>::Instance();
auto& controller = *controllers[*controller_id];
int ret_num = controller.ReadStates(states.data(), num, &connected, &connected_count);
return ProcessStates(handle, pData, controller, states.data(), ret_num, connected,
connected_count);
}
int PS4_SYSV_ABI scePadReadBlasterForTracker() {
@ -430,17 +480,18 @@ int PS4_SYSV_ABI scePadReadHistory() {
}
int PS4_SYSV_ABI scePadReadState(s32 handle, OrbisPadData* pData) {
LOG_TRACE(Lib_Pad, "called");
if (handle < 1) {
LOG_TRACE(Lib_Pad, "handle: {}", handle);
auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle);
if (!controller_id) {
return ORBIS_PAD_ERROR_INVALID_HANDLE;
}
auto* controller = Common::Singleton<GameController>::Instance();
const auto* engine = controller->GetEngine();
auto& controllers = *Common::Singleton<GameControllers>::Instance();
auto& controller = *controllers[*controller_id];
int connected_count = 0;
bool connected = false;
Input::State state;
controller->ReadState(&state, &connected, &connected_count);
ProcessStates(handle, pData, &state, 1, connected, connected_count);
controller.ReadState(&state, &connected, &connected_count);
ProcessStates(handle, pData, controller, &state, 1, connected, connected_count);
return ORBIS_OK;
}
@ -450,13 +501,30 @@ int PS4_SYSV_ABI scePadReadStateExt() {
}
int PS4_SYSV_ABI scePadResetLightBar(s32 handle) {
LOG_INFO(Lib_Pad, "(DUMMY) called");
if (handle != 1) {
LOG_DEBUG(Lib_Pad, "called, handle: {}", handle);
auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle);
if (!controller_id) {
return ORBIS_PAD_ERROR_INVALID_HANDLE;
}
auto* controller = Common::Singleton<GameController>::Instance();
int* rgb = Config::GetControllerCustomColor();
controller->SetLightBarRGB(rgb[0], rgb[1], rgb[2]);
auto& controllers = *Common::Singleton<GameControllers>::Instance();
s32 colour_index = UserManagement.GetUserByPlayerIndex(handle)->user_color - 1;
Input::Colour colour{255, 0, 0};
if (colour_index >= 0 && colour_index <= 3) {
static constexpr Input::Colour colours[4]{
{0, 0, 255}, // blue
{255, 0, 0}, // red
{0, 255, 0}, // green
{255, 0, 255}, // pink
};
colour = colours[colour_index];
} else {
LOG_ERROR(Lib_Pad, "Invalid user colour value {} for controller {}, falling back to blue",
colour_index, handle);
}
if (auto oc = GameControllers::GetControllerCustomColor(*controller_id)) {
colour = *oc;
}
controllers[*controller_id]->SetLightBarRGB(colour.r, colour.g, colour.b);
return ORBIS_OK;
}
@ -473,14 +541,15 @@ int PS4_SYSV_ABI scePadResetLightBarAllByPortType() {
int PS4_SYSV_ABI scePadResetOrientation(s32 handle) {
LOG_INFO(Lib_Pad, "scePadResetOrientation called handle = {}", handle);
if (handle != 1) {
auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle);
if (!controller_id) {
return ORBIS_PAD_ERROR_INVALID_HANDLE;
}
auto* controller = Common::Singleton<GameController>::Instance();
auto& controllers = *Common::Singleton<GameControllers>::Instance();
Libraries::Pad::OrbisFQuaternion defaultOrientation = {0.0f, 0.0f, 0.0f, 1.0f};
controller->SetLastOrientation(defaultOrientation);
controller->SetLastUpdate(std::chrono::steady_clock::now());
controllers[*controller_id]->SetLastOrientation(defaultOrientation);
controllers[*controller_id]->SetLastUpdate(std::chrono::steady_clock::now());
return ORBIS_OK;
}
@ -526,7 +595,11 @@ int PS4_SYSV_ABI scePadSetForceIntercepted() {
}
int PS4_SYSV_ABI scePadSetLightBar(s32 handle, const OrbisPadLightBarParam* pParam) {
if (Config::GetOverrideControllerColor()) {
auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle);
if (!controller_id) {
return ORBIS_PAD_ERROR_INVALID_HANDLE;
}
if (GameControllers::GetControllerCustomColor(*controller_id)) {
return ORBIS_OK;
}
if (pParam != nullptr) {
@ -538,8 +611,8 @@ int PS4_SYSV_ABI scePadSetLightBar(s32 handle, const OrbisPadLightBarParam* pPar
return ORBIS_PAD_ERROR_INVALID_LIGHTBAR_SETTING;
}
auto* controller = Common::Singleton<GameController>::Instance();
controller->SetLightBarRGB(pParam->r, pParam->g, pParam->b);
auto& controllers = *Common::Singleton<GameControllers>::Instance();
controllers[*controller_id]->SetLightBarRGB(pParam->r, pParam->g, pParam->b);
return ORBIS_OK;
}
return ORBIS_PAD_ERROR_INVALID_ARG;
@ -555,8 +628,14 @@ int PS4_SYSV_ABI scePadSetLightBarBlinking() {
return ORBIS_OK;
}
int PS4_SYSV_ABI scePadSetLightBarForTracker() {
LOG_ERROR(Lib_Pad, "(STUBBED) called");
int PS4_SYSV_ABI scePadSetLightBarForTracker(s32 handle, const OrbisPadLightBarParam* pParam) {
LOG_INFO(Lib_Pad, "called, r: {} g: {} b: {}", pParam->r, pParam->g, pParam->b);
auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle);
if (!controller_id) {
return ORBIS_PAD_ERROR_INVALID_HANDLE;
}
auto& controllers = *Common::Singleton<GameControllers>::Instance();
controllers[*controller_id]->SetLightBarRGB(pParam->r, pParam->g, pParam->b);
return ORBIS_OK;
}
@ -603,11 +682,15 @@ int PS4_SYSV_ABI scePadSetUserColor() {
}
int PS4_SYSV_ABI scePadSetVibration(s32 handle, const OrbisPadVibrationParam* pParam) {
auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle);
if (!controller_id) {
return ORBIS_PAD_ERROR_INVALID_HANDLE;
}
if (pParam != nullptr) {
LOG_DEBUG(Lib_Pad, "scePadSetVibration called handle = {} data = {} , {}", handle,
pParam->smallMotor, pParam->largeMotor);
auto* controller = Common::Singleton<GameController>::Instance();
controller->SetVibration(pParam->smallMotor, pParam->largeMotor);
auto& controllers = *Common::Singleton<GameControllers>::Instance();
controllers[*controller_id]->SetVibration(pParam->smallMotor, pParam->largeMotor);
return ORBIS_OK;
}
return ORBIS_PAD_ERROR_INVALID_ARG;

View File

@ -253,6 +253,18 @@ struct OrbisPadVibrationParam {
u8 smallMotor;
};
struct OrbisPadInfo {
u32 unk1;
u32 unk2;
u32 pad_handle;
u32 unk3;
u32 unk4;
u32 unk5;
u32 colour;
u32 unk6;
u32 unk[30];
};
int PS4_SYSV_ABI scePadClose(s32 handle);
int PS4_SYSV_ABI scePadConnectPort();
int PS4_SYSV_ABI scePadDeviceClassGetExtendedInformation(
@ -280,7 +292,7 @@ int PS4_SYSV_ABI scePadGetFeatureReport();
int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId userId, s32 type,
s32 index);
int PS4_SYSV_ABI scePadGetIdleCount();
int PS4_SYSV_ABI scePadGetInfo();
int PS4_SYSV_ABI scePadGetInfo(OrbisPadInfo* data);
int PS4_SYSV_ABI scePadGetInfoByPortType();
int PS4_SYSV_ABI scePadGetLicenseControllerInformation();
int PS4_SYSV_ABI scePadGetMotionSensorPosition();
@ -324,7 +336,7 @@ int PS4_SYSV_ABI scePadSetForceIntercepted();
int PS4_SYSV_ABI scePadSetLightBar(s32 handle, const OrbisPadLightBarParam* pParam);
int PS4_SYSV_ABI scePadSetLightBarBaseBrightness();
int PS4_SYSV_ABI scePadSetLightBarBlinking();
int PS4_SYSV_ABI scePadSetLightBarForTracker();
int PS4_SYSV_ABI scePadSetLightBarForTracker(s32 handle, const OrbisPadLightBarParam* pParam);
int PS4_SYSV_ABI scePadSetLoginUserNumber();
int PS4_SYSV_ABI scePadSetMotionSensorState(s32 handle, bool bEnable);
int PS4_SYSV_ABI scePadSetProcessFocus();

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <iostream>
@ -6,7 +6,6 @@
#include <magic_enum/magic_enum.hpp>
#include "common/assert.h"
#include "common/config.h"
#include "common/path_util.h"
#include "common/singleton.h"
#include "core/emulator_settings.h"
@ -49,12 +48,13 @@ namespace Libraries::SaveData {
fs::path SaveInstance::MakeTitleSavePath(Libraries::UserService::OrbisUserServiceUserId user_id,
std::string_view game_serial) {
return Config::GetSaveDataPath() / std::to_string(user_id) / game_serial;
return EmulatorSettings.GetHomeDir() / std::to_string(user_id) / "savedata" / game_serial;
}
fs::path SaveInstance::MakeDirSavePath(Libraries::UserService::OrbisUserServiceUserId user_id,
std::string_view game_serial, std::string_view dir_name) {
return Config::GetSaveDataPath() / std::to_string(user_id) / game_serial / dir_name;
fs::path SaveInstance::MakeDirSavePath(OrbisUserServiceUserId user_id, std::string_view game_serial,
std::string_view dir_name) {
return EmulatorSettings.GetHomeDir() / std::to_string(user_id) / "savedata" / game_serial /
dir_name;
}
uint64_t SaveInstance::GetMaxBlockFromSFO(const PSF& psf) {

View File

@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <span>
#include <thread>
#include <vector>
@ -8,13 +9,13 @@
#include <magic_enum/magic_enum.hpp>
#include "common/assert.h"
#include "common/config.h"
#include "common/cstring.h"
#include "common/elf_info.h"
#include "common/enum.h"
#include "common/logging/log.h"
#include "common/path_util.h"
#include "common/string_util.h"
#include "core/emulator_settings.h"
#include "core/file_format/psf.h"
#include "core/file_sys/fs.h"
#include "core/libraries/error_codes.h"
@ -441,7 +442,8 @@ static Error saveDataMount(const OrbisSaveDataMount2* mount_info,
LOG_INFO(Lib_SaveData, "called with invalid block size");
}
const auto root_save = Config::GetSaveDataPath();
const auto root_save =
EmulatorSettings.GetHomeDir() / std::to_string(mount_info->userId) / "savedata";
fs::create_directories(root_save);
const auto available = fs::space(root_save).available;
@ -489,7 +491,9 @@ static Error Umount(const OrbisSaveDataMountPoint* mountPoint, bool call_backup
return Error::PARAMETER;
}
LOG_DEBUG(Lib_SaveData, "Umount mountPoint:{}", mountPoint->data.to_view());
const std::string_view mount_point_str{mountPoint->data};
std::string mount_point_str = mountPoint->data.to_string();
for (auto& instance : g_mount_slots) {
if (instance.has_value()) {
const auto& slot_name = instance->GetMountPoint();

View File

@ -20,6 +20,7 @@
#include "core/libraries/sysmodule/sysmodule_error.h"
#include "core/libraries/sysmodule/sysmodule_internal.h"
#include "core/libraries/sysmodule/sysmodule_table.h"
#include "core/libraries/system_gesture/system_gesture.h"
#include "core/linker.h"
#include "emulator.h"
@ -223,7 +224,8 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) {
{"libSceAudiodec.sprx", nullptr},
{"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont},
{"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt},
{"libSceFreeTypeOt.sprx", nullptr}});
{"libSceFreeTypeOt.sprx", nullptr},
{"libSceSystemGesture.sprx", &Libraries::SystemGesture::RegisterLib}});
// Iterate through the allowed array
const auto it = std::ranges::find_if(

View File

@ -1,13 +1,19 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/config.h"
#include <queue>
#include "common/logging/log.h"
#include <core/user_settings.h>
#include <queue>
#include "common/singleton.h"
#include "core/emulator_settings.h"
#include "core/libraries/libs.h"
#include "core/libraries/system/userservice.h"
#include "core/libraries/system/userservice_error.h"
#include "core/tls.h"
#include "input/controller.h"
namespace Libraries::UserService {
@ -114,14 +120,15 @@ void AddUserServiceEvent(const OrbisUserServiceEvent e) {
}
s32 PS4_SYSV_ABI sceUserServiceGetEvent(OrbisUserServiceEvent* event) {
LOG_TRACE(Lib_UserService, "(DUMMY) called");
// fake a loggin event
static bool logged_in = false;
LOG_TRACE(Lib_UserService, "called");
if (!logged_in) {
logged_in = true;
event->event = OrbisUserServiceEventType::Login;
event->userId = 1;
if (!user_service_event_queue.empty()) {
OrbisUserServiceEvent& temp = user_service_event_queue.front();
event->event = temp.event;
event->userId = temp.userId;
user_service_event_queue.pop();
LOG_INFO(Lib_UserService, "Event processed by the game: {} {}", (u8)temp.event,
temp.userId);
return ORBIS_OK;
}
@ -504,8 +511,7 @@ s32 PS4_SYSV_ABI sceUserServiceGetInitialUser(int* user_id) {
LOG_ERROR(Lib_UserService, "user_id is null");
return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
// select first user (TODO add more)
*user_id = 1;
*user_id = UserManagement.GetDefaultUser().user_id;
return ORBIS_OK;
}
@ -575,20 +581,29 @@ int PS4_SYSV_ABI sceUserServiceGetLoginFlag() {
}
s32 PS4_SYSV_ABI sceUserServiceGetLoginUserIdList(OrbisUserServiceLoginUserIdList* userIdList) {
LOG_DEBUG(Lib_UserService, "called");
if (userIdList == nullptr) {
LOG_ERROR(Lib_UserService, "user_id is null");
LOG_ERROR(Lib_UserService, "userIdList is null");
return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
// TODO only first user, do the others as well
userIdList->user_id[0] = 1;
userIdList->user_id[1] = ORBIS_USER_SERVICE_USER_ID_INVALID;
userIdList->user_id[2] = ORBIS_USER_SERVICE_USER_ID_INVALID;
userIdList->user_id[3] = ORBIS_USER_SERVICE_USER_ID_INVALID;
// Initialize all slots to invalid (-1)
for (int i = 0; i < ORBIS_USER_SERVICE_MAX_LOGIN_USERS; i++) {
userIdList->user_id[i] = ORBIS_USER_SERVICE_USER_ID_INVALID;
}
auto& user_manager = UserManagement;
auto logged_in_users = user_manager.GetLoggedInUsers();
for (int i = 0; i < ORBIS_USER_SERVICE_MAX_LOGIN_USERS; i++) {
s32 id =
logged_in_users[i] ? logged_in_users[i]->user_id : ORBIS_USER_SERVICE_USER_ID_INVALID;
userIdList->user_id[i] = id;
LOG_DEBUG(Lib_UserService, "Slot {}: User ID {} (port {})", i, id,
logged_in_users[i] ? logged_in_users[i]->player_index : -1);
}
return ORBIS_OK;
}
int PS4_SYSV_ABI sceUserServiceGetMicLevel() {
LOG_ERROR(Lib_UserService, "(STUBBED) called");
return ORBIS_OK;
@ -1056,7 +1071,7 @@ s32 PS4_SYSV_ABI sceUserServiceGetUserColor(int user_id, OrbisUserServiceUserCol
LOG_ERROR(Lib_UserService, "color is null");
return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
*color = OrbisUserServiceUserColor::Blue;
*color = (OrbisUserServiceUserColor)UserManagement.GetUserByID(user_id)->user_color;
return ORBIS_OK;
}
@ -1076,12 +1091,18 @@ int PS4_SYSV_ABI sceUserServiceGetUserGroupNum() {
}
s32 PS4_SYSV_ABI sceUserServiceGetUserName(int user_id, char* user_name, std::size_t size) {
LOG_DEBUG(Lib_UserService, "called user_id = {} ,size = {} ", user_id, size);
LOG_DEBUG(Lib_UserService, "called user_id = {}, size = {} ", user_id, size);
if (user_name == nullptr) {
LOG_ERROR(Lib_UserService, "user_name is null");
return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT;
}
std::string name = Config::getUserName();
std::string name = "shadPS4";
auto const* u = UserManagement.GetUserByID(user_id);
if (u != nullptr) {
name = u->user_name;
} else {
LOG_ERROR(Lib_UserService, "No user found");
}
if (size < name.length()) {
LOG_ERROR(Lib_UserService, "buffer is too short");
return ORBIS_USER_SERVICE_ERROR_BUFFER_TOO_SHORT;

View File

@ -203,36 +203,23 @@ s32 PS4_SYSV_ABI sceVideodec2GetPictureInfo(const OrbisVideodec2OutputInfo* outp
LOG_ERROR(Lib_Vdec2, "No picture info available");
return ORBIS_OK;
}
if (gPictureInfos.empty()) {
LOG_ERROR(Lib_Vdec2, "No picture info available");
return ORBIS_OK;
}
// If the game uses the older Videodec2 structs, we need to accomodate that.
if (outputInfo->thisSize != sizeof(OrbisVideodec2OutputInfo)) {
if (gLegacyPictureInfos.empty()) {
LOG_ERROR(Lib_Vdec2, "No picture info available");
return ORBIS_OK;
}
if (p1stPictureInfoOut) {
OrbisVideodec2LegacyAvcPictureInfo* picInfo =
static_cast<OrbisVideodec2LegacyAvcPictureInfo*>(p1stPictureInfoOut);
if (picInfo->thisSize != sizeof(OrbisVideodec2LegacyAvcPictureInfo)) {
LOG_ERROR(Lib_Vdec2, "Invalid struct size");
return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE;
}
*picInfo = gLegacyPictureInfos.back();
}
} else {
if (gPictureInfos.empty()) {
LOG_ERROR(Lib_Vdec2, "No picture info available");
return ORBIS_OK;
}
if (p1stPictureInfoOut) {
OrbisVideodec2AvcPictureInfo* picInfo =
static_cast<OrbisVideodec2AvcPictureInfo*>(p1stPictureInfoOut);
if (picInfo->thisSize != sizeof(OrbisVideodec2AvcPictureInfo)) {
LOG_ERROR(Lib_Vdec2, "Invalid struct size");
return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE;
}
*picInfo = gPictureInfos.back();
if (p1stPictureInfoOut) {
// Copy enough data to check thisSize.
u64 picture_size = 0;
memcpy(&picture_size, p1stPictureInfoOut, sizeof(u64));
if ((picture_size | 0x10) != sizeof(OrbisVideodec2AvcPictureInfo)) {
LOG_ERROR(Lib_Vdec2, "Invalid struct size");
return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE;
}
// Copy the requested picture data to the output.
memcpy(p1stPictureInfoOut, &gPictureInfos.back(), picture_size);
// Correct the outputted picture struct size.
memcpy(p1stPictureInfoOut, &picture_size, sizeof(u64));
}
if (outputInfo->pictureCount > 1) {

View File

@ -74,57 +74,4 @@ struct OrbisVideodec2AvcPictureInfo {
};
static_assert(sizeof(OrbisVideodec2AvcPictureInfo) == 0x78);
// An older version of the OrbisVideodec2AvcPictureInfo struct
// Keeping this is needed for compatiblity with older games.
struct OrbisVideodec2LegacyAvcPictureInfo {
u64 thisSize;
bool isValid;
u64 ptsData;
u64 dtsData;
u64 attachedData;
u8 idrPictureflag;
u8 profile_idc;
u8 level_idc;
u32 pic_width_in_mbs_minus1;
u32 pic_height_in_map_units_minus1;
u8 frame_mbs_only_flag;
u8 frame_cropping_flag;
u32 frameCropLeftOffset;
u32 frameCropRightOffset;
u32 frameCropTopOffset;
u32 frameCropBottomOffset;
u8 aspect_ratio_info_present_flag;
u8 aspect_ratio_idc;
u16 sar_width;
u16 sar_height;
u8 video_signal_type_present_flag;
u8 video_format;
u8 video_full_range_flag;
u8 colour_description_present_flag;
u8 colour_primaries;
u8 transfer_characteristics;
u8 matrix_coefficients;
u8 timing_info_present_flag;
u32 num_units_in_tick;
u32 time_scale;
u8 fixed_frame_rate_flag;
u8 bitstream_restriction_flag;
u8 max_dec_frame_buffering;
u8 pic_struct_present_flag;
u8 pic_struct;
u8 field_pic_flag;
u8 bottom_field_flag;
};
static_assert(sizeof(OrbisVideodec2LegacyAvcPictureInfo) == 0x68);
} // namespace Libraries::Videodec2

View File

@ -12,7 +12,6 @@
namespace Libraries::Videodec2 {
std::vector<OrbisVideodec2AvcPictureInfo> gPictureInfos;
std::vector<OrbisVideodec2LegacyAvcPictureInfo> gLegacyPictureInfos;
static inline void CopyNV12Data(u8* dst, const AVFrame& src) {
if (src.width == src.linesize[0]) {
@ -132,46 +131,27 @@ s32 VdecDecoder::Decode(const OrbisVideodec2InputData& inputData,
outputInfo.isErrorFrame = false;
outputInfo.pictureCount = 1; // TODO: 2 pictures for interlaced video
// For proper compatibility with older games, check the inputted OutputInfo struct size.
// Only set framePitchInBytes if the game uses the newer struct version.
if (outputInfo.thisSize == sizeof(OrbisVideodec2OutputInfo)) {
// framePitchInBytes only exists in the newer struct.
outputInfo.framePitchInBytes = frame->width;
if (outputInfo.isValid) {
OrbisVideodec2AvcPictureInfo pictureInfo = {};
outputInfo.framePitchInBytes = frame->linesize[0];
}
pictureInfo.thisSize = sizeof(OrbisVideodec2AvcPictureInfo);
pictureInfo.isValid = true;
if (outputInfo.isValid) {
OrbisVideodec2AvcPictureInfo pictureInfo = {};
pictureInfo.ptsData = inputData.ptsData;
pictureInfo.dtsData = inputData.dtsData;
pictureInfo.attachedData = inputData.attachedData;
pictureInfo.thisSize = sizeof(OrbisVideodec2AvcPictureInfo);
pictureInfo.isValid = true;
pictureInfo.frameCropLeftOffset = frame->crop_left;
pictureInfo.frameCropRightOffset = frame->crop_right;
pictureInfo.frameCropTopOffset = frame->crop_top;
pictureInfo.frameCropBottomOffset = frame->crop_bottom;
pictureInfo.ptsData = inputData.ptsData;
pictureInfo.dtsData = inputData.dtsData;
pictureInfo.attachedData = inputData.attachedData;
gPictureInfos.push_back(pictureInfo);
}
} else {
if (outputInfo.isValid) {
// If the game uses the older struct versions, we need to use it too.
OrbisVideodec2LegacyAvcPictureInfo pictureInfo = {};
pictureInfo.frameCropLeftOffset = frame->crop_left;
pictureInfo.frameCropRightOffset = frame->crop_right;
pictureInfo.frameCropTopOffset = frame->crop_top;
pictureInfo.frameCropBottomOffset = frame->crop_bottom;
pictureInfo.thisSize = sizeof(OrbisVideodec2LegacyAvcPictureInfo);
pictureInfo.isValid = true;
pictureInfo.ptsData = inputData.ptsData;
pictureInfo.dtsData = inputData.dtsData;
pictureInfo.attachedData = inputData.attachedData;
pictureInfo.frameCropLeftOffset = frame->crop_left;
pictureInfo.frameCropRightOffset = frame->crop_right;
pictureInfo.frameCropTopOffset = frame->crop_top;
pictureInfo.frameCropBottomOffset = frame->crop_bottom;
gLegacyPictureInfos.push_back(pictureInfo);
}
gPictureInfos.push_back(pictureInfo);
}
}

View File

@ -16,7 +16,6 @@ extern "C" {
namespace Libraries::Videodec2 {
extern std::vector<OrbisVideodec2AvcPictureInfo> gPictureInfos;
extern std::vector<OrbisVideodec2LegacyAvcPictureInfo> gLegacyPictureInfos;
class VdecDecoder {
public:

View File

@ -10,6 +10,8 @@
#ifdef _WIN32
#include <windows.h>
#elif defined(__FreeBSD__)
#include <machine/sysarch.h>
#elif defined(__APPLE__) && defined(ARCH_X86_64)
#include <architecture/i386/table.h>
#include <boost/icl/interval_set.hpp>
@ -157,12 +159,17 @@ Tcb* GetTcbBase() {
#elif defined(ARCH_X86_64)
// Other POSIX x86_64
// Linux x86_64
#if defined(__FreeBSD__)
void SetTcbBase(void* image_address) {
amd64_set_gsbase(image_address);
}
#else
void SetTcbBase(void* image_address) {
const int ret = syscall(SYS_arch_prctl, ARCH_SET_GS, (unsigned long)image_address);
ASSERT_MSG(ret == 0, "Failed to set GS base: errno {}", errno);
}
#endif
Tcb* GetTcbBase() {
return Libraries::Kernel::g_curthread->tcb;

View File

@ -28,6 +28,7 @@
#include "common/singleton.h"
#include "core/debugger.h"
#include "core/devtools/widget/module_list.h"
#include "core/emulator_settings.h"
#include "core/emulator_state.h"
#include "core/file_format/psf.h"
#include "core/file_format/trp.h"
@ -38,6 +39,7 @@
#include "core/libraries/save_data/save_backup.h"
#include "core/linker.h"
#include "core/memory.h"
#include "core/user_settings.h"
#include "emulator.h"
#include "video_core/cache_storage.h"
#include "video_core/renderdoc.h"
@ -50,6 +52,7 @@
#include <sys/wait.h>
#include <unistd.h>
#endif
#include <core/file_format/npbind.h>
Frontend::WindowSDL* g_window = nullptr;
@ -106,7 +109,8 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
} else {
game_folder = file.parent_path();
if (const auto game_folder_name = game_folder.filename().string();
game_folder_name.ends_with("-UPDATE") || game_folder_name.ends_with("-patch")) {
game_folder_name.ends_with("-UPDATE") || game_folder_name.ends_with("-patch") ||
game_folder_name.ends_with("-mods")) {
// If an executable was launched from a separate update directory,
// use the base game directory as the game folder.
const std::string base_name = game_folder_name.substr(0, game_folder_name.rfind('-'));
@ -196,14 +200,20 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
}
game_info.game_folder = game_folder;
std::filesystem::path npbindPath = game_folder / "sce_sys/npbind.dat";
NPBindFile npbind;
if (!npbind.Load(npbindPath.string())) {
LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file");
} else {
auto npCommIds = npbind.GetNpCommIds();
if (npCommIds.empty()) {
LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat");
} else {
game_info.npCommIds = std::move(npCommIds);
}
}
EmulatorSettings.Load(id);
if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) /
(id + ".json"))) {
EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(true);
} else {
EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(false);
}
// Initialize logging as soon as possible
if (!id.empty() && EmulatorSettings.IsSeparateLoggingEnabled()) {
@ -224,9 +234,8 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
LOG_INFO(Loader, "Description {}", Common::g_scm_desc);
LOG_INFO(Loader, "Remote {}", Common::g_scm_remote_url);
const bool has_game_config = std::filesystem::exists(
Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".json"));
LOG_INFO(Config, "Game-specific config exists: {}", has_game_config);
LOG_INFO(Config, "Game-specific config used: {}",
EmulatorState::GetInstance()->IsGameSpecifigConfigUsed());
LOG_INFO(Config, "General LogType: {}", EmulatorSettings.GetLogType());
LOG_INFO(Config, "General isIdenticalLogGrouped: {}", EmulatorSettings.IsIdenticalLogGrouped());
@ -284,12 +293,19 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
}
}
std::filesystem::path mods_folder = game_folder;
mods_folder += "-mods";
if (std::filesystem::exists(mods_folder) && !std::filesystem::is_empty(mods_folder)) {
LOG_INFO(Loader, "Files found in game mods folder");
}
// Create stdin/stdout/stderr
Common::Singleton<FileSys::HandleTable>::Instance()->CreateStdHandles();
// Initialize components
memory = Core::Memory::Instance();
controller = Common::Singleton<Input::GameController>::Instance();
controllers = Common::Singleton<Input::GameControllers>::Instance();
linker = Common::Singleton<Core::Linker>::Instance();
// Load renderdoc module
@ -298,15 +314,30 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
// Initialize patcher and trophies
if (!id.empty()) {
MemoryPatcher::g_game_serial = id;
Libraries::Np::NpTrophy::game_serial = id;
const auto trophyDir =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / id / "TrophyFiles";
if (!std::filesystem::exists(trophyDir)) {
TRP trp;
if (!trp.Extract(game_folder, id)) {
LOG_ERROR(Loader, "Couldn't extract trophies");
int index = 0;
for (std::string npCommId : game_info.npCommIds) {
const auto trophyDir =
Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "trophy" / npCommId;
if (!std::filesystem::exists(trophyDir)) {
TRP trp;
if (!trp.Extract(game_folder, index, npCommId, trophyDir)) {
LOG_ERROR(Loader, "Couldn't extract trophies");
}
}
for (User user : UserSettings.GetUserManager().GetValidUsers()) {
auto const user_trophy_file = EmulatorSettings.GetHomeDir() /
std::to_string(user.user_id) / "trophy" /
(npCommId + ".xml");
if (!std::filesystem::exists(user_trophy_file)) {
auto temp = user_trophy_file.parent_path();
std::filesystem::create_directories(temp);
std::error_code discard;
std::filesystem::copy_file(trophyDir / "Xml" / "TROPCONF.XML", user_trophy_file,
discard);
}
}
index++;
}
}
@ -331,7 +362,7 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
}
}
window = std::make_unique<Frontend::WindowSDL>(EmulatorSettings.GetWindowWidth(),
EmulatorSettings.GetWindowHeight(), controller,
EmulatorSettings.GetWindowHeight(), controllers,
window_title);
g_window = window.get();
@ -512,7 +543,7 @@ void Emulator::Restart(std::filesystem::path eboot_path,
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
#elif defined(__APPLE__) || defined(__linux__)
#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
std::vector<char*> argv;
// Emulator executable

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -43,7 +43,7 @@ private:
void LoadSystemModules(const std::string& game_serial);
Core::MemoryManager* memory;
Input::GameController* controller;
Input::GameControllers* controllers;
Core::Linker* linker;
std::unique_ptr<Frontend::WindowSDL> window;
std::chrono::steady_clock::time_point start_time;

View File

@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/emulator_settings.h"
#include "imgui_translations.h"
namespace ImguiTranslate {
const std::map<u32, std::map<std::string, std::string>> langMap = {
{0, JapaneseMap},
// {1, EnglishUsMap}, - not used
{2, FrenchMap},
{3, SpanishMap},
{4, GermanMap},
{5, ItalianMap},
{6, DutchMap},
{7, PortugesePtMap},
{8, RussianMap},
{9, KoreanMap},
{10, ChineseTraditionalMap},
{11, ChineseSimplifiedMap},
{12, FinnishMap},
{13, SwedishMap},
{14, DanishMap},
{15, NorwegianMap},
{16, PolishMap},
{17, PortugeseBrMap},
// {18, "English (UK)"}, - not used
{19, TurkishMap},
{20, SpanishLatinAmericanMap},
{21, ArabicMap},
{22, FrenchCanadaMap},
{23, CzechMap},
{24, HungarianMap},
{25, GreekMap},
{26, RomanianMap},
{27, ThaiMap},
{28, VietnameseMap},
{29, IndonesianMap},
{30, UkranianMap},
};
std::string tr(std::string input) {
// since we're coding in English
if (EmulatorSettings.GetConsoleLanguage() == 1 || EmulatorSettings.GetConsoleLanguage() == 18)
return input;
const std::map<std::string, std::string> translationTable =
langMap.at(EmulatorSettings.GetConsoleLanguage());
if (!translationTable.contains(input)) {
return input;
}
return translationTable.at(input);
}
} // namespace ImguiTranslate

View File

@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <string>
namespace ImguiTranslate {
std::string tr(std::string input);
///////////// ImGui Translation Tables
// disable clang line limits for ease of translation
// clang-format off
const std::map<std::string, std::string> JapaneseMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> FrenchMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> FrenchCanadaMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> SpanishMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> SpanishLatinAmericanMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> GermanMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> ItalianMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> DutchMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> PortugesePtMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> PortugeseBrMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> RussianMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> KoreanMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> ChineseTraditionalMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> ChineseSimplifiedMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> FinnishMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> SwedishMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> DanishMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> NorwegianMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> PolishMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> TurkishMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> ArabicMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> CzechMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> HungarianMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> GreekMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> RomanianMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> ThaiMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> VietnameseMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> IndonesianMap = {
{"Trophy Earned", "Trophy Earned"},
};
const std::map<std::string, std::string> UkranianMap = {
{"Trophy Earned", "Trophy Earned"},
};
// clang-format on
///////////// End ImGui Translation Tables
} // namespace ImguiTranslate

View File

@ -737,9 +737,8 @@ static void UpdateGamepads() {
ImGuiIO& io = ImGui::GetIO();
SdlData* bd = GetBackendData();
auto controller = Common::Singleton<Input::GameController>::Instance();
auto engine = controller->GetEngine();
SDL_Gamepad* SDLGamepad = engine->m_gamepad;
auto& controllers = *Common::Singleton<Input::GameControllers>::Instance();
SDL_Gamepad* SDLGamepad = controllers[0]->m_sdl_gamepad;
// Update list of gamepads to use
if (bd->want_update_gamepads_list && bd->gamepad_mode != ImGui_ImplSDL3_GamepadMode_Manual) {
if (SDLGamepad) {

View File

@ -1219,6 +1219,10 @@ void ImGuiImplVulkanDestroyDeviceObjects() {
v.device.destroyDescriptorSetLayout(bd->descriptor_set_layout, v.allocator);
bd->descriptor_set_layout = VK_NULL_HANDLE;
}
if (bd->descriptor_pool) {
v.device.destroyDescriptorPool(bd->descriptor_pool, v.allocator);
bd->descriptor_pool = VK_NULL_HANDLE;
}
if (bd->pipeline_layout) {
v.device.destroyPipelineLayout(bd->pipeline_layout, v.allocator);
bd->pipeline_layout = VK_NULL_HANDLE;

View File

@ -1,15 +1,20 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <sstream>
#include <unordered_set>
#include <SDL3/SDL.h>
#include "common/config.h"
#include <common/elf_info.h>
#include <common/singleton.h>
#include "common/logging/log.h"
#include "controller.h"
#include "core/emulator_settings.h"
#include "core/libraries/kernel/time.h"
#include "core/libraries/pad/pad.h"
#include "core/libraries/system/userservice.h"
#include "core/user_settings.h"
#include "input/controller.h"
static std::string SelectedGamepad = "";
namespace Input {
using Libraries::Pad::OrbisPadButtonDataOffset;
@ -22,7 +27,15 @@ void State::OnButton(OrbisPadButtonDataOffset button, bool isPressed) {
}
}
void State::OnAxis(Axis axis, int value) {
void State::OnAxis(Axis axis, int value, bool smooth) {
auto const i = std::to_underlying(axis);
// forcibly finish the previous smoothing task by jumping to the end
axes[i] = axis_smoothing_end_values[i];
axis_smoothing_start_times[i] = time;
axis_smoothing_start_values[i] = axes[i];
axis_smoothing_end_values[i] = value;
axis_smoothing_flags[i] = smooth;
const auto toggle = [&](const auto button) {
if (value > 0) {
buttonsState |= button;
@ -40,7 +53,6 @@ void State::OnAxis(Axis axis, int value) {
default:
break;
}
axes[static_cast<int>(axis)] = value;
}
void State::OnTouchpad(int touchIndex, bool isDown, float x, float y) {
@ -61,6 +73,22 @@ void State::OnAccel(const float accel[3]) {
acceleration.z = accel[2];
}
void State::UpdateAxisSmoothing() {
for (int i = 0; i < std::to_underlying(Axis::AxisMax); i++) {
// if it's not to be smoothed or close enough, just jump to the end
if (!axis_smoothing_flags[i] || std::abs(axes[i] - axis_smoothing_end_values[i]) < 16) {
if (axes[i] != axis_smoothing_end_values[i]) {
axes[i] = axis_smoothing_end_values[i];
}
continue;
}
auto now = Libraries::Kernel::sceKernelGetProcessTime();
f32 t =
std::clamp((now - axis_smoothing_start_times[i]) / f32{axis_smoothing_time}, 0.f, 1.f);
axes[i] = s32(axis_smoothing_start_values[i] * (1 - t) + axis_smoothing_end_values[i] * t);
}
}
GameController::GameController() : m_states_queue(64) {}
void GameController::ReadState(State* state, bool* isConnected, int* connectedCount) {
@ -88,31 +116,86 @@ int GameController::ReadStates(State* states, int states_num, bool* isConnected,
return ret_num;
}
void GameController::Button(int id, OrbisPadButtonDataOffset button, bool is_pressed) {
void GameController::Button(OrbisPadButtonDataOffset button, bool is_pressed) {
m_state.OnButton(button, is_pressed);
PushState();
}
void GameController::Axis(int id, Input::Axis axis, int value) {
m_state.OnAxis(axis, value);
void GameController::Axis(Input::Axis axis, int value, bool smooth) {
m_state.OnAxis(axis, value, smooth);
PushState();
}
void GameController::Gyro(int id, const float gyro[3]) {
m_state.OnGyro(gyro);
void GameController::Gyro(int id) {
m_state.OnGyro(gyro_buf);
PushState();
}
void GameController::Acceleration(int id, const float acceleration[3]) {
m_state.OnAccel(acceleration);
void GameController::Acceleration(int id) {
m_state.OnAccel(accel_buf);
PushState();
}
void GameController::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration,
Libraries::Pad::OrbisFVector3& angularVelocity,
float deltaTime,
Libraries::Pad::OrbisFQuaternion& lastOrientation,
Libraries::Pad::OrbisFQuaternion& orientation) {
void GameController::UpdateGyro(const float gyro[3]) {
std::scoped_lock l(m_states_queue_mutex);
std::memcpy(gyro_buf, gyro, sizeof(gyro_buf));
}
void GameController::UpdateAcceleration(const float acceleration[3]) {
std::scoped_lock l(m_states_queue_mutex);
std::memcpy(accel_buf, acceleration, sizeof(accel_buf));
}
void GameController::UpdateAxisSmoothing() {
m_state.UpdateAxisSmoothing();
}
void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) {
colour = {r, g, b};
if (m_sdl_gamepad != nullptr) {
SDL_SetGamepadLED(m_sdl_gamepad, r, g, b);
}
}
Colour GameController::GetLightBarRGB() {
return colour;
}
void GameController::PollLightColour() {
if (m_sdl_gamepad != nullptr) {
SDL_SetGamepadLED(m_sdl_gamepad, colour.r, colour.g, colour.b);
}
}
bool GameController::SetVibration(u8 smallMotor, u8 largeMotor) {
if (m_sdl_gamepad != nullptr) {
return SDL_RumbleGamepad(m_sdl_gamepad, (smallMotor / 255.0f) * 0xFFFF,
(largeMotor / 255.0f) * 0xFFFF, -1);
}
return true;
}
void GameController::SetTouchpadState(int touchIndex, bool touchDown, float x, float y) {
if (touchIndex < 2) {
m_state.OnTouchpad(touchIndex, touchDown, x, y);
PushState();
}
}
std::array<std::optional<Colour>, 4> GameControllers::controller_override_colors{
std::nullopt, std::nullopt, std::nullopt, std::nullopt};
void GameControllers::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration,
Libraries::Pad::OrbisFVector3& angularVelocity,
float deltaTime,
Libraries::Pad::OrbisFQuaternion& lastOrientation,
Libraries::Pad::OrbisFQuaternion& orientation) {
// avoid wildly off values coming from elapsed time between two samples
// being too high, such as on the first time the controller is polled
if (deltaTime > 1.0f) {
orientation = lastOrientation;
return;
}
Libraries::Pad::OrbisFQuaternion q = lastOrientation;
Libraries::Pad::OrbisFQuaternion ω = {angularVelocity.x, angularVelocity.y, angularVelocity.z,
0.0f};
@ -143,27 +226,100 @@ void GameController::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceler
orientation.y, orientation.z, orientation.w);
}
void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) {
if (!m_engine) {
return;
}
m_engine->SetLightBarRGB(r, g, b);
}
bool is_first_check = true;
void GameController::SetVibration(u8 smallMotor, u8 largeMotor) {
if (!m_engine) {
return;
}
m_engine->SetVibration(smallMotor, largeMotor);
}
void GameControllers::TryOpenSDLControllers() {
using namespace Libraries::UserService;
int controller_count;
s32 move_count = 0;
SDL_JoystickID* new_joysticks = SDL_GetGamepads(&controller_count);
LOG_INFO(Input, "{} controllers are currently connected", controller_count);
void GameController::SetTouchpadState(int touchIndex, bool touchDown, float x, float y) {
if (touchIndex < 2) {
m_state.OnTouchpad(touchIndex, touchDown, x, y);
PushState();
}
}
std::unordered_set<SDL_JoystickID> assigned_ids;
std::array<bool, 4> slot_taken{false, false, false, false};
for (int i = 0; i < 4; i++) {
SDL_Gamepad* pad = controllers[i]->m_sdl_gamepad;
if (pad) {
SDL_JoystickID id = SDL_GetGamepadID(pad);
bool still_connected = false;
ControllerType type = ControllerType::Standard;
for (int j = 0; j < controller_count; j++) {
if (new_joysticks[j] == id) {
still_connected = true;
assigned_ids.insert(id);
slot_taken[i] = true;
break;
}
}
if (!still_connected) {
auto u = UserManagement.GetUserByID(controllers[i]->user_id);
UserManagement.LogoutUser(u);
SDL_CloseGamepad(pad);
controllers[i]->m_sdl_gamepad = nullptr;
controllers[i]->user_id = -1;
slot_taken[i] = false;
}
}
}
for (int j = 0; j < controller_count; j++) {
SDL_JoystickID id = new_joysticks[j];
if (assigned_ids.contains(id))
continue;
SDL_Gamepad* pad = SDL_OpenGamepad(id);
if (!pad) {
continue;
}
for (int i = 0; i < 4; i++) {
if (!slot_taken[i]) {
auto u = UserManagement.GetUserByPlayerIndex(i + 1);
if (!u) {
LOG_INFO(Input, "User {} not found", i + 1);
continue; // for now, if you don't specify who Player N is in the config,
// Player N won't be registered at all
}
auto* c = controllers[i];
c->m_sdl_gamepad = pad;
LOG_INFO(Input, "Gamepad registered for slot {}! Handle: {}", i,
SDL_GetGamepadID(pad));
c->user_id = u->user_id;
slot_taken[i] = true;
UserManagement.LoginUser(u, i + 1);
if (EmulatorSettings.IsMotionControlsEnabled()) {
if (SDL_SetGamepadSensorEnabled(c->m_sdl_gamepad, SDL_SENSOR_GYRO, true)) {
c->gyro_poll_rate =
SDL_GetGamepadSensorDataRate(c->m_sdl_gamepad, SDL_SENSOR_GYRO);
LOG_INFO(Input, "Gyro initialized, poll rate: {}", c->gyro_poll_rate);
} else {
LOG_ERROR(Input, "Failed to initialize gyro controls for gamepad {}",
c->user_id);
}
if (SDL_SetGamepadSensorEnabled(c->m_sdl_gamepad, SDL_SENSOR_ACCEL, true)) {
c->accel_poll_rate =
SDL_GetGamepadSensorDataRate(c->m_sdl_gamepad, SDL_SENSOR_ACCEL);
LOG_INFO(Input, "Accel initialized, poll rate: {}", c->accel_poll_rate);
} else {
LOG_ERROR(Input, "Failed to initialize accel controls for gamepad {}",
c->user_id);
}
}
break;
}
}
}
if (is_first_check) [[unlikely]] {
is_first_check = false;
if (controller_count - move_count == 0) {
auto u = UserManagement.GetUserByPlayerIndex(1);
controllers[0]->user_id = u->user_id;
UserManagement.LoginUser(u, 1);
}
}
SDL_free(new_joysticks);
}
u8 GameController::GetTouchCount() {
return m_touch_count;
}
@ -215,73 +371,37 @@ void GameController::SetLastUpdate(std::chrono::steady_clock::time_point lastUpd
m_last_update = lastUpdate;
}
void GameController::SetEngine(std::unique_ptr<Engine> engine) {
m_engine = std::move(engine);
if (m_engine) {
m_engine->Init();
}
}
Engine* GameController::GetEngine() {
return m_engine.get();
}
void GameController::PushState() {
std::lock_guard lg(m_states_queue_mutex);
m_state.time = Libraries::Kernel::sceKernelGetProcessTime();
m_states_queue.Push(m_state);
}
u32 GameController::Poll() {
if (m_connected) {
PushState();
}
return 33;
}
} // namespace Input
namespace GamepadSelect {
int GetDefaultGamepad(SDL_JoystickID* gamepadIDs, int gamepadCount) {
char GUIDbuf[33];
if (Config::getDefaultControllerID() != "") {
for (int i = 0; i < gamepadCount; i++) {
SDL_GUIDToString(SDL_GetGamepadGUIDForID(gamepadIDs[i]), GUIDbuf, 33);
std::string currentGUID = std::string(GUIDbuf);
if (currentGUID == Config::getDefaultControllerID()) {
return i;
}
}
}
return -1;
}
int GetIndexfromGUID(SDL_JoystickID* gamepadIDs, int gamepadCount, std::string GUID) {
char GUIDbuf[33];
for (int i = 0; i < gamepadCount; i++) {
SDL_GUIDToString(SDL_GetGamepadGUIDForID(gamepadIDs[i]), GUIDbuf, 33);
std::string currentGUID = std::string(GUIDbuf);
if (currentGUID == GUID) {
u8 GameControllers::GetGamepadIndexFromJoystickId(SDL_JoystickID id) {
auto g = SDL_GetGamepadFromID(id);
ASSERT(g != nullptr);
for (int i = 0; i < 5; i++) {
if (controllers[i]->m_sdl_gamepad == g) {
return i;
}
}
// LOG_TRACE(Input, "Gamepad index: {}", index);
return -1;
}
std::string GetGUIDString(SDL_JoystickID* gamepadIDs, int index) {
char GUIDbuf[33];
SDL_GUIDToString(SDL_GetGamepadGUIDForID(gamepadIDs[index]), GUIDbuf, 33);
std::string GUID = std::string(GUIDbuf);
return GUID;
std::optional<u8> GameControllers::GetControllerIndexFromUserID(s32 user_id) {
auto const u = UserManagement.GetUserByID(user_id);
if (!u) {
return std::nullopt;
}
return u->player_index - 1;
}
std::string GetSelectedGamepad() {
return SelectedGamepad;
std::optional<u8> GameControllers::GetControllerIndexFromControllerID(s32 controller_id) {
if (controller_id < 1 || controller_id > 5) {
return std::nullopt;
}
return controller_id - 1;
}
void SetSelectedGamepad(std::string GUID) {
SelectedGamepad = GUID;
}
} // namespace GamepadSelect
} // namespace Input

View File

@ -3,18 +3,25 @@
#pragma once
#include <algorithm>
#include <memory>
#include <mutex>
#include <utility>
#include <vector>
#include <SDL3/SDL_gamepad.h>
#include "SDL3/SDL_joystick.h"
#include "common/assert.h"
#include "common/types.h"
#include "core/libraries/pad/pad.h"
#include "core/libraries/system/userservice.h"
struct SDL_Gamepad;
namespace Input {
enum class ControllerType {
Standard,
};
enum class Axis {
LeftX = 0,
LeftY = 1,
@ -33,37 +40,41 @@ struct TouchpadEntry {
u16 y{};
};
class State {
struct Colour {
u8 r, g, b;
};
struct State {
private:
template <typename T>
using AxisArray = std::array<T, std::to_underlying(Axis::AxisMax)>;
static constexpr AxisArray<s32> axis_defaults{128, 128, 128, 128, 0, 0};
static constexpr u64 axis_smoothing_time{33000};
AxisArray<bool> axis_smoothing_flags{true};
AxisArray<u64> axis_smoothing_start_times{0};
AxisArray<int> axis_smoothing_start_values{axis_defaults};
AxisArray<int> axis_smoothing_end_values{axis_defaults};
public:
void OnButton(Libraries::Pad::OrbisPadButtonDataOffset, bool);
void OnAxis(Axis, int);
void OnAxis(Axis, int, bool smooth = true);
void OnTouchpad(int touchIndex, bool isDown, float x, float y);
void OnGyro(const float[3]);
void OnAccel(const float[3]);
void UpdateAxisSmoothing();
Libraries::Pad::OrbisPadButtonDataOffset buttonsState{};
u64 time = 0;
int axes[static_cast<int>(Axis::AxisMax)] = {128, 128, 128, 128, 0, 0};
AxisArray<s32> axes{axis_defaults};
TouchpadEntry touchpad[2] = {{false, 0, 0}, {false, 0, 0}};
Libraries::Pad::OrbisFVector3 acceleration = {0.0f, 0.0f, 0.0f};
Libraries::Pad::OrbisFVector3 acceleration = {0.0f, -9.81f, 0.0f};
Libraries::Pad::OrbisFVector3 angularVelocity = {0.0f, 0.0f, 0.0f};
Libraries::Pad::OrbisFQuaternion orientation = {0.0f, 0.0f, 0.0f, 1.0f};
};
class Engine {
public:
virtual ~Engine() = default;
virtual void Init() = 0;
virtual void SetLightBarRGB(u8 r, u8 g, u8 b) = 0;
virtual void SetVibration(u8 smallMotor, u8 largeMotor) = 0;
virtual State ReadState() = 0;
virtual float GetAccelPollRate() const = 0;
virtual float GetGyroPollRate() const = 0;
SDL_Gamepad* m_gamepad;
};
inline int GetAxis(int min, int max, int value) {
return std::clamp((255 * (value - min)) / (max - min), 0, 255);
int v = (255 * (value - min)) / (max - min);
return (v < 0 ? 0 : (v > 255 ? 255 : v));
}
template <class T>
@ -98,6 +109,8 @@ private:
};
class GameController {
friend class GameControllers;
public:
GameController();
virtual ~GameController() = default;
@ -105,16 +118,18 @@ public:
void ReadState(State* state, bool* isConnected, int* connectedCount);
int ReadStates(State* states, int states_num, bool* isConnected, int* connectedCount);
void Button(int id, Libraries::Pad::OrbisPadButtonDataOffset button, bool isPressed);
void Axis(int id, Input::Axis axis, int value);
void Gyro(int id, const float gyro[3]);
void Acceleration(int id, const float acceleration[3]);
void Button(Libraries::Pad::OrbisPadButtonDataOffset button, bool isPressed);
void Axis(Input::Axis axis, int value, bool smooth = true);
void Gyro(int id);
void Acceleration(int id);
void UpdateGyro(const float gyro[3]);
void UpdateAcceleration(const float acceleration[3]);
void UpdateAxisSmoothing();
void SetLightBarRGB(u8 r, u8 g, u8 b);
void SetVibration(u8 smallMotor, u8 largeMotor);
Colour GetLightBarRGB();
void PollLightColour();
bool SetVibration(u8 smallMotor, u8 largeMotor);
void SetTouchpadState(int touchIndex, bool touchDown, float x, float y);
void SetEngine(std::unique_ptr<Engine>);
Engine* GetEngine();
u32 Poll();
u8 GetTouchCount();
void SetTouchCount(u8 touchCount);
@ -129,11 +144,12 @@ public:
Libraries::Pad::OrbisFQuaternion GetLastOrientation();
std::chrono::steady_clock::time_point GetLastUpdate();
void SetLastUpdate(std::chrono::steady_clock::time_point lastUpdate);
static void CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration,
Libraries::Pad::OrbisFVector3& angularVelocity,
float deltaTime,
Libraries::Pad::OrbisFQuaternion& lastOrientation,
Libraries::Pad::OrbisFQuaternion& orientation);
float gyro_poll_rate;
float accel_poll_rate;
float gyro_buf[3] = {0.0f, 0.0f, 0.0f}, accel_buf[3] = {0.0f, 9.81f, 0.0f};
s32 user_id = Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID;
SDL_Gamepad* m_sdl_gamepad = nullptr;
private:
void PushState();
@ -146,22 +162,46 @@ private:
bool m_was_secondary_reset = false;
std::chrono::steady_clock::time_point m_last_update = {};
Libraries::Pad::OrbisFQuaternion m_orientation = {0.0f, 0.0f, 0.0f, 1.0f};
Colour colour;
State m_state;
std::mutex m_states_queue_mutex;
RingBufferQueue<State> m_states_queue;
};
std::unique_ptr<Engine> m_engine = nullptr;
class GameControllers {
std::array<GameController*, 5> controllers;
static std::array<std::optional<Colour>, 4> controller_override_colors;
public:
GameControllers()
: controllers({new GameController(), new GameController(), new GameController(),
new GameController(), new GameController()}) {};
virtual ~GameControllers() = default;
GameController* operator[](const size_t& i) const {
if (i > 4) {
UNREACHABLE_MSG("Index {} is out of bounds for GameControllers!", i);
}
return controllers[i];
}
void TryOpenSDLControllers();
u8 GetGamepadIndexFromJoystickId(SDL_JoystickID id);
static std::optional<u8> GetControllerIndexFromUserID(s32 user_id);
static std::optional<u8> GetControllerIndexFromControllerID(s32 controller_id);
static void CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration,
Libraries::Pad::OrbisFVector3& angularVelocity,
float deltaTime,
Libraries::Pad::OrbisFQuaternion& lastOrientation,
Libraries::Pad::OrbisFQuaternion& orientation);
static void SetControllerCustomColor(s32 i, u8 r, u8 g, u8 b) {
controller_override_colors[i] = {r, g, b};
}
static std::optional<Colour> GetControllerCustomColor(s32 i) {
return controller_override_colors[i];
}
};
} // namespace Input
namespace GamepadSelect {
int GetIndexfromGUID(SDL_JoystickID* gamepadIDs, int gamepadCount, std::string GUID);
std::string GetGUIDString(SDL_JoystickID* gamepadIDs, int index);
std::string GetSelectedGamepad();
void SetSelectedGamepad(std::string GUID);
} // namespace GamepadSelect

View File

@ -18,10 +18,10 @@
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_timer.h"
#include "common/config.h"
#include "common/elf_info.h"
#include "common/io_file.h"
#include "common/path_util.h"
#include "common/singleton.h"
#include "core/devtools/layer.h"
#include "core/emulator_settings.h"
#include "core/emulator_state.h"
@ -43,78 +43,185 @@ What structs are needed?
InputBinding(key1, key2, key3)
ControllerOutput(button, axis) - we only need a const array of these, and one of the attr-s is
always 0 BindingConnection(inputBinding (member), controllerOutput (ref to the array element))
Things to always test before pushing like a dumbass:
Button outputs
Axis outputs
Input hierarchy
Multi key inputs
Mouse to joystick
Key toggle
Joystick halfmode
Don't be an idiot and test only the changed part expecting everything else to not be broken
*/
constexpr std::string_view GetDefaultGlobalConfig() {
return R"(# Anything put here will be loaded for all games,
# alongside the game's config or default.ini depending on your preference.
)";
}
constexpr std::string_view GetDefaultInputConfig() {
return R"(#Feeling lost? Check out the Help section!
# Keyboard bindings
triangle = kp8
circle = kp6
cross = kp2
square = kp4
# Alternatives for users without a keypad
triangle = c
circle = b
cross = n
square = v
l1 = q
r1 = u
l2 = e
r2 = o
l3 = x
r3 = m
options = enter
touchpad_center = space
pad_up = up
pad_down = down
pad_left = left
pad_right = right
axis_left_x_minus = a
axis_left_x_plus = d
axis_left_y_minus = w
axis_left_y_plus = s
axis_right_x_minus = j
axis_right_x_plus = l
axis_right_y_minus = i
axis_right_y_plus = k
# Controller bindings
triangle = triangle
cross = cross
square = square
circle = circle
l1 = l1
l2 = l2
l3 = l3
r1 = r1
r2 = r2
r3 = r3
options = options
touchpad_center = back
pad_up = pad_up
pad_down = pad_down
pad_left = pad_left
pad_right = pad_right
axis_left_x = axis_left_x
axis_left_y = axis_left_y
axis_right_x = axis_right_x
axis_right_y = axis_right_y
# Range of deadzones: 1 (almost none) to 127 (max)
analog_deadzone = leftjoystick, 2, 127
analog_deadzone = rightjoystick, 2, 127
override_controller_color = false, 0, 0, 255
)";
}
std::filesystem::path GetInputConfigFile(const std::string& game_id) {
// Read configuration file of the game, and if it doesn't exist, generate it from default
// If that doesn't exist either, generate that from getDefaultConfig() and try again
// If even the folder is missing, we start with that.
const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "input_config";
const auto config_file = config_dir / (game_id + ".ini");
const auto default_config_file = config_dir / "default.ini";
// Ensure the config directory exists
if (!std::filesystem::exists(config_dir)) {
std::filesystem::create_directories(config_dir);
}
// Check if the default config exists
if (!std::filesystem::exists(default_config_file)) {
// If the default config is also missing, create it from getDefaultConfig()
const auto default_config = GetDefaultInputConfig();
std::ofstream default_config_stream(default_config_file);
if (default_config_stream) {
default_config_stream << default_config;
}
}
// if empty, we only need to execute the function up until this point
if (game_id.empty()) {
return default_config_file;
}
// Create global config if it doesn't exist yet
if (game_id == "global" && !std::filesystem::exists(config_file)) {
if (!std::filesystem::exists(config_file)) {
const auto global_config = GetDefaultGlobalConfig();
std::ofstream global_config_stream(config_file);
if (global_config_stream) {
global_config_stream << global_config;
}
}
}
if (game_id == "global") {
std::map<std::string, std::string> default_bindings_to_add = {
{"hotkey_renderdoc_capture", "f12"},
{"hotkey_fullscreen", "f11"},
{"hotkey_show_fps", "f10"},
{"hotkey_pause", "f9"},
{"hotkey_reload_inputs", "f8"},
{"hotkey_toggle_mouse_to_joystick", "f7"},
{"hotkey_toggle_mouse_to_gyro", "f6"},
{"hotkey_add_virtual_user", "f5"},
{"hotkey_remove_virtual_user", "f4"},
{"hotkey_toggle_mouse_to_touchpad", "delete"},
{"hotkey_quit", "lctrl, lshift, end"},
{"hotkey_volume_up", "kpplus"},
{"hotkey_volume_down", "kpminus"},
};
std::ifstream global_in(config_file);
std::string line;
while (std::getline(global_in, line)) {
line.erase(std::remove_if(line.begin(), line.end(),
[](unsigned char c) { return std::isspace(c); }),
line.end());
std::size_t equal_pos = line.find('=');
if (equal_pos == std::string::npos) {
continue;
}
std::string output_string = line.substr(0, equal_pos);
default_bindings_to_add.erase(output_string);
}
global_in.close();
std::ofstream global_out(config_file, std::ios::app);
for (auto const& b : default_bindings_to_add) {
global_out << b.first << " = " << b.second << "\n";
}
}
// If game-specific config doesn't exist, create it from the default config
if (!std::filesystem::exists(config_file)) {
std::filesystem::copy(default_config_file, config_file);
}
return config_file;
}
bool leftjoystick_halfmode = false, rightjoystick_halfmode = false;
std::pair<int, int> leftjoystick_deadzone, rightjoystick_deadzone, lefttrigger_deadzone,
righttrigger_deadzone;
std::array<std::pair<int, int>, 4> leftjoystick_deadzone, rightjoystick_deadzone,
lefttrigger_deadzone, righttrigger_deadzone;
std::list<std::pair<InputEvent, bool>> pressed_keys;
std::list<InputID> toggled_keys;
static std::vector<BindingConnection> connections;
auto output_array = std::array{
// Important: these have to be the first, or else they will update in the wrong order
ControllerOutput(LEFTJOYSTICK_HALFMODE),
ControllerOutput(RIGHTJOYSTICK_HALFMODE),
ControllerOutput(KEY_TOGGLE),
ControllerOutput(MOUSE_GYRO_ROLL_MODE),
GameControllers ControllerOutput::controllers =
*Common::Singleton<Input::GameControllers>::Instance();
// Button mappings
ControllerOutput(SDL_GAMEPAD_BUTTON_NORTH), // Triangle
ControllerOutput(SDL_GAMEPAD_BUTTON_EAST), // Circle
ControllerOutput(SDL_GAMEPAD_BUTTON_SOUTH), // Cross
ControllerOutput(SDL_GAMEPAD_BUTTON_WEST), // Square
ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), // L1
ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_STICK), // L3
ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), // R1
ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_STICK), // R3
ControllerOutput(SDL_GAMEPAD_BUTTON_START), // Options
ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT), // TouchPad
ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER), // TouchPad
ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT), // TouchPad
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_UP), // Up
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_DOWN), // Down
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_LEFT), // Left
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), // Right
// Axis mappings
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY, false),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER),
ControllerOutput(HOTKEY_FULLSCREEN),
ControllerOutput(HOTKEY_PAUSE),
ControllerOutput(HOTKEY_SIMPLE_FPS),
ControllerOutput(HOTKEY_QUIT),
ControllerOutput(HOTKEY_RELOAD_INPUTS),
ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_JOYSTICK),
ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_GYRO),
ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD),
ControllerOutput(HOTKEY_RENDERDOC),
ControllerOutput(HOTKEY_VOLUME_UP),
ControllerOutput(HOTKEY_VOLUME_DOWN),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID),
std::array<ControllerAllOutputs, 9> output_arrays = {
ControllerAllOutputs(0), ControllerAllOutputs(1), ControllerAllOutputs(2),
ControllerAllOutputs(3), ControllerAllOutputs(4), ControllerAllOutputs(5),
ControllerAllOutputs(6), ControllerAllOutputs(7), ControllerAllOutputs(8),
};
void ControllerOutput::LinkJoystickAxes() {
@ -158,6 +265,8 @@ static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) {
return OPBDO::TouchPad;
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
return OPBDO::L1;
case SDL_GAMEPAD_BUTTON_MISC1: // Move
return OPBDO::L1;
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
return OPBDO::R1;
case SDL_GAMEPAD_BUTTON_LEFT_STICK:
@ -223,10 +332,19 @@ InputBinding GetBindingFromString(std::string& line) {
return InputBinding(keys[0], keys[1], keys[2]);
}
std::optional<int> parseInt(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt;
}
};
void ParseInputConfig(const std::string game_id = "") {
std::string game_id_or_default = Config::GetUseUnifiedInputConfig() ? "default" : game_id;
const auto config_file = Config::GetInputConfigFile(game_id_or_default);
const auto global_config_file = Config::GetInputConfigFile("global");
std::string game_id_or_default =
EmulatorSettings.IsUseUnifiedInputConfig() ? "default" : game_id;
const auto config_file = GetInputConfigFile(game_id_or_default);
const auto global_config_file = GetInputConfigFile("global");
// we reset these here so in case the user fucks up or doesn't include some of these,
// we can fall back to default
@ -235,13 +353,14 @@ void ParseInputConfig(const std::string game_id = "") {
float mouse_speed = 1;
float mouse_speed_offset = 0.125;
leftjoystick_deadzone = {1, 127};
rightjoystick_deadzone = {1, 127};
lefttrigger_deadzone = {1, 127};
righttrigger_deadzone = {1, 127};
// me when I'm in a type deduction tournament and my opponent is clang
constexpr std::array<std::pair<int, int>, 4> default_deadzone = {
std::pair{1, 127}, {1, 127}, {1, 127}, {1, 127}};
Config::SetOverrideControllerColor(false);
Config::SetControllerCustomColor(0, 0, 255);
leftjoystick_deadzone = default_deadzone;
rightjoystick_deadzone = default_deadzone;
lefttrigger_deadzone = default_deadzone;
righttrigger_deadzone = default_deadzone;
int lineCount = 0;
@ -278,21 +397,37 @@ void ParseInputConfig(const std::string game_id = "") {
std::string output_string = line.substr(0, equal_pos);
std::string input_string = line.substr(equal_pos + 1);
// Remove trailing semicolon from input_string
if (!input_string.empty() && input_string[input_string.length() - 1] == ';' &&
input_string != ";") {
line = line.substr(0, line.length() - 1);
s8 input_gamepad_id = -1, output_gamepad_id = -1; // -1 means it's not specified
// input gamepad id is only for controllers, it's discarded otherwise
std::size_t input_colon_pos = input_string.find(':');
if (input_colon_pos != std::string::npos) {
auto temp = parseInt(input_string.substr(input_colon_pos + 1));
if (!temp) {
LOG_WARNING(Input, "Invalid gamepad ID value at line {}: \"{}\"", lineCount, line);
} else {
input_gamepad_id = *temp;
}
input_string = input_string.substr(0, input_colon_pos);
}
// if not provided, assume it's for all gamepads, if the input is a controller and that also
// doesn't have an ID, and for the first otherwise
std::size_t output_colon_pos = output_string.find(':');
if (output_colon_pos != std::string::npos) {
auto temp = parseInt(output_string.substr(output_colon_pos + 1));
if (!temp) {
LOG_WARNING(Input, "Invalid gamepad ID value at line {}: \"{}\"", lineCount, line);
} else {
output_gamepad_id = *temp;
}
output_string = output_string.substr(0, output_colon_pos);
}
std::size_t comma_pos = input_string.find(',');
auto parseInt = [](const std::string& s) -> std::optional<int> {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt;
}
};
// todo make override_controller_color and analog_deadzone be controller specific
// instead of global
if (output_string == "mouse_to_joystick") {
if (input_string == "left") {
SetMouseToJoystick(1);
@ -315,7 +450,7 @@ void ParseInputConfig(const std::string game_id = "") {
return;
}
ControllerOutput* toggle_out =
&*std::ranges::find(output_array, ControllerOutput(KEY_TOGGLE));
&*std::ranges::find(output_arrays[0].data, ControllerOutput(KEY_TOGGLE));
BindingConnection toggle_connection = BindingConnection(
InputBinding(toggle_keys.keys[0]), toggle_out, 0, toggle_keys.keys[1]);
connections.insert(connections.end(), toggle_connection);
@ -356,15 +491,17 @@ void ParseInputConfig(const std::string game_id = "") {
std::pair<int, int> deadzone = {*inner_deadzone, *outer_deadzone};
static std::unordered_map<std::string, std::pair<int, int>&> deadzone_map = {
{"leftjoystick", leftjoystick_deadzone},
{"rightjoystick", rightjoystick_deadzone},
{"l2", lefttrigger_deadzone},
{"r2", righttrigger_deadzone},
};
static std::unordered_map<std::string, std::array<std::pair<int, int>, 4>&>
deadzone_map = {
{"leftjoystick", leftjoystick_deadzone},
{"rightjoystick", rightjoystick_deadzone},
{"l2", lefttrigger_deadzone},
{"r2", righttrigger_deadzone},
};
output_gamepad_id = output_gamepad_id == -1 ? 1 : output_gamepad_id;
if (auto it = deadzone_map.find(device); it != deadzone_map.end()) {
it->second = deadzone;
it->second[output_gamepad_id - 1] = deadzone;
LOG_DEBUG(Input, "Parsed deadzone: {} {} {}", device, inner_deadzone_str,
outer_deadzone_str);
} else {
@ -390,10 +527,12 @@ void ParseInputConfig(const std::string game_id = "") {
lineCount, line);
return;
}
Config::SetOverrideControllerColor(enable == "true");
Config::SetControllerCustomColor(*r, *g, *b);
LOG_DEBUG(Input, "Parsed color settings: {} {} {} {}",
enable == "true" ? "override" : "no override", *r, *b, *g);
output_gamepad_id = output_gamepad_id == -1 ? 1 : output_gamepad_id;
if (enable == "true") {
GameControllers::SetControllerCustomColor(output_gamepad_id - 1, *r, *g, *b);
}
LOG_DEBUG(Input, "Parsed color settings: {} {} - {} {} {}",
enable == "true" ? "override" : "no override", output_gamepad_id, *r, *b, *g);
return;
}
@ -410,31 +549,46 @@ void ParseInputConfig(const std::string game_id = "") {
auto axis_it = string_to_axis_map.find(output_string);
if (button_it != string_to_cbutton_map.end()) {
connection = BindingConnection(
binding, &*std::ranges::find(output_array, ControllerOutput(button_it->second)));
connections.insert(connections.end(), connection);
binding,
&*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data,
ControllerOutput(button_it->second)));
} else if (hotkey_it != string_to_hotkey_map.end()) {
connection = BindingConnection(
binding, &*std::ranges::find(output_array, ControllerOutput(hotkey_it->second)));
connections.insert(connections.end(), connection);
binding,
&*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data,
ControllerOutput(hotkey_it->second)));
} else if (axis_it != string_to_axis_map.end()) {
int value_to_set = binding.keys[2].type == InputType::Axis ? 0 : axis_it->second.value;
connection = BindingConnection(
binding,
&*std::ranges::find(output_array, ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID,
axis_it->second.axis,
axis_it->second.value >= 0)),
&*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data,
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID,
axis_it->second.axis,
axis_it->second.value >= 0)),
value_to_set);
connections.insert(connections.end(), connection);
} else {
LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.",
lineCount, line);
return;
}
// if the input binding contains a controller input, and gamepad ID
// isn't specified for either inputs or output (both are -1), then multiply the binding and
// add it to all 4 controllers
if (connection.HasGamepadInput() && input_gamepad_id == -1 && output_gamepad_id == -1) {
for (int i = 0; i < output_arrays.size(); i++) {
BindingConnection copy = connection.CopyWithChangedGamepadId(i + 1);
copy.output = &*std::ranges::find(output_arrays[i].data, *connection.output);
connections.push_back(copy);
}
} else {
connections.push_back(connection);
}
LOG_DEBUG(Input, "Succesfully parsed line {}", lineCount);
};
while (std::getline(global_config_stream, line)) {
ProcessLine();
}
lineCount = 0;
while (std::getline(config_stream, line)) {
ProcessLine();
}
@ -446,6 +600,16 @@ void ParseInputConfig(const std::string game_id = "") {
LOG_DEBUG(Input, "Done parsing the input config!");
}
BindingConnection BindingConnection::CopyWithChangedGamepadId(u8 gamepad) {
BindingConnection copy = *this;
for (auto& key : copy.binding.keys) {
if (key.type == InputType::Controller || key.type == InputType::Axis) {
key.gamepad_id = gamepad;
}
}
return copy;
}
u32 GetMouseWheelEvent(const SDL_Event& event) {
if (event.type != SDL_EVENT_MOUSE_WHEEL && event.type != SDL_EVENT_MOUSE_WHEEL_OFF) {
LOG_WARNING(Input, "Something went wrong with wheel input parsing!");
@ -464,6 +628,7 @@ u32 GetMouseWheelEvent(const SDL_Event& event) {
}
InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) {
u8 gamepad = 1;
switch (e.type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
@ -478,21 +643,17 @@ InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) {
e.type == SDL_EVENT_MOUSE_WHEEL, 0);
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
return InputEvent(InputType::Controller, static_cast<u32>(e.gbutton.button), e.gbutton.down,
0); // clang made me do it
gamepad = ControllerOutput::controllers.GetGamepadIndexFromJoystickId(e.gbutton.which) + 1;
return InputEvent({InputType::Controller, (u32)e.gbutton.button, gamepad}, e.gbutton.down,
0);
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
return InputEvent(InputType::Axis, static_cast<u32>(e.gaxis.axis), true,
e.gaxis.value / 256); // this too
gamepad = ControllerOutput::controllers.GetGamepadIndexFromJoystickId(e.gaxis.which) + 1;
return InputEvent({InputType::Axis, (u32)e.gaxis.axis, gamepad}, true, e.gaxis.value / 256);
default:
return InputEvent();
}
}
GameController* ControllerOutput::controller = nullptr;
void ControllerOutput::SetControllerOutputController(GameController* c) {
ControllerOutput::controller = c;
}
void ToggleKeyInList(InputID input) {
if (input.type == InputType::Axis) {
LOG_ERROR(Input, "Toggling analog inputs is not supported!");
@ -538,7 +699,7 @@ void ControllerOutput::AddUpdate(InputEvent event) {
*new_param = (event.active ? event.axis_value : 0) + *new_param;
}
}
void ControllerOutput::FinalizeUpdate() {
void ControllerOutput::FinalizeUpdate(u8 gamepad_index) {
auto PushSDLEvent = [&](u32 event_type) {
if (new_button_state) {
SDL_Event e;
@ -553,20 +714,24 @@ void ControllerOutput::FinalizeUpdate() {
}
old_button_state = new_button_state;
old_param = *new_param;
bool is_game_specific = EmulatorState::GetInstance()->IsGameSpecifigConfigUsed();
GameController* controller;
if (gamepad_index < 5)
controller = controllers[gamepad_index];
else
UNREACHABLE();
if (button != SDL_GAMEPAD_BUTTON_INVALID) {
switch (button) {
case SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT:
controller->SetTouchpadState(0, new_button_state, 0.25f, 0.5f);
controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state);
controller->Button(SDLGamepadToOrbisButton(button), new_button_state);
break;
case SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER:
controller->SetTouchpadState(0, new_button_state, 0.50f, 0.5f);
controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state);
controller->Button(SDLGamepadToOrbisButton(button), new_button_state);
break;
case SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT:
controller->SetTouchpadState(0, new_button_state, 0.75f, 0.5f);
controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state);
controller->Button(SDLGamepadToOrbisButton(button), new_button_state);
break;
case LEFTJOYSTICK_HALFMODE:
leftjoystick_halfmode = new_button_state;
@ -598,6 +763,12 @@ void ControllerOutput::FinalizeUpdate() {
case HOTKEY_RENDERDOC:
PushSDLEvent(SDL_EVENT_RDOC_CAPTURE);
break;
case HOTKEY_ADD_VIRTUAL_USER:
PushSDLEvent(SDL_EVENT_ADD_VIRTUAL_USER);
break;
case HOTKEY_REMOVE_VIRTUAL_USER:
PushSDLEvent(SDL_EVENT_REMOVE_VIRTUAL_USER);
break;
case HOTKEY_VOLUME_UP:
EmulatorSettings.SetVolumeSlider(
std::clamp(EmulatorSettings.GetVolumeSlider() + 10, 0, 500));
@ -618,7 +789,7 @@ void ControllerOutput::FinalizeUpdate() {
SetMouseGyroRollMode(new_button_state);
break;
default: // is a normal key (hopefully)
controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state);
controller->Button(SDLGamepadToOrbisButton(button), new_button_state);
break;
}
} else if (axis != SDL_GAMEPAD_AXIS_INVALID && positive_axis) {
@ -638,28 +809,28 @@ void ControllerOutput::FinalizeUpdate() {
switch (c_axis) {
case Axis::LeftX:
case Axis::LeftY:
ApplyDeadzone(new_param, leftjoystick_deadzone);
ApplyDeadzone(new_param, leftjoystick_deadzone[gamepad_index]);
multiplier = leftjoystick_halfmode ? 0.5 : 1.0;
break;
case Axis::RightX:
case Axis::RightY:
ApplyDeadzone(new_param, rightjoystick_deadzone);
ApplyDeadzone(new_param, rightjoystick_deadzone[gamepad_index]);
multiplier = rightjoystick_halfmode ? 0.5 : 1.0;
break;
case Axis::TriggerLeft:
ApplyDeadzone(new_param, lefttrigger_deadzone);
controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param));
controller->Button(0, OrbisPadButtonDataOffset::L2, *new_param > 0x20);
ApplyDeadzone(new_param, lefttrigger_deadzone[gamepad_index]);
controller->Axis(c_axis, GetAxis(0x0, 0x7f, *new_param));
controller->Button(OrbisPadButtonDataOffset::L2, *new_param > 0x20);
return;
case Axis::TriggerRight:
ApplyDeadzone(new_param, righttrigger_deadzone);
controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param));
controller->Button(0, OrbisPadButtonDataOffset::R2, *new_param > 0x20);
ApplyDeadzone(new_param, righttrigger_deadzone[gamepad_index]);
controller->Axis(c_axis, GetAxis(0x0, 0x7f, *new_param));
controller->Button(OrbisPadButtonDataOffset::R2, *new_param > 0x20);
return;
default:
break;
}
controller->Axis(0, c_axis, GetAxis(-0x80, 0x7f, *new_param * multiplier));
controller->Axis(c_axis, GetAxis(-0x80, 0x7f, *new_param * multiplier));
}
}
@ -674,11 +845,9 @@ bool UpdatePressedKeys(InputEvent event) {
if (input.type == InputType::Axis) {
// analog input, it gets added when it first sends an event,
// and from there, it only changes the parameter
auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input,
[](const std::pair<InputEvent, bool>& e, InputID i) {
return std::tie(e.first.input.type, e.first.input.sdl_id) <
std::tie(i.type, i.sdl_id);
});
auto it = std::lower_bound(
pressed_keys.begin(), pressed_keys.end(), input,
[](const std::pair<InputEvent, bool>& e, InputID i) { return e.first.input < i; });
if (it == pressed_keys.end() || it->first.input != input) {
pressed_keys.insert(it, {event, false});
LOG_DEBUG(Input, "Added axis {} to the input list", event.input.sdl_id);
@ -791,25 +960,33 @@ InputEvent BindingConnection::ProcessBinding() {
}
void ActivateOutputsFromInputs() {
// Reset values and flags
for (auto& it : pressed_keys) {
it.second = false;
}
for (auto& it : output_array) {
it.ResetUpdate();
}
// Check for input blockers
ApplyMouseInputBlockers();
// todo find a better solution
for (int i = 0; i < output_arrays.size(); i++) {
// Iterate over all inputs, and update their respecive outputs accordingly
for (auto& it : connections) {
it.output->AddUpdate(it.ProcessBinding());
}
// Reset values and flags
for (auto& it : pressed_keys) {
it.second = false;
}
for (auto& it : output_arrays[i].data) {
it.ResetUpdate();
}
// Update all outputs
for (auto& it : output_array) {
it.FinalizeUpdate();
// Check for input blockers
ApplyMouseInputBlockers();
// Iterate over all inputs, and update their respecive outputs accordingly
for (auto& it : connections) {
// only update this when it's the correct pass
if (it.output->gamepad_id == i) {
it.output->AddUpdate(it.ProcessBinding());
}
}
// Update all outputs
for (auto& it : output_arrays[i].data) {
it.FinalizeUpdate(i);
}
}
}

View File

@ -7,6 +7,7 @@
#include <map>
#include <string>
#include <unordered_set>
#include <vector>
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_timer.h"
@ -35,9 +36,11 @@
#define SDL_EVENT_MOUSE_TO_JOYSTICK SDL_EVENT_USER + 6
#define SDL_EVENT_MOUSE_TO_GYRO SDL_EVENT_USER + 7
#define SDL_EVENT_MOUSE_TO_TOUCHPAD SDL_EVENT_USER + 8
#define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 9
#define SDL_EVENT_QUIT_DIALOG SDL_EVENT_USER + 10
#define SDL_EVENT_MOUSE_WHEEL_OFF SDL_EVENT_USER + 11
#define SDL_EVENT_QUIT_DIALOG SDL_EVENT_USER + 9
#define SDL_EVENT_MOUSE_WHEEL_OFF SDL_EVENT_USER + 10
#define SDL_EVENT_ADD_VIRTUAL_USER SDL_EVENT_USER + 11
#define SDL_EVENT_REMOVE_VIRTUAL_USER SDL_EVENT_USER + 12
#define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 13
#define LEFTJOYSTICK_HALFMODE 0x00010000
#define RIGHTJOYSTICK_HALFMODE 0x00020000
@ -57,6 +60,8 @@
#define HOTKEY_RENDERDOC 0xf0000009
#define HOTKEY_VOLUME_UP 0xf000000a
#define HOTKEY_VOLUME_DOWN 0xf000000b
#define HOTKEY_ADD_VIRTUAL_USER 0xf000000c
#define HOTKEY_REMOVE_VIRTUAL_USER 0xf000000d
#define SDL_UNMAPPED UINT32_MAX - 1
@ -77,21 +82,24 @@ class InputID {
public:
InputType type;
u32 sdl_id;
InputID(InputType d = InputType::Count, u32 i = SDL_UNMAPPED) : type(d), sdl_id(i) {}
u8 gamepad_id;
InputID(InputType d = InputType::Count, u32 i = (u32)-1, u8 g = 1)
: type(d), sdl_id(i), gamepad_id(g) {}
bool operator==(const InputID& o) const {
return type == o.type && sdl_id == o.sdl_id;
return type == o.type && sdl_id == o.sdl_id && gamepad_id == o.gamepad_id;
}
bool operator!=(const InputID& o) const {
return type != o.type || sdl_id != o.sdl_id;
return type != o.type || sdl_id != o.sdl_id || gamepad_id != o.gamepad_id;
}
bool operator<=(const InputID& o) const {
return type <= o.type && sdl_id <= o.sdl_id;
auto operator<=>(const InputID& o) const {
return std::tie(gamepad_id, type, sdl_id, gamepad_id) <=>
std::tie(o.gamepad_id, o.type, o.sdl_id, o.gamepad_id);
}
bool IsValid() const {
return *this != InputID();
}
std::string ToString() {
return fmt::format("({}: {:x})", input_type_names[static_cast<u8>(type)], sdl_id);
return fmt::format("({}. {}: {:x})", gamepad_id, input_type_names[(u8)type], sdl_id);
}
};
@ -149,6 +157,8 @@ const std::map<std::string, u32> string_to_hotkey_map = {
{"hotkey_toggle_mouse_to_gyro", HOTKEY_TOGGLE_MOUSE_TO_GYRO},
{"hotkey_toggle_mouse_to_touchpad", HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD},
{"hotkey_renderdoc_capture", HOTKEY_RENDERDOC},
{"hotkey_add_virtual_user", HOTKEY_ADD_VIRTUAL_USER},
{"hotkey_remove_virtual_user", HOTKEY_REMOVE_VIRTUAL_USER},
{"hotkey_volume_up", HOTKEY_VOLUME_UP},
{"hotkey_volume_down", HOTKEY_VOLUME_DOWN},
};
@ -401,7 +411,7 @@ public:
inline bool IsEmpty() {
return !(keys[0].IsValid() || keys[1].IsValid() || keys[2].IsValid());
}
std::string ToString() { // todo add device type
std::string ToString() {
switch (KeyCount()) {
case 1:
return fmt::format("({})", keys[0].ToString());
@ -420,14 +430,14 @@ public:
};
class ControllerOutput {
static GameController* controller;
public:
static void SetControllerOutputController(GameController* c);
static GameControllers controllers;
static void GetGetGamepadIndexFromSDLJoystickID(const SDL_JoystickID id) {}
static void LinkJoystickAxes();
u32 button;
u32 axis;
u8 gamepad_id;
// these are only used as s8,
// but I added some padding to avoid overflow if it's activated by multiple inputs
// axis_plus and axis_minus pairs share a common new_param, the other outputs have their own
@ -441,6 +451,7 @@ public:
new_param = new s16(0);
old_param = 0;
positive_axis = p;
gamepad_id = 0;
}
ControllerOutput(const ControllerOutput& o) : button(o.button), axis(o.axis) {
new_param = new s16(*o.new_param);
@ -466,7 +477,7 @@ public:
void ResetUpdate();
void AddUpdate(InputEvent event);
void FinalizeUpdate();
void FinalizeUpdate(u8 gamepad_index);
};
class BindingConnection {
public:
@ -481,6 +492,13 @@ public:
output = out;
toggle = t;
}
BindingConnection& operator=(const BindingConnection& o) {
binding = o.binding;
output = o.output;
axis_param = o.axis_param;
toggle = o.toggle;
return *this;
}
bool operator<(const BindingConnection& other) const {
// a button is a higher priority than an axis, as buttons can influence axes
// (e.g. joystick_halfmode)
@ -494,9 +512,82 @@ public:
}
return false;
}
bool HasGamepadInput() {
for (auto& key : binding.keys) {
if (key.type == InputType::Controller || key.type == InputType::Axis) {
return true;
}
}
return false;
}
BindingConnection CopyWithChangedGamepadId(u8 gamepad);
InputEvent ProcessBinding();
};
class ControllerAllOutputs {
public:
static constexpr u64 output_count = 40;
std::array<ControllerOutput, output_count> data = {
// Important: these have to be the first, or else they will update in the wrong order
ControllerOutput(LEFTJOYSTICK_HALFMODE),
ControllerOutput(RIGHTJOYSTICK_HALFMODE),
ControllerOutput(KEY_TOGGLE),
ControllerOutput(MOUSE_GYRO_ROLL_MODE),
// Button mappings
ControllerOutput(SDL_GAMEPAD_BUTTON_NORTH), // Triangle
ControllerOutput(SDL_GAMEPAD_BUTTON_EAST), // Circle
ControllerOutput(SDL_GAMEPAD_BUTTON_SOUTH), // Cross
ControllerOutput(SDL_GAMEPAD_BUTTON_WEST), // Square
ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), // L1
ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_STICK), // L3
ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), // R1
ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_STICK), // R3
ControllerOutput(SDL_GAMEPAD_BUTTON_START), // Options
ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT), // TouchPad
ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER), // TouchPad
ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT), // TouchPad
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_UP), // Up
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_DOWN), // Down
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_LEFT), // Left
ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), // Right
// Axis mappings
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX, false),
// ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY, false),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER),
ControllerOutput(HOTKEY_FULLSCREEN),
ControllerOutput(HOTKEY_PAUSE),
ControllerOutput(HOTKEY_SIMPLE_FPS),
ControllerOutput(HOTKEY_QUIT),
ControllerOutput(HOTKEY_RELOAD_INPUTS),
ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_JOYSTICK),
ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_GYRO),
ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD),
ControllerOutput(HOTKEY_RENDERDOC),
ControllerOutput(HOTKEY_ADD_VIRTUAL_USER),
ControllerOutput(HOTKEY_REMOVE_VIRTUAL_USER),
ControllerOutput(HOTKEY_VOLUME_UP),
ControllerOutput(HOTKEY_VOLUME_DOWN),
ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID),
};
ControllerAllOutputs(u8 g) {
for (int i = 0; i < output_count; i++) {
data[i].gamepad_id = g;
}
}
};
// Updates the list of pressed keys with the given input.
// Returns whether the list was updated or not.
bool UpdatePressedKeys(InputEvent event);

View File

@ -77,11 +77,11 @@ void EmulateJoystick(GameController* controller, u32 interval) {
float a_x = cos(angle) * output_speed, a_y = sin(angle) * output_speed;
if (d_x != 0 || d_y != 0) {
controller->Axis(0, axis_x, GetAxis(-0x80, 0x7f, a_x));
controller->Axis(0, axis_y, GetAxis(-0x80, 0x7f, a_y));
controller->Axis(axis_x, GetAxis(-0x80, 0x7f, a_x), false);
controller->Axis(axis_y, GetAxis(-0x80, 0x7f, a_y), false);
} else {
controller->Axis(0, axis_x, GetAxis(-0x80, 0x7f, 0));
controller->Axis(0, axis_y, GetAxis(-0x80, 0x7f, 0));
controller->Axis(axis_x, GetAxis(-0x80, 0x7f, 0), false);
controller->Axis(axis_y, GetAxis(-0x80, 0x7f, 0), false);
}
}
@ -89,13 +89,13 @@ constexpr float constant_down_accel[3] = {0.0f, 9.81f, 0.0f};
void EmulateGyro(GameController* controller, u32 interval) {
float d_x = 0, d_y = 0;
SDL_GetRelativeMouseState(&d_x, &d_y);
controller->Acceleration(1, constant_down_accel);
controller->UpdateAcceleration(constant_down_accel);
float gyro_from_mouse[3] = {-d_y / 100, -d_x / 100, 0.0f};
if (mouse_gyro_roll_mode) {
gyro_from_mouse[1] = 0.0f;
gyro_from_mouse[2] = -d_x / 100;
}
controller->Gyro(1, gyro_from_mouse);
controller->UpdateGyro(gyro_from_mouse);
}
void EmulateTouchpad(GameController* controller, u32 interval) {
@ -104,7 +104,7 @@ void EmulateTouchpad(GameController* controller, u32 interval) {
controller->SetTouchpadState(0, (mouse_buttons & SDL_BUTTON_LMASK) != 0,
std::clamp(x / g_window->GetWidth(), 0.0f, 1.0f),
std::clamp(y / g_window->GetHeight(), 0.0f, 1.0f));
controller->Button(0, Libraries::Pad::OrbisPadButtonDataOffset::TouchPad,
controller->Button(Libraries::Pad::OrbisPadButtonDataOffset::TouchPad,
(mouse_buttons & SDL_BUTTON_RMASK) != 0);
}

View File

@ -8,12 +8,14 @@
#include "SDL3/SDL_timer.h"
#include "SDL3/SDL_video.h"
#include "common/assert.h"
#include "common/config.h"
#include "common/elf_info.h"
#include "core/debug_state.h"
#include "core/devtools/layer.h"
#include "core/emulator_settings.h"
#include "core/libraries/kernel/time.h"
#include "core/libraries/pad/pad.h"
#include "core/libraries/system/userservice.h"
#include "core/user_settings.h"
#include "imgui/renderer/imgui_core.h"
#include "input/controller.h"
#include "input/input_handler.h"
@ -26,9 +28,9 @@
#endif
#include <core/emulator_settings.h>
namespace Input {
namespace Frontend {
using Libraries::Pad::OrbisPadButtonDataOffset;
using namespace Libraries::Pad;
static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) {
using OPBDO = OrbisPadButtonDataOffset;
@ -69,220 +71,24 @@ static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) {
}
}
static SDL_GamepadAxis InputAxisToSDL(Axis axis) {
switch (axis) {
case Axis::LeftX:
return SDL_GAMEPAD_AXIS_LEFTX;
case Axis::LeftY:
return SDL_GAMEPAD_AXIS_LEFTY;
case Axis::RightX:
return SDL_GAMEPAD_AXIS_RIGHTX;
case Axis::RightY:
return SDL_GAMEPAD_AXIS_RIGHTY;
case Axis::TriggerLeft:
return SDL_GAMEPAD_AXIS_LEFT_TRIGGER;
case Axis::TriggerRight:
return SDL_GAMEPAD_AXIS_RIGHT_TRIGGER;
default:
UNREACHABLE();
}
}
SDLInputEngine::~SDLInputEngine() {
if (m_gamepad) {
SDL_CloseGamepad(m_gamepad);
}
}
void SDLInputEngine::Init() {
if (m_gamepad) {
SDL_CloseGamepad(m_gamepad);
m_gamepad = nullptr;
}
int gamepad_count;
SDL_JoystickID* gamepads = SDL_GetGamepads(&gamepad_count);
if (!gamepads) {
LOG_ERROR(Input, "Cannot get gamepad list: {}", SDL_GetError());
return;
}
if (gamepad_count == 0) {
LOG_INFO(Input, "No gamepad found!");
SDL_free(gamepads);
return;
}
int selectedIndex = GamepadSelect::GetIndexfromGUID(gamepads, gamepad_count,
GamepadSelect::GetSelectedGamepad());
int defaultIndex =
GamepadSelect::GetIndexfromGUID(gamepads, gamepad_count, Config::getDefaultControllerID());
// If user selects a gamepad in the GUI, use that, otherwise try the default
if (!m_gamepad) {
if (selectedIndex != -1) {
m_gamepad = SDL_OpenGamepad(gamepads[selectedIndex]);
LOG_INFO(Input, "Opening gamepad selected in GUI.");
} else if (defaultIndex != -1) {
m_gamepad = SDL_OpenGamepad(gamepads[defaultIndex]);
LOG_INFO(Input, "Opening default gamepad.");
} else {
m_gamepad = SDL_OpenGamepad(gamepads[0]);
LOG_INFO(Input, "Got {} gamepads. Opening the first one.", gamepad_count);
}
}
if (!m_gamepad) {
if (!m_gamepad) {
LOG_ERROR(Input, "Failed to open gamepad: {}", SDL_GetError());
SDL_free(gamepads);
return;
}
}
SDL_Joystick* joystick = SDL_GetGamepadJoystick(m_gamepad);
Uint16 vendor = SDL_GetJoystickVendor(joystick);
Uint16 product = SDL_GetJoystickProduct(joystick);
bool isDualSense = (vendor == 0x054C && product == 0x0CE6);
LOG_INFO(Input, "Gamepad Vendor: {:04X}, Product: {:04X}", vendor, product);
if (isDualSense) {
LOG_INFO(Input, "Detected DualSense Controller");
}
if (Config::getIsMotionControlsEnabled()) {
if (SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_GYRO, true)) {
m_gyro_poll_rate = SDL_GetGamepadSensorDataRate(m_gamepad, SDL_SENSOR_GYRO);
LOG_INFO(Input, "Gyro initialized, poll rate: {}", m_gyro_poll_rate);
} else {
LOG_ERROR(Input, "Failed to initialize gyro controls for gamepad, error: {}",
SDL_GetError());
SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_GYRO, false);
}
if (SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_ACCEL, true)) {
m_accel_poll_rate = SDL_GetGamepadSensorDataRate(m_gamepad, SDL_SENSOR_ACCEL);
LOG_INFO(Input, "Accel initialized, poll rate: {}", m_accel_poll_rate);
} else {
LOG_ERROR(Input, "Failed to initialize accel controls for gamepad, error: {}",
SDL_GetError());
SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_ACCEL, false);
}
}
SDL_free(gamepads);
int* rgb = Config::GetControllerCustomColor();
if (isDualSense) {
if (SDL_SetJoystickLED(joystick, rgb[0], rgb[1], rgb[2]) == 0) {
LOG_INFO(Input, "Set DualSense LED to R:{} G:{} B:{}", rgb[0], rgb[1], rgb[2]);
} else {
LOG_ERROR(Input, "Failed to set DualSense LED: {}", SDL_GetError());
}
} else {
SetLightBarRGB(rgb[0], rgb[1], rgb[2]);
}
}
void SDLInputEngine::SetLightBarRGB(u8 r, u8 g, u8 b) {
if (m_gamepad) {
SDL_SetGamepadLED(m_gamepad, r, g, b);
}
}
void SDLInputEngine::SetVibration(u8 smallMotor, u8 largeMotor) {
if (m_gamepad) {
const auto low_freq = (smallMotor / 255.0f) * 0xFFFF;
const auto high_freq = (largeMotor / 255.0f) * 0xFFFF;
SDL_RumbleGamepad(m_gamepad, low_freq, high_freq, -1);
}
}
State SDLInputEngine::ReadState() {
State state{};
state.time = Libraries::Kernel::sceKernelGetProcessTime();
// Buttons
for (u8 i = 0; i < SDL_GAMEPAD_BUTTON_COUNT; ++i) {
auto orbisButton = SDLGamepadToOrbisButton(i);
if (orbisButton == OrbisPadButtonDataOffset::None) {
continue;
}
state.OnButton(orbisButton, SDL_GetGamepadButton(m_gamepad, (SDL_GamepadButton)i));
}
// Axes
for (int i = 0; i < static_cast<int>(Axis::AxisMax); ++i) {
const auto axis = static_cast<Axis>(i);
const auto value = SDL_GetGamepadAxis(m_gamepad, InputAxisToSDL(axis));
switch (axis) {
case Axis::TriggerLeft:
case Axis::TriggerRight:
state.OnAxis(axis, GetAxis(0, 0x8000, value));
break;
default:
state.OnAxis(axis, GetAxis(-0x8000, 0x8000, value));
break;
}
}
// Touchpad
if (SDL_GetNumGamepadTouchpads(m_gamepad) > 0) {
for (int finger = 0; finger < 2; ++finger) {
bool down;
float x, y;
if (SDL_GetGamepadTouchpadFinger(m_gamepad, 0, finger, &down, &x, &y, NULL)) {
state.OnTouchpad(finger, down, x, y);
}
}
}
// Gyro
if (SDL_GamepadHasSensor(m_gamepad, SDL_SENSOR_GYRO)) {
float gyro[3];
if (SDL_GetGamepadSensorData(m_gamepad, SDL_SENSOR_GYRO, gyro, 3)) {
state.OnGyro(gyro);
}
}
// Accel
if (SDL_GamepadHasSensor(m_gamepad, SDL_SENSOR_ACCEL)) {
float accel[3];
if (SDL_GetGamepadSensorData(m_gamepad, SDL_SENSOR_ACCEL, accel, 3)) {
state.OnAccel(accel);
}
}
return state;
}
float SDLInputEngine::GetGyroPollRate() const {
return m_gyro_poll_rate;
}
float SDLInputEngine::GetAccelPollRate() const {
return m_accel_poll_rate;
}
} // namespace Input
namespace Frontend {
using namespace Libraries::Pad;
std::mutex motion_control_mutex;
float gyro_buf[3] = {0.0f, 0.0f, 0.0f}, accel_buf[3] = {0.0f, 9.81f, 0.0f};
static Uint32 SDLCALL PollGyroAndAccel(void* userdata, SDL_TimerID timer_id, Uint32 interval) {
static Uint32 SDLCALL PollController(void* userdata, SDL_TimerID timer_id, Uint32 interval) {
auto* controller = reinterpret_cast<Input::GameController*>(userdata);
std::scoped_lock l{motion_control_mutex};
controller->Gyro(0, gyro_buf);
controller->Acceleration(0, accel_buf);
return 4;
controller->UpdateAxisSmoothing();
controller->Gyro(0);
controller->Acceleration(0);
return interval;
}
WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_,
static Uint32 SDLCALL PollControllerLightColour(void* userdata, SDL_TimerID timer_id,
Uint32 interval) {
auto* controller = reinterpret_cast<Input::GameController*>(userdata);
controller->PollLightColour();
return interval;
}
WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controllers_,
std::string_view window_title)
: width{width_}, height{height_}, controller{controller_} {
: width{width_}, height{height_}, controllers{*controllers_} {
if (!SDL_SetHint(SDL_HINT_APP_NAME, "shadPS4")) {
UNREACHABLE_MSG("Failed to set SDL window hint: {}", SDL_GetError());
}
@ -290,7 +96,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_
UNREACHABLE_MSG("Failed to initialize SDL video subsystem: {}", SDL_GetError());
}
if (!SDL_Init(SDL_INIT_CAMERA)) {
UNREACHABLE_MSG("Failed to initialize SDL camera subsystem: {}", SDL_GetError());
LOG_ERROR(Input, "Failed to initialize SDL camera subsystem: {}", SDL_GetError());
}
SDL_InitSubSystem(SDL_INIT_AUDIO);
@ -330,13 +136,13 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_
SDL_SyncWindow(window);
SDL_InitSubSystem(SDL_INIT_GAMEPAD);
controller->SetEngine(std::make_unique<Input::SDLInputEngine>());
#if defined(SDL_PLATFORM_WIN32)
window_info.type = WindowSystemType::Windows;
window_info.render_surface = SDL_GetPointerProperty(SDL_GetWindowProperties(window),
SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
#elif defined(SDL_PLATFORM_LINUX)
#elif defined(SDL_PLATFORM_LINUX) || defined(__FreeBSD__)
// SDL doesn't have a platform define for FreeBSD AAAAAAAAAA
if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) {
window_info.type = WindowSystemType::X11;
window_info.display_connection = SDL_GetPointerProperty(
@ -355,11 +161,11 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_
window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(window));
#endif
// input handler init-s
Input::ControllerOutput::SetControllerOutputController(controller);
Input::ControllerOutput::LinkJoystickAxes();
Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
controllers.TryOpenSDLControllers();
if (Config::getBackgroundControllerInput()) {
if (EmulatorSettings.IsBackgroundControllerInput()) {
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
}
}
@ -399,37 +205,16 @@ void WindowSDL::WaitEvent() {
break;
case SDL_EVENT_GAMEPAD_ADDED:
case SDL_EVENT_GAMEPAD_REMOVED:
controller->SetEngine(std::make_unique<Input::SDLInputEngine>());
break;
case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
controller->SetTouchpadState(event.gtouchpad.finger,
event.type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP, event.gtouchpad.x,
event.gtouchpad.y);
controllers.TryOpenSDLControllers();
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
OnGamepadEvent(&event);
break;
// i really would have appreciated ANY KIND OF DOCUMENTATION ON THIS
// AND IT DOESN'T EVEN USE PROPER ENUMS
case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
case SDL_EVENT_GAMEPAD_SENSOR_UPDATE:
switch ((SDL_SensorType)event.gsensor.sensor) {
case SDL_SENSOR_GYRO: {
std::scoped_lock l{motion_control_mutex};
memcpy(gyro_buf, event.gsensor.data, sizeof(gyro_buf));
break;
}
case SDL_SENSOR_ACCEL: {
std::scoped_lock l{motion_control_mutex};
memcpy(accel_buf, event.gsensor.data, sizeof(accel_buf));
break;
}
default:
break;
}
OnGamepadEvent(&event);
break;
case SDL_EVENT_QUIT:
is_open = false;
@ -455,7 +240,7 @@ void WindowSDL::WaitEvent() {
}
break;
case SDL_EVENT_CHANGE_CONTROLLER:
controller->GetEngine()->Init();
UNREACHABLE_MSG("todo");
break;
case SDL_EVENT_TOGGLE_SIMPLE_FPS:
Overlay::ToggleSimpleFps();
@ -476,6 +261,29 @@ void WindowSDL::WaitEvent() {
Input::ToggleMouseModeTo(Input::MouseMode::Touchpad));
SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), false);
break;
case SDL_EVENT_ADD_VIRTUAL_USER:
for (int i = 0; i < 4; i++) {
if (controllers[i]->user_id == -1) {
auto u = UserManagement.GetUserByPlayerIndex(i + 1);
if (!u) {
break;
}
controllers[i]->user_id = u->user_id;
UserManagement.LoginUser(u, i + 1);
break;
}
}
break;
case SDL_EVENT_REMOVE_VIRTUAL_USER:
LOG_INFO(Input, "Remove user");
for (int i = 3; i >= 0; i--) {
if (controllers[i]->user_id != -1) {
UserManagement.LogoutUser(UserManagement.GetUserByID(controllers[i]->user_id));
controllers[i]->user_id = -1;
break;
}
}
break;
case SDL_EVENT_RDOC_CAPTURE:
VideoCore::TriggerCapture();
break;
@ -485,8 +293,10 @@ void WindowSDL::WaitEvent() {
}
void WindowSDL::InitTimers() {
SDL_AddTimer(4, &PollGyroAndAccel, controller);
SDL_AddTimer(33, Input::MousePolling, (void*)controller);
for (int i = 0; i < 4; ++i) {
SDL_AddTimer(4, &PollController, controllers[i]);
}
SDL_AddTimer(33, Input::MousePolling, (void*)controllers[0]);
}
void WindowSDL::RequestKeyboard() {
@ -554,10 +364,44 @@ void WindowSDL::OnGamepadEvent(const SDL_Event* event) {
// as it would break the entire touchpad handling
// You can still bind other things to it though
if (event->gbutton.button == SDL_GAMEPAD_BUTTON_TOUCHPAD) {
controller->Button(0, OrbisPadButtonDataOffset::TouchPad, input_down);
controllers[controllers.GetGamepadIndexFromJoystickId(event->gbutton.which)]->Button(
OrbisPadButtonDataOffset::TouchPad, input_down);
return;
}
u8 gamepad;
switch (event->type) {
case SDL_EVENT_GAMEPAD_SENSOR_UPDATE:
switch ((SDL_SensorType)event->gsensor.sensor) {
case SDL_SENSOR_GYRO:
gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which);
if (gamepad < 5) {
controllers[gamepad]->UpdateGyro(event->gsensor.data);
}
break;
case SDL_SENSOR_ACCEL:
gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which);
if (gamepad < 5) {
controllers[gamepad]->UpdateAcceleration(event->gsensor.data);
}
break;
default:
break;
}
return;
case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
controllers[controllers.GetGamepadIndexFromJoystickId(event->gtouchpad.which)]
->SetTouchpadState(event->gtouchpad.finger,
event->type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP, event->gtouchpad.x,
event->gtouchpad.y);
return;
default:
break;
}
// add/remove it from the list
bool inputs_changed = Input::UpdatePressedKeys(input_event);

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -14,23 +14,8 @@ struct SDL_Gamepad;
union SDL_Event;
namespace Input {
class SDLInputEngine : public Engine {
public:
~SDLInputEngine() override;
void Init() override;
void SetLightBarRGB(u8 r, u8 g, u8 b) override;
void SetVibration(u8 smallMotor, u8 largeMotor) override;
float GetGyroPollRate() const override;
float GetAccelPollRate() const override;
State ReadState() override;
private:
float m_gyro_poll_rate = 0.0f;
float m_accel_poll_rate = 0.0f;
};
} // namespace Input
class GameController;
}
namespace Frontend {
@ -62,7 +47,7 @@ class WindowSDL {
int keyboard_grab = 0;
public:
explicit WindowSDL(s32 width, s32 height, Input::GameController* controller,
explicit WindowSDL(s32 width, s32 height, Input::GameControllers* controllers,
std::string_view window_title);
~WindowSDL();
@ -100,7 +85,7 @@ private:
private:
s32 width;
s32 height;
Input::GameController* controller;
Input::GameControllers controllers{};
WindowSystemInfo window_info{};
SDL_Window* window{};
bool is_shown{};

View File

@ -351,6 +351,15 @@ Id EmitBufferAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id ad
&Sirit::Module::OpAtomicCompareExchange);
}
Id EmitBufferAtomicFCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value,
Id cmp_value) {
const auto u32_value = ctx.OpBitcast(ctx.U32[1], value);
const auto u32_cmp = ctx.OpBitcast(ctx.U32[1], cmp_value);
const auto result = BufferAtomicU32CmpSwap(ctx, inst, handle, address, u32_value, u32_cmp,
&Sirit::Module::OpAtomicCompareExchange);
return ctx.OpBitcast(ctx.F32[1], result);
}
Id EmitImageAtomicIAdd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value) {
return ImageAtomicU32(ctx, inst, handle, coords, value, &Sirit::Module::OpAtomicIAdd);
}

View File

@ -108,6 +108,8 @@ Id EmitBufferAtomicXor32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addres
Id EmitBufferAtomicSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value);
Id EmitBufferAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value,
Id cmp_value);
Id EmitBufferAtomicFCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value,
Id cmp_value);
Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, u32 index);
Id EmitGetAttributeU32(EmitContext& ctx, IR::Attribute attr, u32 comp);
void EmitSetAttribute(EmitContext& ctx, IR::Attribute attr, Id value, u32 comp);

View File

@ -45,6 +45,14 @@ CopyShaderData ParseCopyShader(std::span<const u32> code) {
sources[inst.dst[0].code] += inst.control.sopk.simm;
break;
}
case Gcn::Opcode::S_BFM_B32: {
ASSERT(inst.src[0].field == Gcn::OperandField::SignedConstIntPos &&
inst.src[1].field == Gcn::OperandField::SignedConstIntPos);
const auto src0 = inst.src[0].code - Gcn::OperandFieldRange::SignedConstIntPosMin + 1;
const auto src1 = inst.src[1].code - Gcn::OperandFieldRange::SignedConstIntPosMin + 1;
sources[inst.dst[0].code] = ((1 << src0) - 1) << src1;
break;
}
case Gcn::Opcode::EXP: {
const auto& exp = inst.control.exp;
const IR::Attribute semantic = static_cast<IR::Attribute>(exp.target);
@ -71,6 +79,7 @@ CopyShaderData ParseCopyShader(std::span<const u32> code) {
ASSERT(sources[index] != -1);
offsets[inst.src[1].code] += sources[index];
}
data.num_comps++;
break;
}
default:

View File

@ -15,6 +15,7 @@ struct CopyShaderData {
std::map<u32, std::pair<Shader::IR::Attribute, u32>> attr_map;
u32 num_attrs{0};
u32 output_vertices{0};
u32 num_comps{0};
};
CopyShaderData ParseCopyShader(std::span<const u32> code);

View File

@ -3,6 +3,7 @@
#include <algorithm>
#include "common/assert.h"
#include "core/libraries/kernel/process.h"
#include "shader_recompiler/frontend/decode.h"
#include <magic_enum/magic_enum.hpp>
@ -39,6 +40,7 @@ InstEncoding GetInstructionEncoding(u32 token) {
encoding = static_cast<InstEncoding>(token & (u32)EncodingMask::MASK_6bit);
switch (encoding) {
case InstEncoding::VOP3:
case InstEncoding::VOP3P:
case InstEncoding::EXP:
case InstEncoding::VINTRP:
case InstEncoding::DS:
@ -82,7 +84,6 @@ InstEncoding GetInstructionEncoding(u32 token) {
break;
}
UNREACHABLE();
return InstEncoding::ILLEGAL;
}
@ -111,7 +112,7 @@ GcnInst GcnDecodeContext::decodeInstruction(GcnCodeSlice& code) {
const uint32_t token = code.at(0);
InstEncoding encoding = GetInstructionEncoding(token);
ASSERT_MSG(encoding != InstEncoding::ILLEGAL, "illegal encoding");
ASSERT_MSG(encoding != InstEncoding::ILLEGAL, "illegal encoding: {:#x}", token);
uint32_t encodingLen = getEncodingLength(encoding);
// Clear the instruction
@ -155,6 +156,7 @@ uint32_t GcnDecodeContext::getEncodingLength(InstEncoding encoding) {
break;
case InstEncoding::VOP3:
case InstEncoding::VOP3P:
case InstEncoding::MUBUF:
case InstEncoding::MTBUF:
case InstEncoding::MIMG:
@ -189,6 +191,9 @@ uint32_t GcnDecodeContext::getOpMapOffset(InstEncoding encoding) {
case InstEncoding::VOP3:
offset = (uint32_t)OpcodeMap::OP_MAP_VOP3;
break;
case InstEncoding::VOP3P:
offset = (uint32_t)OpcodeMap::OP_MAP_VOP3P;
break;
case InstEncoding::EXP:
offset = (uint32_t)OpcodeMap::OP_MAP_EXP;
break;
@ -381,6 +386,9 @@ void GcnDecodeContext::decodeInstruction64(InstEncoding encoding, GcnCodeSlice&
case InstEncoding::VOP3:
decodeInstructionVOP3(hexInstruction);
break;
case InstEncoding::VOP3P:
decodeInstructionVOP3P(hexInstruction);
break;
case InstEncoding::MUBUF:
decodeInstructionMUBUF(hexInstruction);
break;
@ -712,6 +720,55 @@ void GcnDecodeContext::decodeInstructionVOP3(uint64_t hexInstruction) {
}
}
void GcnDecodeContext::decodeInstructionVOP3P(uint64_t hexInstruction) {
u32 vdst = bit::extract(hexInstruction, 7, 0);
u32 op = bit::extract(hexInstruction, 22, 16);
u32 src0 = bit::extract(hexInstruction, 40, 32);
u32 src1 = bit::extract(hexInstruction, 49, 41);
u32 src2 = bit::extract(hexInstruction, 58, 50);
m_instruction.opcode = static_cast<Opcode>(op + static_cast<u32>(OpcodeMap::OP_MAP_VOP3P));
m_instruction.src[0].field = getOperandField(src0);
m_instruction.src[0].code =
m_instruction.src[0].field == OperandField::VectorGPR ? src0 - VectorGPRMin : src0;
m_instruction.src[1].field = getOperandField(src1);
m_instruction.src[1].code =
m_instruction.src[1].field == OperandField::VectorGPR ? src1 - VectorGPRMin : src1;
m_instruction.src[2].field = getOperandField(src2);
m_instruction.src[2].code =
m_instruction.src[2].field == OperandField::VectorGPR ? src2 - VectorGPRMin : src2;
m_instruction.dst[0].field = OperandField::VectorGPR;
m_instruction.dst[0].code = vdst;
m_instruction.control.vop3p = *reinterpret_cast<InstControlVOP3P*>(&hexInstruction);
// update input modifier
auto& control = m_instruction.control.vop3p;
for (u32 i = 0; i != 3; ++i) {
if (control.neg & (1u << i)) {
m_instruction.src[i].input_modifier.neg = true;
}
if (control.neg_hi & (1u << i)) {
m_instruction.src[i].input_modifier.neg_hi = true;
}
if (control.op_sel & (1u << i)) {
m_instruction.src[i].op_sel.op_sel = true;
}
if (control.get_op_sel_hi(i)) {
m_instruction.src[i].op_sel.op_sel_hi = true;
}
}
// update output modifier
auto& outputMod = m_instruction.dst[0].output_modifier;
outputMod.clamp = static_cast<bool>(control.clamp);
}
void GcnDecodeContext::decodeInstructionMUBUF(uint64_t hexInstruction) {
u32 op = bit::extract(hexInstruction, 24, 18);
u32 vaddr = bit::extract(hexInstruction, 39, 32);

View File

@ -84,6 +84,7 @@ private:
void decodeInstructionVINTRP(uint32_t hexInstruction);
// 64 bits encodings
void decodeInstructionVOP3(uint64_t hexInstruction);
void decodeInstructionVOP3P(uint64_t hexInstruction);
void decodeInstructionMUBUF(uint64_t hexInstruction);
void decodeInstructionMTBUF(uint64_t hexInstruction);
void decodeInstructionMIMG(uint64_t hexInstruction);

View File

@ -1784,6 +1784,88 @@ constexpr std::array<InstFormat, 455> InstructionFormatVOP3 = {{
{},
}};
constexpr std::array<InstFormat, 35> InstructionFormatVOP3P = {{
// 0 = V_PK_MAD_I16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 3, 1, ScalarType::Sint16,
ScalarType::Sint16},
// 1 = V_PK_MUL_LO_U16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16,
ScalarType::Uint16},
// 2 = V_PK_ADD_I16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16,
ScalarType::Sint16},
// 3 = V_PK_SUB_I16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16,
ScalarType::Sint16},
// 4 = V_PK_LSHLREV_B16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16,
ScalarType::Uint16},
// 5 = V_PK_LSHRREV_B16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16,
ScalarType::Uint16},
// 6 = V_PK_ASHRREV_I16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16,
ScalarType::Uint16},
// 7 = V_PK_MAX_I16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16,
ScalarType::Sint16},
// 8 = V_PK_MIN_I16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Sint16,
ScalarType::Sint16},
// 9 = V_PK_MAD_U16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 3, 1, ScalarType::Uint16,
ScalarType::Uint16},
// 10 = V_PK_ADD_U16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16,
ScalarType::Uint16},
// 11 = V_PK_SUB_U16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16,
ScalarType::Uint16},
// 12 = V_PK_MAX_U16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16,
ScalarType::Uint16},
// 13 = V_PK_MIN_U16
{InstClass::VectorIntArith16, InstCategory::VectorALU, 2, 1, ScalarType::Uint16,
ScalarType::Uint16},
// 14 = V_PK_FMA_F16
{InstClass::VectorFpArith16, InstCategory::VectorALU, 3, 1, ScalarType::Float16,
ScalarType::Float16},
// 15 = V_PK_ADD_F16
{InstClass::VectorFpArith16, InstCategory::VectorALU, 2, 1, ScalarType::Float16,
ScalarType::Float16},
// 16 = V_PK_MUL_F16
{InstClass::VectorFpArith16, InstCategory::VectorALU, 2, 1, ScalarType::Float16,
ScalarType::Float16},
// 17 = V_PK_MIN_F16
{InstClass::VectorFpArith16, InstCategory::VectorALU, 2, 1, ScalarType::Float16,
ScalarType::Float16},
// 18 = V_PK_MAX_F16
{InstClass::VectorFpArith16, InstCategory::VectorALU, 2, 1, ScalarType::Float16,
ScalarType::Float16},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
// 32 = V_MAD_MIX_F32
{InstClass::VectorFpArith32, InstCategory::VectorALU, 3, 1, ScalarType::Float32,
ScalarType::Float32},
// 33 = V_MAD_MIXLO_F16
{InstClass::VectorFpArith32, InstCategory::VectorALU, 3, 1, ScalarType::Float32,
ScalarType::Float32},
// 34 = V_MAD_MIXHI_F16
{InstClass::VectorFpArith32, InstCategory::VectorALU, 3, 1, ScalarType::Float32,
ScalarType::Float32},
}};
constexpr std::array<InstFormat, 71> InstructionFormatVOP1 = {{
// 0 = V_NOP
{InstClass::VectorMisc, InstCategory::VectorALU, 0, 1, ScalarType::Any, ScalarType::Any},
@ -3705,6 +3787,8 @@ InstFormat InstructionFormat(InstEncoding encoding, uint32_t opcode) {
return InstructionFormatVOPC[opcode];
case InstEncoding::VOP3:
return InstructionFormatVOP3[opcode];
case InstEncoding::VOP3P:
return InstructionFormatVOP3P[opcode];
case InstEncoding::EXP:
return InstructionFormatEXP[opcode];
case InstEncoding::VINTRP:

View File

@ -3,6 +3,7 @@
#pragma once
#include "common/assert.h"
#include "shader_recompiler/frontend/opcodes.h"
namespace Shader::Gcn {
@ -25,6 +26,7 @@ enum OperandFieldRange {
/// These are applied after loading an operand register.
struct InputModifiers {
bool neg = false;
bool neg_hi = false;
bool abs = false;
};
@ -34,11 +36,18 @@ struct OutputModifiers {
float multiplier = 0.f;
};
struct OperandSelection {
bool op_sel = false;
bool op_sel_hi = false;
};
struct InstOperand {
OperandField field = OperandField::Undefined;
ScalarType type = ScalarType::Undefined;
InputModifiers input_modifier = {};
OutputModifiers output_modifier = {};
// only valid for packed 16bit operations
OperandSelection op_sel = {};
u32 code = 0xFFFFFFFF;
};
@ -90,6 +99,32 @@ struct InstControlVOP3 {
u64 neg : 3;
};
struct InstControlVOP3P {
u64 : 8;
u64 neg_hi : 3;
u64 op_sel : 3;
u64 op_sel_hi_2 : 1;
u64 clamp : 1;
u64 : 43;
u64 op_sel_hi_01 : 2;
u64 neg : 3;
bool get_op_sel_hi(int idx) {
switch (idx) {
case 0:
return (op_sel_hi_01 & 1) == 1;
case 1:
return ((op_sel_hi_01 >> 1) & 1) == 1;
case 2:
return (op_sel_hi_2 & 1) == 1;
default:
UNREACHABLE_MSG("get_op_sel_hi: {}", idx);
}
}
};
static_assert(sizeof(InstControlVOP3P) == 8);
struct InstControlSMRD {
u32 offset : 8;
u32 imm : 1;
@ -174,6 +209,7 @@ union InstControl {
InstControlSOPK sopk;
InstControlSOPP sopp;
InstControlVOP3 vop3;
InstControlVOP3P vop3p;
InstControlSMRD smrd;
InstControlMUBUF mubuf;
InstControlMTBUF mtbuf;

View File

@ -662,6 +662,33 @@ enum class OpcodeVOP3 : u32 {
OP_RANGE_VOP3 = V_EXP_LEGACY_F32 + 1,
};
enum class OpcodeVOP3P : u32 {
V_PK_MAD_I16 = 0,
V_PK_MUL_LO_U16 = 1,
V_PK_ADD_I16 = 2,
V_PK_SUB_I16 = 3,
V_PK_LSHLREV_B16 = 4,
V_PK_LSHRREV_B16 = 5,
V_PK_ASHRREV_I16 = 6,
V_PK_MAX_I16 = 7,
V_PK_MIN_I16 = 8,
V_PK_MAD_U16 = 9,
V_PK_ADD_U16 = 10,
V_PK_SUB_U16 = 11,
V_PK_MAX_U16 = 12,
V_PK_MIN_U16 = 13,
V_PK_FMA_F16 = 14,
V_PK_ADD_F16 = 15,
V_PK_MUL_F16 = 16,
V_PK_MIN_F16 = 17,
V_PK_MAX_F16 = 18,
V_MAD_MIX_F32 = 32,
V_MAD_MIXLO_F16 = 33,
V_MAD_MIXHI_F16 = 34,
OP_RANGE_VOP3P = V_MAD_MIXHI_F16 + 1,
};
enum class OpcodeVOP1 : u32 {
V_NOP = 0,
V_MOV_B32 = 1,
@ -1313,6 +1340,7 @@ enum class OpcodeMap : u32 {
OP_MAP_MTBUF = OP_MAP_MUBUF + (u32)OpcodeMUBUF::OP_RANGE_MUBUF,
OP_MAP_MIMG = OP_MAP_MTBUF + (u32)OpcodeMTBUF::OP_RANGE_MTBUF,
OP_MAP_EXP = OP_MAP_MIMG + (u32)OpcodeMIMG::OP_RANGE_MIMG,
OP_MAP_VOP3P = OP_MAP_EXP + (u32)OpcodeEXP::OP_RANGE_EXP,
};
enum class Opcode : u32 {
@ -2192,6 +2220,29 @@ enum class Opcode : u32 {
IMAGE_SAMPLE_C_CD_CL_O = 111 + (u32)OpcodeMap::OP_MAP_MIMG,
// EXP
EXP = 0 + (u32)OpcodeMap::OP_MAP_EXP,
// VOP3P
V_PK_MAD_I16 = 0 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_MUL_LO_U16 = 1 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_ADD_I16 = 2 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_SUB_I16 = 3 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_LSHLREV_B16 = 4 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_LSHRREV_B16 = 5 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_ASHRREV_I16 = 6 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_MAX_I16 = 7 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_MIN_I16 = 8 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_MAD_U16 = 9 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_ADD_U16 = 10 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_SUB_U16 = 11 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_MAX_U16 = 12 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_MIN_U16 = 13 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_FMA_F16 = 14 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_ADD_F16 = 15 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_MUL_F16 = 16 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_MIN_F16 = 17 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_PK_MAX_F16 = 18 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_MAD_MIX_F32 = 32 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_MAD_MIXLO_F16 = 33 + (u32)OpcodeMap::OP_MAP_VOP3P,
V_MAD_MIXHI_F16 = 34 + (u32)OpcodeMap::OP_MAP_VOP3P,
};
enum class EncodingMask : u32 {
@ -2222,6 +2273,8 @@ enum class InstEncoding : u32 {
VOP3 = 0x00000034u << 26,
/// bits [31:26] - (1 1 1 1 1 0)
EXP = 0x0000003Eu << 26,
/// bits [31:26] - (1 1 0 0 1 1)
VOP3P = 0x00000033u << 26,
/// bits [31:26] - (1 1 0 0 1 0)
VINTRP = 0x00000032u << 26,
/// bits [31:26] - (1 1 0 1 1 0)
@ -2271,6 +2324,7 @@ enum class InstClass : u32 {
VectorBitField32,
VectorThreadMask,
VectorBitField64,
VectorFpArith16,
VectorFpArith32,
VectorFpRound32,
VectorFpField32,
@ -2281,6 +2335,7 @@ enum class InstClass : u32 {
VectorFpField64,
VectorFpTran64,
VectorFpCmp64,
VectorIntArith16,
VectorIntArith32,
VectorIntArith64,
VectorIntCmp32,
@ -2360,8 +2415,10 @@ enum class InstCategory : u32 {
enum class ScalarType : u32 {
Undefined,
Any,
Uint16,
Uint32,
Uint64,
Sint16,
Sint32,
Sint64,
Float16,

View File

@ -116,6 +116,8 @@ void Translator::EmitVectorMemory(const GcnInst& inst) {
return BUFFER_ATOMIC<IR::F32>(AtomicOp::Fmin, inst);
case Opcode::BUFFER_ATOMIC_FMAX:
return BUFFER_ATOMIC<IR::F32>(AtomicOp::Fmax, inst);
case Opcode::BUFFER_ATOMIC_FCMPSWAP:
return BUFFER_ATOMIC<IR::F32>(AtomicOp::FCmpSwap, inst);
// MIMG
// Image load operations
@ -379,6 +381,10 @@ void Translator::BUFFER_ATOMIC(AtomicOp op, const GcnInst& inst) {
const IR::Value cmp_val = ir.GetVectorReg(vdata + 1);
return ir.BufferAtomicCmpSwap(handle, address, vdata_val, cmp_val, buffer_info);
}
case AtomicOp::FCmpSwap: {
const IR::Value cmp_val = ir.GetVectorReg(vdata + 1);
return ir.BufferAtomicFCmpSwap(handle, address, vdata_val, cmp_val, buffer_info);
}
case AtomicOp::Add:
return ir.BufferAtomicIAdd(handle, address, vdata_val, buffer_info);
case AtomicOp::Smin:

View File

@ -627,6 +627,11 @@ Value IREmitter::BufferAtomicCmpSwap(const Value& handle, const Value& address,
return Inst(Opcode::BufferAtomicCmpSwap32, Flags{info}, handle, address, vdata, cmp_value);
}
Value IREmitter::BufferAtomicFCmpSwap(const Value& handle, const Value& address, const Value& vdata,
const Value& cmp_value, BufferInstInfo info) {
return Inst(Opcode::BufferAtomicFCmpSwap32, Flags{info}, handle, address, vdata, cmp_value);
}
U32 IREmitter::DataAppend(const U32& counter) {
return Inst<U32>(Opcode::DataAppend, counter, Imm32(0));
}

View File

@ -166,6 +166,9 @@ public:
[[nodiscard]] Value BufferAtomicCmpSwap(const Value& handle, const Value& address,
const Value& value, const Value& cmp_value,
BufferInstInfo info);
[[nodiscard]] Value BufferAtomicFCmpSwap(const Value& handle, const Value& address,
const Value& value, const Value& cmp_value,
BufferInstInfo info);
[[nodiscard]] U32 DataAppend(const U32& counter);
[[nodiscard]] U32 DataConsume(const U32& counter);

View File

@ -82,6 +82,7 @@ bool Inst::MayHaveSideEffects() const noexcept {
case Opcode::BufferAtomicXor32:
case Opcode::BufferAtomicSwap32:
case Opcode::BufferAtomicCmpSwap32:
case Opcode::BufferAtomicFCmpSwap32:
case Opcode::DataAppend:
case Opcode::DataConsume:
case Opcode::WriteSharedU16:

View File

@ -150,6 +150,7 @@ OPCODE(BufferAtomicOr32, U32, Opaq
OPCODE(BufferAtomicXor32, U32, Opaque, Opaque, U32, )
OPCODE(BufferAtomicSwap32, U32, Opaque, Opaque, U32, )
OPCODE(BufferAtomicCmpSwap32, U32, Opaque, Opaque, U32, U32, )
OPCODE(BufferAtomicFCmpSwap32, F32, Opaque, Opaque, F32, F32, )
// Vector utility
OPCODE(CompositeConstructU32x2, U32x2, U32, U32, )

View File

@ -39,6 +39,7 @@ bool IsBufferAtomic(const IR::Inst& inst) {
case IR::Opcode::BufferAtomicXor32:
case IR::Opcode::BufferAtomicSwap32:
case IR::Opcode::BufferAtomicCmpSwap32:
case IR::Opcode::BufferAtomicFCmpSwap32:
return true;
default:
return false;
@ -1088,7 +1089,8 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) {
}
const auto image_handle = inst.Arg(0);
const auto& image_res = info.images[image_handle.U32() & 0xFFFF];
const auto binding_index = image_handle.U32() & 0xFFFF;
const auto& image_res = info.images[binding_index];
auto image = image_res.GetSharp(info);
// Sample instructions must be handled separately using address register data.
@ -1097,7 +1099,7 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) {
}
IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)};
const auto inst_info = inst.Flags<IR::TextureInstInfo>();
auto inst_info = inst.Flags<IR::TextureInstInfo>();
const auto view_type = image.GetViewType(image_res.is_array);
// Now that we know the image type, adjust texture coordinate vector.
@ -1108,8 +1110,26 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) {
return {body->Arg(0), body->Arg(1)};
case AmdGpu::ImageType::Color1DArray: // x, slice, [lod]
case AmdGpu::ImageType::Color2D: // x, y, [lod]
case AmdGpu::ImageType::Color2DMsaa: // x, y. (sample is passed on different argument)
return {ir.CompositeConstruct(body->Arg(0), body->Arg(1)), body->Arg(2)};
case AmdGpu::ImageType::Color2DMsaa: // x, y. (sample is passed on different argument)
{
auto skip_lod = false;
if (inst_info.has_lod) {
const auto mipid = body->Arg(2);
if (mipid.IsImmediate() && mipid.U32() == 0) {
// if image_x_mip refers to a MSAA image, and mipid is 0, it is safe to be
// skipped and fragid is taken from the next arg
LOG_WARNING(Render_Recompiler, "Encountered a _mip instruction with MSAA "
"image, and mipid is 0, skipping LoD");
inst_info.has_lod.Assign(false);
skip_lod = true;
} else {
UNREACHABLE_MSG(
"Encountered a _mip instruction with MSAA image, and mipid is non-zero");
}
}
return {ir.CompositeConstruct(body->Arg(0), body->Arg(1)), body->Arg(skip_lod ? 3 : 2)};
}
case AmdGpu::ImageType::Color2DArray: // x, y, slice, [lod]
case AmdGpu::ImageType::Color2DMsaaArray: // x, y, slice. (sample is passed on different
// argument)

View File

@ -104,6 +104,13 @@ void RingAccessElimination(const IR::Program& program, const RuntimeInfo& runtim
output_vertices, info.gs_copy_data.output_vertices);
output_vertices = info.gs_copy_data.output_vertices;
}
u32 dwords_per_vertex = gs_info.out_vertex_data_size;
if (info.gs_copy_data.num_comps && info.gs_copy_data.num_comps > dwords_per_vertex) {
LOG_WARNING(Render_Vulkan,
"VERT_ITEMSIZE {} is different than actual number of dwords per vertex {}",
dwords_per_vertex, info.gs_copy_data.num_comps);
dwords_per_vertex = info.gs_copy_data.num_comps;
}
ForEachInstruction([&](IR::IREmitter& ir, IR::Inst& inst) {
const auto opcode = inst.GetOpcode();
@ -139,7 +146,7 @@ void RingAccessElimination(const IR::Program& program, const RuntimeInfo& runtim
const auto offset = inst.Flags<IR::BufferInstInfo>().inst_offset.Value();
const auto data = ir.BitCast<IR::F32>(IR::U32{inst.Arg(2)});
const auto comp_ofs = output_vertices * 4u;
const auto output_size = comp_ofs * gs_info.out_vertex_data_size;
const auto output_size = comp_ofs * dwords_per_vertex;
const auto vc_read_ofs = (((offset / comp_ofs) * comp_ofs) % output_size) * 16u;
const auto& it = info.gs_copy_data.attr_map.find(vc_read_ofs);

View File

@ -7,7 +7,7 @@
#include "common/logging/log.h"
#include "core/emulator_settings.h"
#ifdef __linux__
#ifdef __unix__
#include "common/adaptive_mutex.h"
#else
#include "common/spin_lock.h"

View File

@ -36,8 +36,8 @@
namespace VideoCore {
constexpr size_t PAGE_SIZE = 4_KB;
constexpr size_t PAGE_BITS = 12;
constexpr size_t PM_PAGE_SIZE = 4_KB;
constexpr size_t PM_PAGE_BITS = 12;
struct PageManager::Impl {
struct PageState {
@ -85,7 +85,7 @@ struct PageManager::Impl {
};
static constexpr size_t ADDRESS_BITS = 40;
static constexpr size_t NUM_ADDRESS_PAGES = 1ULL << (40 - PAGE_BITS);
static constexpr size_t NUM_ADDRESS_PAGES = 1ULL << (40 - PM_PAGE_BITS);
static constexpr size_t NUM_ADDRESS_LOCKS = NUM_ADDRESS_PAGES / PAGES_PER_LOCK;
inline static Vulkan::Rasterizer* rasterizer;
#ifdef ENABLE_USERFAULTFD
@ -222,8 +222,8 @@ struct PageManager::Impl {
void UpdatePageWatchers(VAddr addr, u64 size) {
RENDERER_TRACE;
size_t page = addr >> PAGE_BITS;
const u64 page_end = Common::DivCeil(addr + size, PAGE_SIZE);
size_t page = addr >> PM_PAGE_BITS;
const u64 page_end = Common::DivCeil(addr + size, PM_PAGE_SIZE);
// Acquire locks for the range of pages
const auto lock_start = locks.begin() + (page / PAGES_PER_LOCK);
@ -239,15 +239,15 @@ struct PageManager::Impl {
if (range_bytes > 0) {
RENDERER_TRACE;
// Perform pending (un)protect action
Protect(range_begin << PAGE_BITS, range_bytes, perms);
Protect(range_begin << PM_PAGE_BITS, range_bytes, perms);
range_bytes = 0;
potential_range_bytes = 0;
}
};
// Iterate requested pages
const u64 aligned_addr = page << PAGE_BITS;
const u64 aligned_end = page_end << PAGE_BITS;
const u64 aligned_addr = page << PM_PAGE_BITS;
const u64 aligned_end = page_end << PM_PAGE_BITS;
if (!rasterizer->IsMapped(aligned_addr, aligned_end - aligned_addr)) {
LOG_WARNING(Render,
"Tracking memory region {:#x} - {:#x} which is not fully GPU mapped.",
@ -266,7 +266,7 @@ struct PageManager::Impl {
perms = new_perms;
} else if (range_bytes != 0) {
// If the protection did not change, extend the potential range
potential_range_bytes += PAGE_SIZE;
potential_range_bytes += PM_PAGE_SIZE;
}
// Only start a new range if the page must be (un)protected
@ -274,7 +274,7 @@ struct PageManager::Impl {
if (range_bytes == 0) {
// Start a new potential range
range_begin = page;
potential_range_bytes = PAGE_SIZE;
potential_range_bytes = PM_PAGE_SIZE;
}
// Extend current range up to potential range
range_bytes = potential_range_bytes;
@ -293,12 +293,12 @@ struct PageManager::Impl {
if (start_range.second == end_range.second) {
// if all pages are contiguous, use the regular UpdatePageWatchers
const VAddr start_addr = base_addr + (start_range.first << PAGE_BITS);
const u64 size = (start_range.second - start_range.first) << PAGE_BITS;
const VAddr start_addr = base_addr + (start_range.first << PM_PAGE_BITS);
const u64 size = (start_range.second - start_range.first) << PM_PAGE_BITS;
return UpdatePageWatchers<track, is_read>(start_addr, size);
}
size_t base_page = (base_addr >> PAGE_BITS);
size_t base_page = (base_addr >> PM_PAGE_BITS);
ASSERT(base_page % PAGES_PER_LOCK == 0);
std::scoped_lock lk(locks[base_page / PAGES_PER_LOCK]);
auto perms = cached_pages[base_page + start_range.first].Perms();
@ -310,7 +310,7 @@ struct PageManager::Impl {
if (range_bytes > 0) {
RENDERER_TRACE;
// Perform pending (un)protect action
Protect((range_begin << PAGE_BITS), range_bytes, perms);
Protect((range_begin << PM_PAGE_BITS), range_bytes, perms);
range_bytes = 0;
potential_range_bytes = 0;
}
@ -331,7 +331,7 @@ struct PageManager::Impl {
perms = new_perms;
} else if (range_bytes != 0) {
// If the protection did not change, extend the potential range
potential_range_bytes += PAGE_SIZE;
potential_range_bytes += PM_PAGE_SIZE;
}
// If the page is not being updated, skip it
@ -344,7 +344,7 @@ struct PageManager::Impl {
if (range_bytes == 0) {
// Start a new potential range
range_begin = base_page + page;
potential_range_bytes = PAGE_SIZE;
potential_range_bytes = PM_PAGE_SIZE;
}
// Extend current rango up to potential range
range_bytes = potential_range_bytes;
@ -356,7 +356,7 @@ struct PageManager::Impl {
}
std::array<PageState, NUM_ADDRESS_PAGES> cached_pages{};
#ifdef __linux__
#ifdef PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP
using LockType = Common::AdaptiveMutex;
#else
using LockType = Common::SpinLock;

View File

@ -3,6 +3,7 @@
#pragma once
#include <cstddef>
#include <memory>
#include "common/alignment.h"
#include "common/types.h"
@ -15,9 +16,10 @@ class Rasterizer;
namespace VideoCore {
class PageManager {
// PAGE_SIZE and PAGE_BITS conflicts with machine/param.h definitions on freebsd!
// Use the same page size as the tracker.
static constexpr size_t PAGE_BITS = TRACKER_PAGE_BITS;
static constexpr size_t PAGE_SIZE = TRACKER_BYTES_PER_PAGE;
static constexpr size_t PM_PAGE_BITS = TRACKER_PAGE_BITS;
static constexpr size_t PM_PAGE_SIZE = TRACKER_BYTES_PER_PAGE;
// Keep the lock granularity the same as region granularity. (since each regions has
// itself a lock)
@ -43,12 +45,12 @@ public:
/// Returns page aligned address.
static constexpr VAddr GetPageAddr(VAddr addr) {
return Common::AlignDown(addr, PAGE_SIZE);
return Common::AlignDown(addr, PM_PAGE_SIZE);
}
/// Returns address of the next page.
static constexpr VAddr GetNextPageAddr(VAddr addr) {
return Common::AlignUp(addr + 1, PAGE_SIZE);
return Common::AlignUp(addr + 1, PM_PAGE_SIZE);
}
private:

View File

@ -8,6 +8,7 @@
#include "common/assert.h"
#include "common/debug.h"
#include "common/types.h"
#include "imgui/renderer/imgui_core.h"
#include "sdl_window.h"
#include "video_core/renderer_vulkan/liverpool_to_vk.h"
#include "video_core/renderer_vulkan/vk_instance.h"
@ -183,6 +184,7 @@ Instance::Instance(Frontend::WindowSDL& window, s32 physical_device_index,
}
Instance::~Instance() {
ImGui::Core::Shutdown(GetDevice());
vmaDestroyAllocator(allocator);
}

View File

@ -137,14 +137,20 @@ Presenter::Presenter(Frontend::WindowSDL& window_, AmdGpu::Liverpool* liverpool_
Presenter::~Presenter() {
ImGui::Layer::RemoveLayer(Common::Singleton<Core::Devtools::Layer>::Instance());
draw_scheduler.Finish();
present_scheduler.Finish();
flip_scheduler.Finish();
Check(draw_scheduler.CommandBuffer().reset());
Check(present_scheduler.CommandBuffer().reset());
Check(flip_scheduler.CommandBuffer().reset());
const vk::Device device = instance.GetDevice();
for (auto& frame : present_frames) {
vmaDestroyImage(instance.GetAllocator(), frame.image, frame.allocation);
device.destroyImageView(frame.image_view);
device.destroyFence(frame.present_done);
}
ImGui::Core::Shutdown(device);
}
bool Presenter::IsVideoOutSurface(const AmdGpu::ColorBuffer& color_buffer) const {

View File

@ -112,13 +112,13 @@ private:
u32 expected_frame_width{1920};
u32 expected_frame_height{1080};
Frontend::WindowSDL& window;
Instance instance;
HostPasses::FsrPass fsr_pass;
HostPasses::FsrPass::Settings fsr_settings{};
HostPasses::PostProcessingPass::Settings pp_settings{};
HostPasses::PostProcessingPass pp_pass;
Frontend::WindowSDL& window;
AmdGpu::Liverpool* liverpool;
Instance instance;
Scheduler draw_scheduler;
Scheduler present_scheduler;
Scheduler flip_scheduler;

View File

@ -261,6 +261,12 @@ void Swapchain::Destroy() {
LOG_WARNING(Render_Vulkan, "Failed to wait for device to become idle: {}",
vk::to_string(wait_result));
}
for (auto& image_view : images_view) {
device.destroyImageView(image_view);
}
images_view.clear();
if (swapchain) {
device.destroySwapchainKHR(swapchain);
}