Merge remote-tracking branch 'upstream/main' into feature/sceRudpGetStatus

This commit is contained in:
Matthew Biskas 2026-02-11 16:08:12 +01:00
commit 14174cdc26
157 changed files with 11652 additions and 2774 deletions

View File

@ -3,7 +3,16 @@
name: Build and Release
on: [push, pull_request]
on:
push:
paths-ignore:
- "documents/**"
- "**/*.md"
pull_request:
paths-ignore:
- "documents/**"
- "**/*.md"
concurrency:
group: ci-${{ github.event_name }}-${{ github.ref }}

11
.gitmodules vendored
View File

@ -2,10 +2,6 @@
path = externals/zlib-ng
url = https://github.com/shadps4-emu/ext-zlib-ng.git
shallow = true
[submodule "externals/sdl3"]
path = externals/sdl3
url = https://github.com/shadps4-emu/ext-SDL.git
shallow = true
[submodule "externals/fmt"]
path = externals/fmt
url = https://github.com/shadps4-emu/ext-fmt.git
@ -123,3 +119,10 @@
[submodule "externals/aacdec/fdk-aac"]
path = externals/aacdec/fdk-aac
url = https://android.googlesource.com/platform/external/aac
[submodule "externals/CLI11"]
path = externals/CLI11
url = https://github.com/shadexternals/CLI11.git
[submodule "externals/sdl3"]
path = externals/sdl3
url = https://github.com/shadexternals/sdl3.git

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
# Version 3.24 needed for FetchContent OVERRIDE_FIND_PACKAGE
@ -202,7 +202,7 @@ execute_process(
# Set Version
set(EMULATOR_VERSION_MAJOR "0")
set(EMULATOR_VERSION_MINOR "13")
set(EMULATOR_VERSION_MINOR "14")
set(EMULATOR_VERSION_PATCH "1")
set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}")
@ -221,6 +221,7 @@ endif()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
find_package(Boost 1.84.0 CONFIG)
find_package(CLI11 2.6.1 CONFIG)
find_package(FFmpeg 5.1.2 MODULE)
find_package(fmt 10.2.0 CONFIG)
find_package(glslang 15 CONFIG)
@ -281,15 +282,16 @@ set(AJM_LIB src/core/libraries/ajm/ajm.cpp
set(AUDIO_LIB src/core/libraries/audio/audioin.cpp
src/core/libraries/audio/audioin.h
src/core/libraries/audio/sdl_in.h
src/core/libraries/audio/sdl_in.cpp
src/core/libraries/audio/audioin_backend.h
src/core/libraries/audio/audioin_error.h
src/core/libraries/audio/sdl_audio_in.cpp
src/core/libraries/voice/voice.cpp
src/core/libraries/voice/voice.h
src/core/libraries/audio/audioout.cpp
src/core/libraries/audio/audioout.h
src/core/libraries/audio/audioout_backend.h
src/core/libraries/audio/audioout_error.h
src/core/libraries/audio/sdl_audio.cpp
src/core/libraries/audio/sdl_audio_out.cpp
src/core/libraries/ngs2/ngs2.cpp
src/core/libraries/ngs2/ngs2.h
)
@ -494,6 +496,8 @@ set(HLE_LIBC_INTERNAL_LIB src/core/libraries/libc_internal/libc_internal.cpp
src/core/libraries/libc_internal/libc_internal_str.h
src/core/libraries/libc_internal/libc_internal_math.cpp
src/core/libraries/libc_internal/libc_internal_math.h
src/core/libraries/libc_internal/libc_internal_threads.cpp
src/core/libraries/libc_internal/libc_internal_threads.h
src/core/libraries/libc_internal/printf.h
)
@ -524,6 +528,9 @@ set(SYSTEM_GESTURE_LIB
set(PNG_LIB src/core/libraries/libpng/pngdec.cpp
src/core/libraries/libpng/pngdec.h
src/core/libraries/libpng/pngdec_error.h
src/core/libraries/libpng/pngenc.cpp
src/core/libraries/libpng/pngenc.h
src/core/libraries/libpng/pngenc_error.h
)
set(JPEG_LIB src/core/libraries/jpeg/jpeg_error.h
@ -581,14 +588,21 @@ set(NP_LIBS src/core/libraries/np/np_error.h
src/core/libraries/np/np_commerce.h
src/core/libraries/np/np_manager.cpp
src/core/libraries/np/np_manager.h
src/core/libraries/np/np_matching2.cpp
src/core/libraries/np/np_matching2.h
src/core/libraries/np/np_score.cpp
src/core/libraries/np/np_score.h
src/core/libraries/np/np_trophy.cpp
src/core/libraries/np/np_trophy.h
src/core/libraries/np/np_tus.cpp
src/core/libraries/np/np_tus.h
src/core/libraries/np/trophy_ui.cpp
src/core/libraries/np/trophy_ui.h
src/core/libraries/np/np_web_api.cpp
src/core/libraries/np/np_web_api.h
src/core/libraries/np/np_web_api_error.h
src/core/libraries/np/np_web_api_internal.cpp
src/core/libraries/np/np_web_api_internal.h
src/core/libraries/np/np_web_api2.cpp
src/core/libraries/np/np_web_api2.h
src/core/libraries/np/np_party.cpp
@ -599,6 +613,9 @@ set(NP_LIBS src/core/libraries/np/np_error.h
src/core/libraries/np/np_profile_dialog.h
src/core/libraries/np/np_sns_facebook_dialog.cpp
src/core/libraries/np/np_sns_facebook_dialog.h
src/core/libraries/np/np_partner.cpp
src/core/libraries/np/np_partner.h
src/core/libraries/np/object_manager.h
)
set(ZLIB_LIB src/core/libraries/zlib/zlib.cpp
@ -744,6 +761,8 @@ set(COMMON src/common/logging/backend.cpp
src/common/memory_patcher.cpp
${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp
src/common/scm_rev.h
src/common/key_manager.cpp
src/common/key_manager.h
)
if (ENABLE_DISCORD_RPC)
@ -787,6 +806,8 @@ set(CORE src/core/aerolib/stubs.cpp
src/core/file_format/playgo_chunk.h
src/core/file_format/trp.cpp
src/core/file_format/trp.h
src/core/file_format/npbind.cpp
src/core/file_format/npbind.h
src/core/file_sys/fs.cpp
src/core/file_sys/fs.h
src/core/ipc/ipc.cpp
@ -912,6 +933,7 @@ set(SHADER_RECOMPILER src/shader_recompiler/profile.h
src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp
src/shader_recompiler/ir/passes/hull_shader_transform.cpp
src/shader_recompiler/ir/passes/identity_removal_pass.cpp
src/shader_recompiler/ir/passes/inject_clip_distance_attributes.cpp
src/shader_recompiler/ir/passes/ir_passes.h
src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp
src/shader_recompiler/ir/passes/lower_fp64_to_fp32.cpp
@ -1093,7 +1115,7 @@ 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 fdk-aac)
target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz fdk-aac CLI11::CLI11)
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")

View File

@ -1,5 +1,5 @@
<!--
SPDX-FileCopyrightText: 2024 shadPS4 Emulator Project
SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
@ -58,6 +58,11 @@ This project began for fun. Given our limited free time, it may take some time b
# Building
## Docker
For building shadPS4 in a containerized environment using Docker and VSCode, check the instructions here:
[**Docker Build Instructions**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-docker.md)
## Windows
Check the build instructions for [**Windows**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-windows.md).
@ -150,7 +155,7 @@ The following firmware modules are supported and must be placed in shadPS4's `sy
| 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 | | | |
| libSceUlt.sprx | libSceAudiodec.sprx | | |
</div>
> [!Caution]

View File

@ -20,6 +20,7 @@ path = [
"documents/Quickstart/2.png",
"documents/Screenshots/*",
"documents/Screenshots/Linux/*",
"documents/Screenshots/Windows/*",
"externals/MoltenVK/MoltenVK_icd.json",
"scripts/ps4_names.txt",
"src/images/bronze.png",

View File

@ -38,6 +38,9 @@
<category translate="no">Game</category>
</categories>
<releases>
<release version="0.14.0" date="2026-02-07">
<url>https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.14.0</url>
</release>
<release version="0.13.0" date="2025-12-24">
<url>https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.13.0</url>
</release>

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
{
"name": "shadPS4-dev",
"dockerComposeFile": [
"../docker-compose.yml"
],
"containerEnv": {
"GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}",
"GITHUB_USER": "${localEnv:GITHUB_USER}"
},
"service": "shadps4",
"workspaceFolder": "/workspaces/shadPS4",
"remoteUser": "root",
"shutdownAction": "none",
"customizations": {
"vscode": {
"extensions": [
"llvm-vs-code-extensions.vscode-clangd",
"ms-vscode.cmake-tools",
"xaver.clang-format"
],
"settings": {
"clangd.arguments": [
"--background-index",
"--clang-tidy",
"--completion-style=detailed",
"--header-insertion=never",
"--compile-commands-dir=/workspaces/shadPS4/Build/x64-Clang-Release"
],
"C_Cpp.intelliSenseEngine": "Disabled"
}
}
},
"settings": {
"cmake.configureOnOpen": false,
"cmake.generator": "Unix Makefiles",
"cmake.environment": {
"CC": "clang",
"CXX": "clang++"
},
"cmake.configureEnvironment": {
"CMAKE_CXX_STANDARD": "23",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
},
"editor.formatOnSave": true,
"clang-format.executable": "clang-format-19"
}
}

View File

@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
FROM archlinux:latest
RUN pacman-key --init && \
pacman-key --populate archlinux && \
pacman -Syu --noconfirm
RUN pacman -S --noconfirm \
base-devel \
clang \
clang19 \
ninja \
git \
ca-certificates \
wget \
alsa-lib \
libpulse \
openal \
openssl \
zlib \
libedit \
systemd-libs \
libevdev \
sdl2 \
jack \
sndio \
libxtst \
vulkan-headers \
vulkan-validation-layers \
libpng \
clang-tools-extra \
cmake \
libx11 \
libxrandr \
libxcursor \
libxi \
libxinerama \
libxss \
&& pacman -Scc --noconfirm
RUN ln -sf /usr/lib/llvm19/bin/clang-format /usr/bin/clang-format-19
WORKDIR /workspaces/shadPS4

View File

@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
services:
shadps4:
build:
context: ./.docker
volumes:
- ./emu:/workspaces/shadPS4:cached
tty: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -0,0 +1,100 @@
<!--
SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Building shadPS4 with Docker and VSCode Support
This guide explains how to build **shadPS4** using Docker while keeping full compatibility with **VSCode** development.
---
## Prerequisites
Before starting, ensure you have:
- **Docker Engine** or **Docker Desktop** installed
[Installation Guide](https://docs.docker.com/engine/install/)
- **Git** installed on your system.
---
## Step 1: Prepare the Docker Environment
Inside the container (or on your host if mounting volumes):
1. Navigate to the repository folder containing the Docker Builder folder:
```bash
cd <path-to-repo>
```
2. Start the Docker container:
```bash
docker compose up -d
```
This will spin up a container with all the necessary build dependencies, including Clang, CMake, SDL2, Vulkan, and more.
## Step 2: Clone shadPS4 Source
```bash
mkdir emu
cd emu
git clone --recursive https://github.com/shadps4-emu/shadPS4.git .
or your fork link.
```
3. Initialize submodules:
```bash
git submodule update --init --recursive
```
## Step 3: Build with CMake Tools (GUI)
Generate build with CMake Tools.
1. Go `CMake Tools > Configure > '>'`
2. And `Build > '>'`
Compiled executable in `Build` folder.
## Alternative Step 3: Build with CMake
Generate the build directory and configure the project using Clang:
```bash
cmake -S . -B build/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
```
Then build the project:
```bash
cmake --build ./build --parallel $(nproc)
```
* Tip: To enable debug builds, add -DCMAKE_BUILD_TYPE=Debug to the CMake command.
---
After a successful build, the executable is located at:
```bash
./build/shadps4
```
## Step 4: VSCode Integration
1. Open the repository in VSCode.
2. The CMake Tools extension should automatically detect the build directory inside the container or on your host.
3. You can configure build options, build, and debug directly from the VSCode interface without extra manual setup.
# Notes
* The Docker environment contains all dependencies, so you dont need to install anything manually.
* Using Clang inside Docker ensures consistent builds across Linux and macOS runners.
* GitHub Actions are recommended for cross-platform builds, including Windows .exe output, which is not trivial to produce locally without Visual Studio or clang-cl.

View File

@ -41,10 +41,171 @@ Go through the Git for Windows installation as normal
Your shadps4.exe will be in `C:\path\to\source\Build\x64-Clang-Release\`
## Option 2: MSYS2/MinGW
## Option 2: VSCode with Visual Studio Build Tools
If your default IDE is VSCode, we have a fully functional example for that as well.
### Requirements
* [**Git for Windows**](https://git-scm.com/download/win)
* [**LLVM 19.1.1**](https://github.com/llvm/llvm-project/releases/download/llvmorg-19.1.1/LLVM-19.1.1-win64.exe)
* [**CMake 4.2.3 or newer**](https://github.com/Kitware/CMake/releases/download/v4.2.3/cmake-4.2.3-windows-x86_64.msi)
* [**Ninja 1.13.2 or newer**](https://github.com/ninja-build/ninja/releases/download/v1.13.2/ninja-win.zip)
**The main reason we use clang19 is because that version is used in CI for formatting.**
### Installs
1. Go through the Git for Windows installation as normal
2. Download and Run LLVM Installer and `Add LLVM to the system PATH for all users`
3. Download and Run CMake Installer and `Add CMake to the system PATH for all users`
4. Download Ninja and extract it to `C:\ninja` and add it to the system PATH for all users
* You can do this by going to `Search with Start Menu -> Environment Variables -> System Variables -> Path -> Edit -> New -> C:\ninja`
### Validate the installs
```bash
git --version
# git version 2.49.0.windows.1
cmake --version
# cmake version 4.2.3
ninja --version
# 1.13.2
clang --version
# clang version 19.1.1
```
### Install Visual Studio Build Tools
1. Download [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
2. Select `MSVC - Windows SDK` and install (you don't need to install an IDE)
* Or you can install via `.vsconfig` file:
```
{
"version": "1.0",
"components": [
"Microsoft.VisualStudio.Component.Roslyn.Compiler",
"Microsoft.Component.MSBuild",
"Microsoft.VisualStudio.Component.CoreBuildTools",
"Microsoft.VisualStudio.Workload.MSBuildTools",
"Microsoft.VisualStudio.Component.Windows10SDK",
"Microsoft.VisualStudio.Component.VC.CoreBuildTools",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"Microsoft.VisualStudio.Component.VC.Redist.14.Latest",
"Microsoft.VisualStudio.Component.Windows11SDK.26100",
"Microsoft.VisualStudio.Component.TestTools.BuildTools",
"Microsoft.VisualStudio.Component.VC.ASAN",
"Microsoft.VisualStudio.Component.TextTemplating",
"Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Core",
"Microsoft.VisualStudio.Workload.VCTools"
],
"extensions": []
}
Save the file as `.vsconfig` and run the following command:
%userprofile%\Downloads\vs_BuildTools.exe --passive --config ".vsconfig"
Be carefull path to vs_BuildTools.exe and .vsconfig file.
```
__This will install the necessary components to build shadPS4.__
### Project structure
```
shadps4/
├── shared (shadps4 main files)
└── shadps4.code-workspace
```
### Content of `shadps4.code-workspace`
```json
{
"folders": [
{
"path": "shared"
}
],
"settings": {
"cmake.generator": "Ninja",
"cmake.configureEnvironment": {
"CMAKE_CXX_STANDARD": "23",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
},
"cmake.configureOnOpen": false,
"C_Cpp.intelliSenseEngine": "Disabled",
"clangd.arguments": [
"--background-index",
"--clang-tidy",
"--completion-style=detailed",
"--header-insertion=never",
"--compile-commands-dir=Build/x64-Clang-Release"
],
"editor.formatOnSave": true,
"clang-format.executable": "clang-format"
},
"extensions": {
"recommendations": [
"llvm-vs-code-extensions.vscode-clangd",
"ms-vscode.cmake-tools",
"xaver.clang-format"
]
}
}
```
### Cloning the source code
1. Open your terminal and where to shadPS4 folder: `cd shadps4\shared`
3. Clone the repository by running
`git clone --depth 1 --recursive https://github.com/shadps4-emu/shadPS4 .`
_or fork link_
* If you have already cloned repo:
```bash
git submodule update --init --recursive
```
### Requirements VSCode extensions
1. CMake Tools
2. Clangd
3. Clang-Format
_These plugins are suggested in the workspace file above and are already configured._
![CMake Tools](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/windows/vscode-ext-1.png)
![Clangd](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/windows/vscode-ext-2.png)
![Clang Format](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/windows/vscode-ext-3.png)
### Building
1. Open VS Code, `File > Open workspace from file > shadps4.code-workspace`
2. Go to the CMake Tools extension on left side bar
3. Change Clang x64 Debug to Clang x64 Release if you want a regular, non-debug build.
4. Click build.
Your shadps4.exe will be in `shadps4\shared\Build\x64-Clang-Release\`
## Option 3: MSYS2/MinGW
> [!IMPORTANT]
> Building with MSYS2 is broken as of right now, the only way to build on Windows is to use [Option 1: Visual Studio 2022](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-windows.md#option-1-visual-studio-2022).
> Building with MSYS2 is broken as of right now, the only way to build on Windows is to use [Option 1: Visual Studio 2022](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-windows.md#option-1-visual-studio-2022) or [Option 2: VSCode with Visual Studio Build Tools](#option-2-vscode-with-visual-studio-build-tools).
### (Prerequisite) Download [**MSYS2**](https://www.msys2.org/)

1
externals/CLI11 vendored Submodule

@ -0,0 +1 @@
Subproject commit bf5a16a26a34a9a7ad75f4a7705585e44675fef0

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
set(BUILD_SHARED_LIBS OFF)
@ -204,6 +204,7 @@ add_subdirectory(tracy)
# pugixml
if (NOT TARGET pugixml::pugixml)
option(PUGIXML_NO_EXCEPTIONS "" ON)
add_subdirectory(pugixml)
endif()
@ -268,3 +269,10 @@ add_subdirectory(json)
# miniz
add_subdirectory(miniz)
# cli11
if (NOT TARGET CLI11::CLI11)
set(CLI11_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(CLI11)
endif()

2
externals/MoltenVK vendored

@ -1 +1 @@
Subproject commit f168dec05998ab0ca09a400bab6831a95c0bdb2e
Subproject commit f79c6c5690d3ee06ec3a00d11a8b1bab4aa1d030

View File

@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include <map>
#include <optional>
#include <string>
#include <fmt/core.h>
@ -14,6 +15,8 @@
#include "common/path_util.h"
#include "common/scm_rev.h"
#include "input/input_handler.h"
using std::nullopt;
using std::optional;
using std::string;
@ -361,7 +364,7 @@ u32 getWindowHeight() {
}
u32 getInternalScreenWidth() {
return internalScreenHeight.get();
return internalScreenWidth.get();
}
u32 getInternalScreenHeight() {
@ -1289,16 +1292,6 @@ void setDefaultValues(bool is_game_specific) {
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.
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_toggle_mouse_to_touchpad = delete
hotkey_quit = lctrl, lshift, end
)";
}
@ -1376,7 +1369,7 @@ analog_deadzone = rightjoystick, 2, 127
override_controller_color = false, 0, 0, 255
)";
}
std::filesystem::path GetFoolproofInputConfigFile(const string& game_id) {
std::filesystem::path GetInputConfigFile(const 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.
@ -1415,6 +1408,39 @@ std::filesystem::path GetFoolproofInputConfigFile(const string& game_id) {
}
}
}
if (game_id == "global") {
std::map<string, 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_toggle_mouse_to_touchpad", "delete"},
{"hotkey_quit", "lctrl, lshift, end"},
{"hotkey_volume_up", "kpplus"},
{"hotkey_volume_down", "kpminus"},
};
std::ifstream global_in(config_file);
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)) {

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -183,6 +183,6 @@ std::filesystem::path getAddonInstallDir();
void setDefaultValues(bool is_game_specific = false);
constexpr std::string_view GetDefaultGlobalConfig();
std::filesystem::path GetFoolproofInputConfigFile(const std::string& game_id = "");
std::filesystem::path GetInputConfigFile(const std::string& game_id = "");
}; // namespace Config

View File

@ -75,6 +75,7 @@ class ElfInfo {
std::filesystem::path game_folder{};
public:
static constexpr u32 FW_10 = 0x1000000;
static constexpr u32 FW_15 = 0x1500000;
static constexpr u32 FW_16 = 0x1600000;
static constexpr u32 FW_17 = 0x1700000;

161
src/common/key_manager.cpp Normal file
View File

@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include "common/logging/log.h"
#include "key_manager.h"
#include "path_util.h"
std::shared_ptr<KeyManager> KeyManager::s_instance = nullptr;
std::mutex KeyManager::s_mutex;
// ------------------- Constructor & Singleton -------------------
KeyManager::KeyManager() {
SetDefaultKeys();
}
KeyManager::~KeyManager() {
SaveToFile();
}
std::shared_ptr<KeyManager> KeyManager::GetInstance() {
std::lock_guard<std::mutex> lock(s_mutex);
if (!s_instance)
s_instance = std::make_shared<KeyManager>();
return s_instance;
}
void KeyManager::SetInstance(std::shared_ptr<KeyManager> instance) {
std::lock_guard<std::mutex> lock(s_mutex);
s_instance = instance;
}
// ------------------- Load / Save -------------------
bool KeyManager::LoadFromFile() {
try {
const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
const auto keysPath = userDir / "keys.json";
if (!std::filesystem::exists(keysPath)) {
SetDefaultKeys();
SaveToFile();
LOG_DEBUG(KeyManager, "Created default key file: {}", keysPath.string());
return true;
}
std::ifstream file(keysPath);
if (!file.is_open()) {
LOG_ERROR(KeyManager, "Could not open key file: {}", keysPath.string());
return false;
}
json j;
file >> j;
SetDefaultKeys(); // start from defaults
if (j.contains("TrophyKeySet"))
j.at("TrophyKeySet").get_to(m_keys.TrophyKeySet);
LOG_DEBUG(KeyManager, "Successfully loaded keys from: {}", keysPath.string());
return true;
} catch (const std::exception& e) {
LOG_ERROR(KeyManager, "Error loading keys, using defaults: {}", e.what());
SetDefaultKeys();
return false;
}
}
bool KeyManager::SaveToFile() {
try {
const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
const auto keysPath = userDir / "keys.json";
json j;
KeysToJson(j);
std::ofstream file(keysPath);
if (!file.is_open()) {
LOG_ERROR(KeyManager, "Could not open key file for writing: {}", keysPath.string());
return false;
}
file << std::setw(4) << j;
file.flush();
if (file.fail()) {
LOG_ERROR(KeyManager, "Failed to write keys to: {}", keysPath.string());
return false;
}
LOG_DEBUG(KeyManager, "Successfully saved keys to: {}", keysPath.string());
return true;
} catch (const std::exception& e) {
LOG_ERROR(KeyManager, "Error saving keys: {}", e.what());
return false;
}
}
// ------------------- JSON conversion -------------------
void KeyManager::KeysToJson(json& j) const {
j = m_keys;
}
void KeyManager::JsonToKeys(const json& j) {
json current = m_keys; // serialize current defaults
current.update(j); // merge only fields present in file
m_keys = current.get<AllKeys>(); // deserialize back
}
// ------------------- Defaults / Checks -------------------
void KeyManager::SetDefaultKeys() {
m_keys = AllKeys{};
}
bool KeyManager::HasKeys() const {
return !m_keys.TrophyKeySet.ReleaseTrophyKey.empty();
}
// ------------------- Hex conversion -------------------
std::vector<u8> KeyManager::HexStringToBytes(const std::string& hexStr) {
std::vector<u8> bytes;
if (hexStr.empty())
return bytes;
if (hexStr.size() % 2 != 0)
throw std::runtime_error("Invalid hex string length");
bytes.reserve(hexStr.size() / 2);
auto hexCharToInt = [](char c) -> u8 {
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
throw std::runtime_error("Invalid hex character");
};
for (size_t i = 0; i < hexStr.size(); i += 2) {
u8 high = hexCharToInt(hexStr[i]);
u8 low = hexCharToInt(hexStr[i + 1]);
bytes.push_back((high << 4) | low);
}
return bytes;
}
std::string KeyManager::BytesToHexString(const std::vector<u8>& bytes) {
static const char hexDigits[] = "0123456789ABCDEF";
std::string hexStr;
hexStr.reserve(bytes.size() * 2);
for (u8 b : bytes) {
hexStr.push_back(hexDigits[(b >> 4) & 0xF]);
hexStr.push_back(hexDigits[b & 0xF]);
}
return hexStr;
}

79
src/common/key_manager.h Normal file
View File

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include <filesystem>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "common/types.h"
#include "nlohmann/json.hpp"
using json = nlohmann::json;
class KeyManager {
public:
// ------------------- Nested keysets -------------------
struct TrophyKeySet {
std::vector<u8> ReleaseTrophyKey;
};
struct AllKeys {
KeyManager::TrophyKeySet TrophyKeySet;
};
// ------------------- Construction -------------------
KeyManager();
~KeyManager();
// ------------------- Singleton -------------------
static std::shared_ptr<KeyManager> GetInstance();
static void SetInstance(std::shared_ptr<KeyManager> instance);
// ------------------- File operations -------------------
bool LoadFromFile();
bool SaveToFile();
// ------------------- Key operations -------------------
void SetDefaultKeys();
bool HasKeys() const;
// ------------------- Getters / Setters -------------------
const AllKeys& GetAllKeys() const {
return m_keys;
}
void SetAllKeys(const AllKeys& keys) {
m_keys = keys;
}
static std::vector<u8> HexStringToBytes(const std::string& hexStr);
static std::string BytesToHexString(const std::vector<u8>& bytes);
private:
void KeysToJson(json& j) const;
void JsonToKeys(const json& j);
AllKeys m_keys{};
static std::shared_ptr<KeyManager> s_instance;
static std::mutex s_mutex;
};
// ------------------- NLOHMANN macros -------------------
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::TrophyKeySet, ReleaseTrophyKey)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::AllKeys, TrophyKeySet)
namespace nlohmann {
template <>
struct adl_serializer<std::vector<u8>> {
static void to_json(json& j, const std::vector<u8>& vec) {
j = KeyManager::BytesToHexString(vec);
}
static void from_json(const json& j, std::vector<u8>& vec) {
vec = KeyManager::HexStringToBytes(j.get<std::string>());
}
};
} // namespace nlohmann

View File

@ -4,6 +4,7 @@
#include <chrono>
#include <filesystem>
#include <mutex>
#include <thread>
#include <fmt/format.h>
@ -208,26 +209,41 @@ public:
}
}
std::unique_lock entry_loc(_mutex);
if (_last_entry.message == message) {
++_last_entry.counter;
return;
}
if (_last_entry.counter >= 2) {
_last_entry.message += " x" + std::to_string(_last_entry.counter);
}
if (_last_entry.counter >= 1) {
if (Config::getLogType() == "async") {
message_queue.EmplaceWait(_last_entry);
} else {
ForEachBackend([this](auto& backend) { backend.Write(this->_last_entry); });
std::fflush(stdout);
}
}
using std::chrono::duration_cast;
using std::chrono::microseconds;
using std::chrono::steady_clock;
const Entry entry = {
this->_last_entry = {
.timestamp = duration_cast<microseconds>(steady_clock::now() - time_origin),
.log_class = log_class,
.log_level = log_level,
.filename = filename,
.line_num = line_num,
.function = function,
.message = std::move(message),
.message = message,
.thread = Common::GetCurrentThreadName(),
.counter = 1,
};
if (Config::getLogType() == "async") {
message_queue.EmplaceWait(entry);
} else {
ForEachBackend([&entry](auto& backend) { backend.Write(entry); });
std::fflush(stdout);
}
}
private:
@ -259,6 +275,22 @@ private:
}
void StopBackendThread() {
// log last message
if (_last_entry.counter >= 2) {
_last_entry.message += " x" + std::to_string(_last_entry.counter);
}
if (_last_entry.counter >= 1) {
if (Config::getLogType() == "async") {
message_queue.EmplaceWait(_last_entry);
} else {
ForEachBackend([this](auto& backend) { backend.Write(this->_last_entry); });
std::fflush(stdout);
}
}
this->_last_entry = {};
backend_thread.request_stop();
if (backend_thread.joinable()) {
backend_thread.join();
@ -292,6 +324,8 @@ private:
MPSCQueue<Entry> message_queue{};
std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()};
std::jthread backend_thread;
Entry _last_entry;
std::mutex _mutex;
};
} // namespace

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -106,12 +107,15 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
SUB(Lib, NpCommon) \
SUB(Lib, NpCommerce) \
SUB(Lib, NpManager) \
SUB(Lib, NpMatching2) \
SUB(Lib, NpScore) \
SUB(Lib, NpTrophy) \
SUB(Lib, NpTus) \
SUB(Lib, NpWebApi) \
SUB(Lib, NpWebApi2) \
SUB(Lib, NpProfileDialog) \
SUB(Lib, NpSnsFacebookDialog) \
SUB(Lib, NpPartner) \
SUB(Lib, Screenshot) \
SUB(Lib, LibCInternal) \
SUB(Lib, AppContent) \
@ -159,6 +163,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
CLS(ImGui) \
CLS(Input) \
CLS(Tty) \
CLS(KeyManager) \
CLS(Loader)
// GetClassName is a macro defined by Windows.h, grrr...

View File

@ -22,6 +22,7 @@ struct Entry {
std::string function;
std::string message;
std::string thread;
u32 counter = 0;
};
} // namespace Common::Log

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2023 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -73,8 +74,10 @@ enum class Class : u8 {
Lib_NpCommerce, ///< The LibSceNpCommerce implementation
Lib_NpAuth, ///< The LibSceNpAuth implementation
Lib_NpManager, ///< The LibSceNpManager implementation
Lib_NpMatching2, ///< The LibSceNpMatching2 implementation
Lib_NpScore, ///< The LibSceNpScore implementation
Lib_NpTrophy, ///< The LibSceNpTrophy implementation
Lib_NpTus, ///< The LibSceNpTus implementation
Lib_NpWebApi, ///< The LibSceWebApi implementation
Lib_NpWebApi2, ///< The LibSceWebApi2 implementation
Lib_NpProfileDialog, ///< The LibSceNpProfileDialog implementation
@ -109,6 +112,7 @@ enum class Class : u8 {
Lib_Mouse, ///< The LibSceMouse implementation
Lib_WebBrowserDialog, ///< The LibSceWebBrowserDialog implementation
Lib_NpParty, ///< The LibSceNpParty implementation
Lib_NpPartner, ///< The LibSceNpPartner implementation
Lib_Zlib, ///< The LibSceZlib implementation.
Lib_Hmd, ///< The LibSceHmd implementation.
Lib_HmdSetupDialog, ///< The LibSceHmdSetupDialog implementation.
@ -127,6 +131,7 @@ enum class Class : u8 {
Loader, ///< ROM loader
Input, ///< Input emulation
Tty, ///< Debug output from emu
KeyManager, ///< Key management system
Count ///< Total number of logging classes
};

View File

@ -17,6 +17,15 @@ public:
writer_active = true;
}
bool try_lock() {
std::lock_guard<std::mutex> lock(mtx);
if (writer_active || readers > 0) {
return false;
}
writer_active = true;
return true;
}
void unlock() {
std::lock_guard<std::mutex> lock(mtx);
writer_active = false;

View File

@ -174,6 +174,9 @@ bool AccurateSleep(const std::chrono::nanoseconds duration, std::chrono::nanosec
// Sets the debugger-visible name of the current thread.
void SetCurrentThreadName(const char* name) {
if (Libraries::Kernel::g_curthread) {
Libraries::Kernel::g_curthread->name = name;
}
SetThreadDescription(GetCurrentThread(), UTF8ToUTF16W(name).data());
}
@ -186,6 +189,9 @@ void SetThreadName(void* thread, const char* name) {
// MinGW with the POSIX threading model does not support pthread_setname_np
#if !defined(_WIN32) || defined(_MSC_VER)
void SetCurrentThreadName(const char* name) {
if (Libraries::Kernel::g_curthread) {
Libraries::Kernel::g_curthread->name = name;
}
#ifdef __APPLE__
pthread_setname_np(name);
#elif defined(__Bitrig__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__OpenBSD__)
@ -212,6 +218,9 @@ void SetThreadName(void* thread, const char* name) {
#if defined(_WIN32)
void SetCurrentThreadName(const char*) {
if (Libraries::Kernel::g_curthread) {
Libraries::Kernel::g_curthread->name = name;
}
// Do Nothing on MinGW
}

View File

@ -93,7 +93,10 @@ static u64 BackingSize = ORBIS_KERNEL_TOTAL_MEM_DEV_PRO;
struct MemoryRegion {
VAddr base;
size_t size;
PAddr phys_base;
u64 size;
u32 prot;
s32 fd;
bool is_mapped;
};
@ -159,7 +162,8 @@ struct AddressSpace::Impl {
// Restrict region size to avoid overly fragmenting the virtual memory space.
if (info.State == MEM_FREE && info.RegionSize > 0x1000000) {
VAddr addr = Common::AlignUp(reinterpret_cast<VAddr>(info.BaseAddress), alignment);
regions.emplace(addr, MemoryRegion{addr, size, false});
regions.emplace(addr,
MemoryRegion{addr, PAddr(-1), size, PAGE_NOACCESS, -1, false});
}
}
@ -207,46 +211,52 @@ struct AddressSpace::Impl {
~Impl() {
if (virtual_base) {
if (!VirtualFree(virtual_base, 0, MEM_RELEASE)) {
LOG_CRITICAL(Render, "Failed to free virtual memory");
LOG_CRITICAL(Core, "Failed to free virtual memory");
}
}
if (backing_base) {
if (!UnmapViewOfFile2(process, backing_base, MEM_PRESERVE_PLACEHOLDER)) {
LOG_CRITICAL(Render, "Failed to unmap backing memory placeholder");
LOG_CRITICAL(Core, "Failed to unmap backing memory placeholder");
}
if (!VirtualFreeEx(process, backing_base, 0, MEM_RELEASE)) {
LOG_CRITICAL(Render, "Failed to free backing memory");
LOG_CRITICAL(Core, "Failed to free backing memory");
}
}
if (!CloseHandle(backing_handle)) {
LOG_CRITICAL(Render, "Failed to free backing memory file handle");
LOG_CRITICAL(Core, "Failed to free backing memory file handle");
}
}
void* Map(VAddr virtual_addr, PAddr phys_addr, size_t size, ULONG prot, uintptr_t fd = 0) {
// Before mapping we must carve a placeholder with the exact properties of our mapping.
auto* region = EnsureSplitRegionForMapping(virtual_addr, size);
region->is_mapped = true;
void* MapRegion(MemoryRegion* region) {
VAddr virtual_addr = region->base;
PAddr phys_addr = region->phys_base;
u64 size = region->size;
ULONG prot = region->prot;
s32 fd = region->fd;
void* ptr = nullptr;
if (phys_addr != -1) {
HANDLE backing = fd ? reinterpret_cast<HANDLE>(fd) : backing_handle;
if (fd && prot == PAGE_READONLY) {
HANDLE backing = fd != -1 ? reinterpret_cast<HANDLE>(fd) : backing_handle;
if (fd != -1 && prot == PAGE_READONLY) {
DWORD resultvar;
ptr = VirtualAlloc2(process, reinterpret_cast<PVOID>(virtual_addr), size,
MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER,
PAGE_READWRITE, nullptr, 0);
bool ret = ReadFile(backing, ptr, size, &resultvar, NULL);
// phys_addr serves as an offset for file mmaps.
// Create an OVERLAPPED with the offset, then supply that to ReadFile
OVERLAPPED param{};
// Offset is the least-significant 32 bits, OffsetHigh is the most-significant.
param.Offset = phys_addr & 0xffffffffull;
param.OffsetHigh = (phys_addr & 0xffffffff00000000ull) >> 32;
bool ret = ReadFile(backing, ptr, size, &resultvar, &param);
ASSERT_MSG(ret, "ReadFile failed. {}", Common::GetLastErrorMsg());
ret = VirtualProtect(ptr, size, prot, &resultvar);
ASSERT_MSG(ret, "VirtualProtect failed. {}", Common::GetLastErrorMsg());
} else {
ptr = MapViewOfFile3(backing, process, reinterpret_cast<PVOID>(virtual_addr),
phys_addr, size, MEM_REPLACE_PLACEHOLDER,
PAGE_EXECUTE_READWRITE, nullptr, 0);
phys_addr, size, MEM_REPLACE_PLACEHOLDER, prot, nullptr, 0);
ASSERT_MSG(ptr, "MapViewOfFile3 failed. {}", Common::GetLastErrorMsg());
DWORD resultvar;
bool ret = VirtualProtect(ptr, size, prot, &resultvar);
ASSERT_MSG(ret, "VirtualProtect failed. {}", Common::GetLastErrorMsg());
}
} else {
ptr =
@ -257,135 +267,220 @@ struct AddressSpace::Impl {
return ptr;
}
void Unmap(VAddr virtual_addr, size_t size, bool has_backing) {
bool ret;
if (has_backing) {
void UnmapRegion(MemoryRegion* region) {
VAddr virtual_addr = region->base;
PAddr phys_base = region->phys_base;
u64 size = region->size;
ULONG prot = region->prot;
s32 fd = region->fd;
bool ret = false;
if ((fd != -1 && prot != PAGE_READONLY) || (fd == -1 && phys_base != -1)) {
ret = UnmapViewOfFile2(process, reinterpret_cast<PVOID>(virtual_addr),
MEM_PRESERVE_PLACEHOLDER);
} else {
ret = VirtualFreeEx(process, reinterpret_cast<PVOID>(virtual_addr), size,
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER);
}
ASSERT_MSG(ret, "Unmap operation on virtual_addr={:#X} failed: {}", virtual_addr,
ASSERT_MSG(ret, "Unmap on virtual_addr {:#x}, size {:#x} failed: {}", virtual_addr, size,
Common::GetLastErrorMsg());
// The unmap call will create a new placeholder region. We need to see if we can coalesce it
// with neighbors.
JoinRegionsAfterUnmap(virtual_addr, size);
}
// The following code is inspired from Dolphin's MemArena
// https://github.com/dolphin-emu/dolphin/blob/deee3ee4/Source/Core/Common/MemArenaWin.cpp#L212
MemoryRegion* EnsureSplitRegionForMapping(VAddr address, size_t size) {
// Find closest region that is <= the given address by using upper bound and decrementing
auto it = regions.upper_bound(address);
ASSERT_MSG(it != regions.begin(), "Invalid address {:#x}", address);
--it;
ASSERT_MSG(!it->second.is_mapped,
"Attempt to map {:#x} with size {:#x} which overlaps with {:#x} mapping",
address, size, it->second.base);
auto& [base, region] = *it;
void SplitRegion(VAddr virtual_addr, u64 size) {
// First, get the region this range covers
auto it = std::prev(regions.upper_bound(virtual_addr));
const VAddr mapping_address = region.base;
const size_t region_size = region.size;
if (mapping_address == address) {
// If this region is already split up correctly we don't have to do anything
if (region_size == size) {
return &region;
// All unmapped areas will coalesce, so there should be a region
// containing the full requested range. If not, then something is mapped here.
ASSERT_MSG(it->second.base + it->second.size >= virtual_addr + size,
"Cannot fit region into one placeholder");
// If the region is mapped, we need to unmap first before we can modify the placeholders.
if (it->second.is_mapped) {
ASSERT_MSG(it->second.phys_base != -1 || !it->second.is_mapped,
"Cannot split unbacked mapping");
UnmapRegion(&it->second);
}
// We need to split this region to create a matching placeholder.
if (it->second.base != virtual_addr) {
// Requested address is not the start of the containing region,
// create a new region to represent the memory before the requested range.
auto& region = it->second;
u64 base_offset = virtual_addr - region.base;
u64 next_region_size = region.size - base_offset;
PAddr next_region_phys_base = -1;
if (region.is_mapped) {
next_region_phys_base = region.phys_base + base_offset;
}
region.size = base_offset;
ASSERT_MSG(region_size >= size,
"Region with address {:#x} and size {:#x} can't fit {:#x}", mapping_address,
region_size, size);
// Split the placeholder.
if (!VirtualFreeEx(process, LPVOID(address), size,
// Use VirtualFreeEx to create the split.
if (!VirtualFreeEx(process, LPVOID(region.base), region.size,
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) {
UNREACHABLE_MSG("Region splitting failed: {}", Common::GetLastErrorMsg());
return nullptr;
}
// Update tracked mappings and return the first of the two
// If the mapping was mapped, remap the region.
if (region.is_mapped) {
MapRegion(&region);
}
// Store a new region matching the removed area.
it = regions.emplace_hint(std::next(it), virtual_addr,
MemoryRegion(virtual_addr, next_region_phys_base,
next_region_size, region.prot, region.fd,
region.is_mapped));
}
// At this point, the region's base will match virtual_addr.
// Now check for a size difference.
if (it->second.size != size) {
// The requested size is smaller than the current region placeholder.
// Update region to match the requested region,
// then make a new region to represent the remaining space.
auto& region = it->second;
VAddr next_region_addr = region.base + size;
u64 next_region_size = region.size - size;
PAddr next_region_phys_base = -1;
if (region.is_mapped) {
next_region_phys_base = region.phys_base + size;
}
region.size = size;
const VAddr new_mapping_start = address + size;
regions.emplace_hint(std::next(it), new_mapping_start,
MemoryRegion(new_mapping_start, region_size - size, false));
return &region;
// Store the new region matching the remaining space
regions.emplace_hint(std::next(it), next_region_addr,
MemoryRegion(next_region_addr, next_region_phys_base,
next_region_size, region.prot, region.fd,
region.is_mapped));
// Use VirtualFreeEx to create the split.
if (!VirtualFreeEx(process, LPVOID(region.base), region.size,
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) {
UNREACHABLE_MSG("Region splitting failed: {}", Common::GetLastErrorMsg());
}
// If these regions were mapped, then map the unmapped area beyond the requested range.
if (region.is_mapped) {
MapRegion(&std::next(it)->second);
}
}
ASSERT(mapping_address < address);
// Is there enough space to map this?
const size_t offset_in_region = address - mapping_address;
const size_t minimum_size = size + offset_in_region;
ASSERT(region_size >= minimum_size);
// Split the placeholder.
if (!VirtualFreeEx(process, LPVOID(address), size,
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) {
UNREACHABLE_MSG("Region splitting failed: {}", Common::GetLastErrorMsg());
return nullptr;
}
// Do we now have two regions or three regions?
if (region_size == minimum_size) {
// Split into two; update tracked mappings and return the second one
region.size = offset_in_region;
it = regions.emplace_hint(std::next(it), address, MemoryRegion(address, size, false));
return &it->second;
} else {
// Split into three; update tracked mappings and return the middle one
region.size = offset_in_region;
const VAddr middle_mapping_start = address;
const size_t middle_mapping_size = size;
const VAddr after_mapping_start = address + size;
const size_t after_mapping_size = region_size - minimum_size;
it = regions.emplace_hint(std::next(it), after_mapping_start,
MemoryRegion(after_mapping_start, after_mapping_size, false));
it = regions.emplace_hint(
it, middle_mapping_start,
MemoryRegion(middle_mapping_start, middle_mapping_size, false));
return &it->second;
// If the requested region was mapped, remap it.
if (it->second.is_mapped) {
MapRegion(&it->second);
}
}
void JoinRegionsAfterUnmap(VAddr address, size_t size) {
// There should be a mapping that matches the request exactly, find it
auto it = regions.find(address);
ASSERT_MSG(it != regions.end() && it->second.size == size,
"Invalid address/size given to unmap.");
void* Map(VAddr virtual_addr, PAddr phys_addr, u64 size, ULONG prot, s32 fd = -1) {
// Get a pointer to the region containing virtual_addr
auto it = std::prev(regions.upper_bound(virtual_addr));
// If needed, split surrounding regions to create a placeholder
if (it->first != virtual_addr || it->second.size != size) {
SplitRegion(virtual_addr, size);
it = std::prev(regions.upper_bound(virtual_addr));
}
// Get the address and region for this range.
auto& [base, region] = *it;
region.is_mapped = false;
ASSERT_MSG(!region.is_mapped, "Cannot overwrite mapped region");
// Check if a placeholder exists right before us.
// Now we have a region matching the requested region, perform the actual mapping.
region.is_mapped = true;
region.phys_base = phys_addr;
region.prot = prot;
region.fd = fd;
return MapRegion(&region);
}
void CoalesceFreeRegions(VAddr virtual_addr) {
// First, get the region to update
auto it = std::prev(regions.upper_bound(virtual_addr));
ASSERT_MSG(!it->second.is_mapped, "Cannot coalesce mapped regions");
// Check if there are adjacent free placeholders before this area.
bool can_coalesce = false;
auto it_prev = it != regions.begin() ? std::prev(it) : regions.end();
if (it_prev != regions.end() && !it_prev->second.is_mapped) {
const size_t total_size = it_prev->second.size + size;
if (!VirtualFreeEx(process, LPVOID(it_prev->first), total_size,
MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) {
UNREACHABLE_MSG("Region coalescing failed: {}", Common::GetLastErrorMsg());
}
it_prev->second.size = total_size;
while (it_prev != regions.end() && !it_prev->second.is_mapped &&
it_prev->first + it_prev->second.size == it->first) {
// If there is an earlier region, move our iterator to that and increase size.
it_prev->second.size = it_prev->second.size + it->second.size;
regions.erase(it);
it = it_prev;
// Mark this region as coalesce-able.
can_coalesce = true;
// Get the next previous region.
it_prev = it != regions.begin() ? std::prev(it) : regions.end();
}
// Check if a placeholder exists right after us.
// Check if there are adjacent free placeholders after this area.
auto it_next = std::next(it);
if (it_next != regions.end() && !it_next->second.is_mapped) {
const size_t total_size = it->second.size + it_next->second.size;
if (!VirtualFreeEx(process, LPVOID(it->first), total_size,
while (it_next != regions.end() && !it_next->second.is_mapped &&
it->first + it->second.size == it_next->first) {
// If there is a later region, increase our current region's size
it->second.size = it->second.size + it_next->second.size;
regions.erase(it_next);
// Mark this region as coalesce-able.
can_coalesce = true;
// Get the next region
it_next = std::next(it);
}
// If there are placeholders to coalesce, then coalesce them.
if (can_coalesce) {
if (!VirtualFreeEx(process, LPVOID(it->first), it->second.size,
MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) {
UNREACHABLE_MSG("Region coalescing failed: {}", Common::GetLastErrorMsg());
}
it->second.size = total_size;
regions.erase(it_next);
}
}
void Protect(VAddr virtual_addr, size_t size, bool read, bool write, bool execute) {
void Unmap(VAddr virtual_addr, u64 size) {
// Loop through all regions in the requested range
u64 remaining_size = size;
VAddr current_addr = virtual_addr;
while (remaining_size > 0) {
// Get a pointer to the region containing virtual_addr
auto it = std::prev(regions.upper_bound(current_addr));
// If necessary, split regions to ensure a valid unmap.
// To prevent complication, ensure size is within the bounds of the current region.
u64 base_offset = current_addr - it->second.base;
u64 size_to_unmap = std::min<u64>(it->second.size - base_offset, remaining_size);
if (current_addr != it->second.base || size_to_unmap != it->second.size) {
SplitRegion(current_addr, size_to_unmap);
it = std::prev(regions.upper_bound(current_addr));
}
// Get the address and region corresponding to this range.
auto& [base, region] = *it;
// Unmap the region if it was previously mapped
if (region.is_mapped) {
UnmapRegion(&region);
}
// Update region data
region.is_mapped = false;
region.fd = -1;
region.phys_base = -1;
region.prot = PAGE_NOACCESS;
// Update loop variables
remaining_size -= size_to_unmap;
current_addr += size_to_unmap;
}
// Coalesce any free space produced from these unmaps.
CoalesceFreeRegions(virtual_addr);
}
void Protect(VAddr virtual_addr, u64 size, bool read, bool write, bool execute) {
DWORD new_flags{};
if (write && !read) {
@ -415,7 +510,7 @@ struct AddressSpace::Impl {
// If no flags are assigned, then something's gone wrong.
if (new_flags == 0) {
LOG_CRITICAL(Common_Memory,
LOG_CRITICAL(Core,
"Unsupported protection flag combination for address {:#x}, size {}, "
"read={}, write={}, execute={}",
virtual_addr, size, read, write, execute);
@ -424,13 +519,14 @@ struct AddressSpace::Impl {
const VAddr virtual_end = virtual_addr + size;
auto it = --regions.upper_bound(virtual_addr);
ASSERT_MSG(it != regions.end(), "addr {:#x} out of bounds", virtual_addr);
for (; it->first < virtual_end; it++) {
if (!it->second.is_mapped) {
continue;
}
const auto& region = it->second;
const size_t range_addr = std::max(region.base, virtual_addr);
const size_t range_size = std::min(region.base + region.size, virtual_end) - range_addr;
const u64 range_addr = std::max(region.base, virtual_addr);
const u64 range_size = std::min(region.base + region.size, virtual_end) - range_addr;
DWORD old_flags{};
if (!VirtualProtectEx(process, LPVOID(range_addr), range_size, new_flags, &old_flags)) {
UNREACHABLE_MSG(
@ -453,11 +549,11 @@ struct AddressSpace::Impl {
u8* backing_base{};
u8* virtual_base{};
u8* system_managed_base{};
size_t system_managed_size{};
u64 system_managed_size{};
u8* system_reserved_base{};
size_t system_reserved_size{};
u64 system_reserved_size{};
u8* user_base{};
size_t user_size{};
u64 user_size{};
std::map<VAddr, MemoryRegion> regions;
};
#else
@ -601,7 +697,7 @@ struct AddressSpace::Impl {
}
}
void* Map(VAddr virtual_addr, PAddr phys_addr, size_t size, PosixPageProtection prot,
void* Map(VAddr virtual_addr, PAddr phys_addr, u64 size, PosixPageProtection prot,
int fd = -1) {
m_free_regions.subtract({virtual_addr, virtual_addr + size});
const int handle = phys_addr != -1 ? (fd == -1 ? backing_fd : fd) : -1;
@ -613,10 +709,10 @@ struct AddressSpace::Impl {
return ret;
}
void Unmap(VAddr virtual_addr, size_t size, bool) {
void Unmap(VAddr virtual_addr, u64 size) {
// Check to see if we are adjacent to any regions.
auto start_address = virtual_addr;
auto end_address = start_address + size;
VAddr start_address = virtual_addr;
VAddr end_address = start_address + size;
auto it = m_free_regions.find({start_address - 1, end_address + 1});
// If we are, join with them, ensuring we stay in bounds.
@ -634,7 +730,7 @@ struct AddressSpace::Impl {
ASSERT_MSG(ret != MAP_FAILED, "mmap failed: {}", strerror(errno));
}
void Protect(VAddr virtual_addr, size_t size, bool read, bool write, bool execute) {
void Protect(VAddr virtual_addr, u64 size, bool read, bool write, bool execute) {
int flags = PROT_NONE;
if (read) {
flags |= PROT_READ;
@ -654,11 +750,11 @@ struct AddressSpace::Impl {
int backing_fd;
u8* backing_base{};
u8* system_managed_base{};
size_t system_managed_size{};
u64 system_managed_size{};
u8* system_reserved_base{};
size_t system_reserved_size{};
u64 system_reserved_size{};
u8* user_base{};
size_t user_size{};
u64 user_size{};
boost::icl::interval_set<VAddr> m_free_regions;
};
#endif
@ -675,8 +771,7 @@ AddressSpace::AddressSpace() : impl{std::make_unique<Impl>()} {
AddressSpace::~AddressSpace() = default;
void* AddressSpace::Map(VAddr virtual_addr, size_t size, u64 alignment, PAddr phys_addr,
bool is_exec) {
void* AddressSpace::Map(VAddr virtual_addr, u64 size, PAddr phys_addr, bool is_exec) {
#if ARCH_X86_64
const auto prot = is_exec ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
#else
@ -687,8 +782,7 @@ void* AddressSpace::Map(VAddr virtual_addr, size_t size, u64 alignment, PAddr ph
return impl->Map(virtual_addr, phys_addr, size, prot);
}
void* AddressSpace::MapFile(VAddr virtual_addr, size_t size, size_t offset, u32 prot,
uintptr_t fd) {
void* AddressSpace::MapFile(VAddr virtual_addr, u64 size, u64 offset, u32 prot, uintptr_t fd) {
#ifdef _WIN32
return impl->Map(virtual_addr, offset, size,
ToWindowsProt(std::bit_cast<Core::MemoryProt>(prot)), fd);
@ -698,31 +792,11 @@ void* AddressSpace::MapFile(VAddr virtual_addr, size_t size, size_t offset, u32
#endif
}
void AddressSpace::Unmap(VAddr virtual_addr, size_t size, VAddr start_in_vma, VAddr end_in_vma,
PAddr phys_base, bool is_exec, bool has_backing, bool readonly_file) {
#ifdef _WIN32
// There does not appear to be comparable support for partial unmapping on Windows.
// Unfortunately, a least one title was found to require this. The workaround is to unmap
// the entire allocation and remap the portions outside of the requested unmapping range.
impl->Unmap(virtual_addr, size, has_backing && !readonly_file);
// TODO: Determine if any titles require partial unmapping support for un-backed allocations.
ASSERT_MSG(has_backing || (start_in_vma == 0 && end_in_vma == size),
"Partial unmapping of un-backed allocations is not supported");
if (start_in_vma != 0) {
Map(virtual_addr, start_in_vma, 0, phys_base, is_exec);
}
if (end_in_vma != size) {
Map(virtual_addr + end_in_vma, size - end_in_vma, 0, phys_base + end_in_vma, is_exec);
}
#else
impl->Unmap(virtual_addr + start_in_vma, end_in_vma - start_in_vma, has_backing);
#endif
void AddressSpace::Unmap(VAddr virtual_addr, u64 size) {
impl->Unmap(virtual_addr, size);
}
void AddressSpace::Protect(VAddr virtual_addr, size_t size, MemoryPermission perms) {
void AddressSpace::Protect(VAddr virtual_addr, u64 size, MemoryPermission perms) {
const bool read = True(perms & MemoryPermission::Read);
const bool write = True(perms & MemoryPermission::Write);
const bool execute = True(perms & MemoryPermission::Execute);

View File

@ -39,7 +39,7 @@ public:
[[nodiscard]] const u8* SystemManagedVirtualBase() const noexcept {
return system_managed_base;
}
[[nodiscard]] size_t SystemManagedVirtualSize() const noexcept {
[[nodiscard]] u64 SystemManagedVirtualSize() const noexcept {
return system_managed_size;
}
@ -49,7 +49,7 @@ public:
[[nodiscard]] const u8* SystemReservedVirtualBase() const noexcept {
return system_reserved_base;
}
[[nodiscard]] size_t SystemReservedVirtualSize() const noexcept {
[[nodiscard]] u64 SystemReservedVirtualSize() const noexcept {
return system_reserved_size;
}
@ -59,7 +59,7 @@ public:
[[nodiscard]] const u8* UserVirtualBase() const noexcept {
return user_base;
}
[[nodiscard]] size_t UserVirtualSize() const noexcept {
[[nodiscard]] u64 UserVirtualSize() const noexcept {
return user_size;
}
@ -73,17 +73,16 @@ public:
* If zero is provided the mapping is considered as private.
* @return A pointer to the mapped memory.
*/
void* Map(VAddr virtual_addr, size_t size, u64 alignment = 0, PAddr phys_addr = -1,
bool exec = false);
void* Map(VAddr virtual_addr, u64 size, PAddr phys_addr = -1, bool exec = false);
/// Memory maps a specified file descriptor.
void* MapFile(VAddr virtual_addr, size_t size, size_t offset, u32 prot, uintptr_t fd);
void* MapFile(VAddr virtual_addr, u64 size, u64 offset, u32 prot, uintptr_t fd);
/// Unmaps specified virtual memory area.
void Unmap(VAddr virtual_addr, size_t size, VAddr start_in_vma, VAddr end_in_vma,
PAddr phys_base, bool is_exec, bool has_backing, bool readonly_file);
void Unmap(VAddr virtual_addr, u64 size);
void Protect(VAddr virtual_addr, size_t size, MemoryPermission perms);
/// Protects requested region.
void Protect(VAddr virtual_addr, u64 size, MemoryPermission perms);
// Returns an interval set containing all usable regions.
boost::icl::interval_set<VAddr> GetUsableRegions();
@ -93,11 +92,11 @@ private:
std::unique_ptr<Impl> impl;
u8* backing_base{};
u8* system_managed_base{};
size_t system_managed_size{};
u64 system_managed_size{};
u8* system_reserved_base{};
size_t system_reserved_size{};
u64 system_reserved_size{};
u8* user_base{};
size_t user_size{};
u64 user_size{};
};
} // namespace Core

View File

@ -32,6 +32,9 @@ static bool show_simple_fps = false;
static bool visibility_toggled = false;
static bool show_quit_window = false;
static bool show_volume = false;
static float volume_start_time;
static float fps_scale = 1.0f;
static int dump_frame_count = 1;
@ -454,6 +457,27 @@ void L::Draw() {
End();
}
if (show_volume) {
float current_time = ImGui::GetTime();
// Show volume for 3 seconds
if (current_time - volume_start_time >= 3.0) {
show_volume = false;
} else {
SetNextWindowPos(ImVec2(ImGui::GetMainViewport()->WorkPos.x +
ImGui::GetMainViewport()->WorkSize.x - 10,
ImGui::GetMainViewport()->WorkPos.y + 10),
ImGuiCond_Always, ImVec2(1.0f, 0.0f));
if (ImGui::Begin("Volume Window", &show_volume,
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) {
Text("Volume: %d", Config::getVolumeSlider());
}
End();
}
}
PopID();
}
@ -482,4 +506,9 @@ void ToggleQuitWindow() {
show_quit_window = !show_quit_window;
}
void ShowVolume() {
volume_start_time = ImGui::GetTime();
show_volume = true;
}
} // namespace Overlay

View File

@ -32,5 +32,6 @@ namespace Overlay {
void ToggleSimpleFps();
void SetSimpleFps(bool enabled);
void ToggleQuitWindow();
void ShowVolume();
} // namespace Overlay

View File

@ -32,7 +32,7 @@ bool MemoryMapViewer::Iterator::DrawLine() {
TableNextColumn();
Text("%s", magic_enum::enum_name(m.prot).data());
TableNextColumn();
if (m.is_exec) {
if (True(m.prot & MemoryProt::CpuExec)) {
Text("X");
}
TableNextColumn();
@ -44,7 +44,7 @@ bool MemoryMapViewer::Iterator::DrawLine() {
return false;
}
auto m = dmem.it->second;
if (m.dma_type == DMAType::Free) {
if (m.dma_type == PhysicalMemoryType::Free) {
++dmem.it;
return DrawLine();
}
@ -56,7 +56,8 @@ bool MemoryMapViewer::Iterator::DrawLine() {
auto type = static_cast<::Libraries::Kernel::MemoryTypes>(m.memory_type);
Text("%s", magic_enum::enum_name(type).data());
TableNextColumn();
Text("%d", m.dma_type == DMAType::Pooled || m.dma_type == DMAType::Committed);
Text("%d",
m.dma_type == PhysicalMemoryType::Pooled || m.dma_type == PhysicalMemoryType::Committed);
++dmem.it;
return true;
}

View File

@ -11,8 +11,8 @@ class MemoryMapViewer {
struct Iterator {
bool is_vma;
struct {
MemoryManager::DMemMap::iterator it;
MemoryManager::DMemMap::iterator end;
MemoryManager::PhysMap::iterator it;
MemoryManager::PhysMap::iterator end;
} dmem;
struct {
MemoryManager::VMAMap::iterator it;

View File

@ -35,3 +35,11 @@ bool EmulatorState::IsAutoPatchesLoadEnabled() const {
void EmulatorState::SetAutoPatchesLoadEnabled(bool enable) {
m_load_patches_auto = enable;
}
bool EmulatorState::IsGameSpecifigConfigUsed() const {
return m_game_specific_config_used;
}
void EmulatorState::SetGameSpecifigConfigUsed(bool used) {
m_game_specific_config_used = used;
}

View File

@ -18,6 +18,8 @@ public:
void SetGameRunning(bool running);
bool IsAutoPatchesLoadEnabled() const;
void SetAutoPatchesLoadEnabled(bool enable);
bool IsGameSpecifigConfigUsed() const;
void SetGameSpecifigConfigUsed(bool used);
private:
static std::shared_ptr<EmulatorState> s_instance;
@ -26,4 +28,5 @@ private:
// state variables
bool m_running = false;
bool m_load_patches_auto = true;
};
bool m_game_specific_config_used = false;
};

View File

@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <vector>
#include "npbind.h"
bool NPBindFile::Load(const std::string& path) {
Clear(); // Clear any existing data
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f)
return false;
std::streamsize sz = f.tellg();
if (sz <= 0)
return false;
f.seekg(0, std::ios::beg);
std::vector<u8> buf(static_cast<size_t>(sz));
if (!f.read(reinterpret_cast<char*>(buf.data()), sz))
return false;
const u64 size = buf.size();
if (size < sizeof(NpBindHeader))
return false;
// Read header
memcpy(&m_header, buf.data(), sizeof(NpBindHeader));
if (m_header.magic != NPBIND_MAGIC)
return false;
// offset start of bodies
size_t offset = sizeof(NpBindHeader);
m_bodies.reserve(static_cast<size_t>(m_header.num_entries));
// For each body: read 4 TLV entries then skip padding (0x98 = 152 bytes)
const u64 body_padding = 0x98; // 152
for (u64 bi = 0; bi < m_header.num_entries; ++bi) {
// Ensure we have room for 4 entries' headers at least
if (offset + 4 * 4 > size)
return false; // 4 entries x (type+size)
NPBindBody body;
// helper lambda to read one entry
auto read_entry = [&](NPBindEntryRaw& e) -> bool {
if (offset + 4 > size)
return false;
memcpy(&e.type, &buf[offset], 2);
memcpy(&e.size, &buf[offset + 2], 2);
offset += 4;
if (offset + e.size > size)
return false;
e.data.assign(buf.begin() + offset, buf.begin() + offset + e.size);
offset += e.size;
return true;
};
// read 4 entries in order
if (!read_entry(body.npcommid))
return false;
if (!read_entry(body.trophy))
return false;
if (!read_entry(body.unk1))
return false;
if (!read_entry(body.unk2))
return false;
// skip fixed padding after body if present (but don't overrun)
if (offset + body_padding <= size) {
offset += body_padding;
} else {
// If padding not fully present, allow file to end (some variants may omit)
offset = size;
}
m_bodies.push_back(std::move(body));
}
// Read digest if available
if (size >= 20) {
// Digest is typically the last 20 bytes, independent of offset
memcpy(m_digest, &buf[size - 20], 20);
} else {
memset(m_digest, 0, 20);
}
return true;
}
std::vector<std::string> NPBindFile::GetNpCommIds() const {
std::vector<std::string> npcommids;
npcommids.reserve(m_bodies.size());
for (const auto& body : m_bodies) {
// Convert binary data to string directly
if (!body.npcommid.data.empty()) {
std::string raw_string(reinterpret_cast<const char*>(body.npcommid.data.data()),
body.npcommid.data.size());
npcommids.push_back(raw_string);
}
}
return npcommids;
}

View File

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "common/endian.h"
#include "common/types.h"
#define NPBIND_MAGIC 0xD294A018u
#pragma pack(push, 1)
struct NpBindHeader {
u32_be magic;
u32_be version;
u64_be file_size;
u64_be entry_size;
u64_be num_entries;
char padding[0x60]; // 96 bytes
};
#pragma pack(pop)
struct NPBindEntryRaw {
u16_be type;
u16_be size; // includes internal padding
std::vector<u8> data;
};
struct NPBindBody {
NPBindEntryRaw npcommid; // expected type 0x0010, size 12
NPBindEntryRaw trophy; // expected type 0x0011, size 12
NPBindEntryRaw unk1; // expected type 0x0012, size 176
NPBindEntryRaw unk2; // expected type 0x0013, size 16
// The 0x98 padding after these entries is skipped while parsing
};
class NPBindFile {
private:
NpBindHeader m_header;
std::vector<NPBindBody> m_bodies;
u8 m_digest[20]; // zeroed if absent
public:
NPBindFile() {
memset(m_digest, 0, sizeof(m_digest));
}
// Load from file
bool Load(const std::string& path);
// Accessors
const NpBindHeader& Header() const {
return m_header;
}
const std::vector<NPBindBody>& Bodies() const {
return m_bodies;
}
const u8* Digest() const {
return m_digest;
}
// Get npcommid data
std::vector<std::string> GetNpCommIds() const;
// Get specific body
const NPBindBody& GetBody(size_t index) const {
return m_bodies.at(index);
}
// Get number of bodies
u64 BodyCount() const {
return m_bodies.size();
}
// Check if file was loaded successfully
bool IsValid() const {
return m_header.magic == NPBIND_MAGIC;
}
// Clear all data
void Clear() {
m_header = NpBindHeader{};
m_bodies.clear();
memset(m_digest, 0, sizeof(m_digest));
}
};

View File

@ -1,44 +1,31 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/aes.h"
#include "common/config.h"
#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<u8, 16> trophyKey, std::span<u8, 16> NPcommID,
std::span<u8, 16> efsmIv, std::span<u8> ciphertext,
static void DecryptEFSM(std::span<const u8, 16> trophyKey, std::span<const u8, 16> NPcommID,
std::span<const u8, 16> efsmIv, std::span<const u8> ciphertext,
std::span<u8> decrypted) {
// Step 1: Encrypt NPcommID
std::array<u8, 16> trophyIv{};
std::array<u8, 16> trpKey;
// Convert spans to pointers for the aes functions
aes::encrypt_cbc(NPcommID.data(), NPcommID.size(), trophyKey.data(), trophyKey.size(),
trophyIv.data(), trpKey.data(), trpKey.size(), false);
// Step 2: Decrypt EFSM
aes::decrypt_cbc(ciphertext.data(), ciphertext.size(), trpKey.data(), trpKey.size(),
efsmIv.data(), decrypted.data(), decrypted.size(), nullptr);
const_cast<u8*>(efsmIv.data()), decrypted.data(), decrypted.size(), nullptr);
}
TRP::TRP() = default;
TRP::~TRP() = default;
void TRP::GetNPcommID(const std::filesystem::path& trophyPath, int index) {
std::filesystem::path trpPath = trophyPath / "sce_sys/npbind.dat";
Common::FS::IOFile npbindFile(trpPath, Common::FS::FileAccessMode::Read);
if (!npbindFile.IsOpen()) {
LOG_CRITICAL(Common_Filesystem, "Failed to open npbind.dat file");
return;
}
if (!npbindFile.Seek(0x84 + (index * 0x180))) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to NPbind offset");
return;
}
npbindFile.ReadRaw<u8>(np_comm_id.data(), 12);
std::fill(np_comm_id.begin() + 12, np_comm_id.end(), 0); // fill with 0, we need 16 bytes.
}
static void removePadding(std::vector<u8>& vec) {
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
if (*it == '>') {
@ -63,91 +50,232 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit
return false;
}
const auto user_key_str = Config::getTrophyKey();
if (user_key_str.size() != 32) {
const auto& user_key_vec =
KeyManager::GetInstance()->GetAllKeys().TrophyKeySet.ReleaseTrophyKey;
if (user_key_vec.size() != 16) {
LOG_INFO(Common_Filesystem, "Trophy decryption key is not specified");
return false;
}
std::array<u8, 16> user_key{};
hexToBytes(user_key_str.c_str(), user_key.data());
std::copy(user_key_vec.begin(), user_key_vec.end(), user_key.begin());
for (int index = 0; const auto& it : std::filesystem::directory_iterator(gameSysDir)) {
if (it.is_regular_file()) {
GetNPcommID(trophyPath, index);
// 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
}
// 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);
}
Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read);
if (!file.IsOpen()) {
LOG_CRITICAL(Common_Filesystem, "Unable to open trophy file for read");
return false;
LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.path().string());
success = false;
continue;
}
TrpHeader header;
file.Read(header);
if (header.magic != 0xDCA24D00) {
LOG_CRITICAL(Common_Filesystem, "Wrong trophy magic number");
return false;
if (!file.Read(header)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}",
it.path().string());
success = false;
continue;
}
if (header.magic != TRP_MAGIC) {
LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.path().string());
success = false;
continue;
}
s64 seekPos = sizeof(TrpHeader);
std::filesystem::path trpFilesPath(
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId /
"TrophyFiles" / it.path().stem());
std::filesystem::create_directories(trpFilesPath / "Icons");
std::filesystem::create_directory(trpFilesPath / "Xml");
// 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_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset");
return false;
LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset");
success = false;
break;
}
seekPos += (s64)header.entry_size;
seekPos += static_cast<s64>(header.entry_size);
TrpEntry entry;
file.Read(entry);
std::string_view name(entry.entry_name);
if (entry.flag == 0) { // PNG
if (!file.Seek(entry.entry_pos)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset");
return false;
}
std::vector<u8> icon(entry.entry_len);
file.Read(icon);
Common::FS::IOFile::WriteBytes(trpFilesPath / "Icons" / name, icon);
if (!file.Read(entry)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP entry");
success = false;
break;
}
if (entry.flag == 3 && np_comm_id[0] == 'N' &&
np_comm_id[1] == 'P') { // ESFM, encrypted.
if (!file.Seek(entry.entry_pos)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset");
return false;
std::string_view name(entry.entry_name);
if (entry.flag == ENTRY_FLAG_PNG) {
if (!ProcessPngEntry(file, entry, trpFilesPath, name)) {
success = false;
// Continue with next entry
}
file.Read(esfmIv); // get iv key.
// Skip the first 16 bytes which are the iv key on every entry as we want a
// clean xml file.
std::vector<u8> ESFM(entry.entry_len - iv_len);
std::vector<u8> XML(entry.entry_len - iv_len);
if (!file.Seek(entry.entry_pos + iv_len)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry + iv offset");
return false;
}
file.Read(ESFM);
DecryptEFSM(user_key, np_comm_id, esfmIv, ESFM, XML); // decrypt
removePadding(XML);
std::string xml_name = entry.entry_name;
size_t pos = xml_name.find("ESFM");
if (pos != std::string::npos)
xml_name.replace(pos, xml_name.length(), "XML");
std::filesystem::path path = trpFilesPath / "Xml" / xml_name;
size_t written = Common::FS::IOFile::WriteBytes(path, XML);
if (written != XML.size()) {
LOG_CRITICAL(
Common_Filesystem,
"Trophy XML {} write failed, wanted to write {} bytes, wrote {}",
fmt::UTF(path.u8string()), XML.size(), written);
} 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);
}
}
trpFileIndex++;
}
index++;
} catch (const std::filesystem::filesystem_error& e) {
LOG_CRITICAL(Common_Filesystem, "Filesystem error during trophy extraction: {}", e.what());
return false;
} catch (const std::exception& e) {
LOG_CRITICAL(Common_Filesystem, "Error during trophy extraction: {}", e.what());
return false;
}
if (success) {
LOG_INFO(Common_Filesystem, "Successfully extracted {} trophy files for {}", trpFileIndex,
titleId);
}
return success;
}
bool TRP::ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name) {
if (!file.Seek(entry.entry_pos)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to PNG entry offset");
return false;
}
std::vector<u8> icon(entry.entry_len);
if (!file.Read(icon)) {
LOG_ERROR(Common_Filesystem, "Failed to read PNG data");
return false;
}
auto outputFile = outputPath / "Icons" / name;
size_t written = Common::FS::IOFile::WriteBytes(outputFile, icon);
if (written != icon.size()) {
LOG_ERROR(Common_Filesystem, "PNG write failed: wanted {} bytes, wrote {}", icon.size(),
written);
return false;
}
return true;
}
bool TRP::ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name,
const std::array<u8, 16>& user_key,
const std::string& npCommId) {
constexpr size_t IV_LEN = 16;
if (!file.Seek(entry.entry_pos)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted XML entry offset");
return false;
}
std::array<u8, IV_LEN> esfmIv;
if (!file.Read(esfmIv)) {
LOG_ERROR(Common_Filesystem, "Failed to read IV for encrypted XML");
return false;
}
if (entry.entry_len <= IV_LEN) {
LOG_ERROR(Common_Filesystem, "Encrypted XML entry too small");
return false;
}
// Skip to the encrypted data (after IV)
if (!file.Seek(entry.entry_pos + IV_LEN)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted data");
return false;
}
std::vector<u8> ESFM(entry.entry_len - IV_LEN);
std::vector<u8> XML(entry.entry_len - IV_LEN);
if (!file.Read(ESFM)) {
LOG_ERROR(Common_Filesystem, "Failed to read encrypted XML data");
return false;
}
// Decrypt the data - FIX: Don't check return value since DecryptEFSM returns void
std::span<const u8, 16> key_span(user_key);
// Convert npCommId string to span (pad or truncate to 16 bytes)
std::array<u8, 16> npcommid_array{};
size_t copy_len = std::min(npCommId.size(), npcommid_array.size());
std::memcpy(npcommid_array.data(), npCommId.data(), copy_len);
std::span<const u8, 16> npcommid_span(npcommid_array);
DecryptEFSM(key_span, npcommid_span, esfmIv, ESFM, XML);
// Remove padding
removePadding(XML);
// Create output filename (replace ESFM with XML)
std::string xml_name(entry.entry_name);
size_t pos = xml_name.find("ESFM");
if (pos != std::string::npos) {
xml_name.replace(pos, 4, "XML");
}
auto outputFile = outputPath / "Xml" / xml_name;
size_t written = Common::FS::IOFile::WriteBytes(outputFile, XML);
if (written != XML.size()) {
LOG_ERROR(Common_Filesystem, "XML write failed: wanted {} bytes, wrote {}", XML.size(),
written);
return false;
}
return true;
}

View File

@ -8,6 +8,10 @@
#include "common/io_file.h"
#include "common/types.h"
static constexpr u32 TRP_MAGIC = 0xDCA24D00;
static constexpr u8 ENTRY_FLAG_PNG = 0;
static constexpr u8 ENTRY_FLAG_ENCRYPTED_XML = 3;
struct TrpHeader {
u32_be magic; // (0xDCA24D00)
u32_be version;
@ -33,9 +37,14 @@ public:
TRP();
~TRP();
bool Extract(const std::filesystem::path& trophyPath, const std::string titleId);
void GetNPcommID(const std::filesystem::path& trophyPath, int index);
private:
bool ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name);
bool ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry,
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{};

View File

@ -232,6 +232,9 @@ File* HandleTable::GetSocket(int d) {
return nullptr;
}
auto file = m_files.at(d);
if (!file) {
return nullptr;
}
if (file->type != Core::FileSys::FileType::Socket) {
return nullptr;
}

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
#include "common/logging/log.h"
@ -34,7 +34,7 @@ u32 GetChannelMask(u32 num_channels) {
case 8:
return ORBIS_AJM_CHANNELMASK_7POINT1;
default:
UNREACHABLE();
UNREACHABLE_MSG("Unexpected number of channels: {}", num_channels);
}
}

View File

@ -5,20 +5,45 @@
#include "ajm_aac.h"
#include "ajm_result.h"
#include <aacdecoder_lib.h>
// using this internal header to manually configure the decoder in RAW mode
#include "externals/aacdec/fdk-aac/libAACdec/src/aacdecoder.h"
#include <aacdecoder_lib.h>
#include <magic_enum/magic_enum.hpp>
#include <algorithm> // std::transform
#include <iterator> // std::back_inserter
#include <limits>
namespace Libraries::Ajm {
std::span<const s16> AjmAacDecoder::GetOuputPcm(u32 skipped_pcm, u32 max_pcm) const {
const auto pcm_data = std::span(m_pcm_buffer).subspan(skipped_pcm);
return pcm_data.subspan(0, std::min<u32>(pcm_data.size(), max_pcm));
}
template <>
size_t AjmAacDecoder::WriteOutputSamples<float>(SparseOutputBuffer& out, std::span<const s16> pcm) {
if (pcm.empty()) {
return 0;
}
m_resample_buffer.clear();
constexpr float inv_scale = 1.0f / std::numeric_limits<s16>::max();
std::transform(pcm.begin(), pcm.end(), std::back_inserter(m_resample_buffer),
[](auto sample) { return float(sample) * inv_scale; });
return out.Write(std::span(m_resample_buffer));
}
AjmAacDecoder::AjmAacDecoder(AjmFormatEncoding format, AjmAacCodecFlags flags, u32 channels)
: m_format(format), m_flags(flags), m_channels(channels), m_pcm_buffer(2048 * 8),
m_skip_frames(True(flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2) {}
: m_format(format), m_flags(flags), m_channels(channels), m_pcm_buffer(1024 * 8),
m_skip_frames(True(flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2) {
m_resample_buffer.reserve(m_pcm_buffer.size());
}
AjmAacDecoder::~AjmAacDecoder() {
aacDecoder_Close(m_decoder);
if (m_decoder) {
aacDecoder_Close(m_decoder);
}
}
TRANSPORT_TYPE TransportTypeFromConfigType(ConfigType config_type) {
@ -98,7 +123,7 @@ AjmSidebandFormat AjmAacDecoder::GetFormat() const {
.num_channels = static_cast<u32>(info->numChannels),
.channel_mask = GetChannelMask(info->numChannels),
.sampl_freq = static_cast<u32>(info->sampleRate),
.sample_encoding = m_format, // AjmFormatEncoding
.sample_encoding = m_format,
.bitrate = static_cast<u32>(info->bitRate),
};
}
@ -130,8 +155,7 @@ DecoderResult AjmAacDecoder::ProcessData(std::span<u8>& input, SparseOutputBuffe
const UINT sizes[] = {static_cast<UINT>(input.size())};
UINT valid = sizes[0];
aacDecoder_Fill(m_decoder, buffers, sizes, &valid);
auto ret = aacDecoder_DecodeFrame(m_decoder, reinterpret_cast<s16*>(m_pcm_buffer.data()),
m_pcm_buffer.size() / 2, 0);
auto ret = aacDecoder_DecodeFrame(m_decoder, m_pcm_buffer.data(), m_pcm_buffer.size(), 0);
switch (ret) {
case AAC_DEC_OK:
@ -167,16 +191,16 @@ DecoderResult AjmAacDecoder::ProcessData(std::span<u8>& input, SparseOutputBuffe
gapless.init.total_samples != 0 ? gapless.current.total_samples : info->aacSamplesPerFrame;
size_t pcm_written = 0;
auto pcm = GetOuputPcm(skip_samples * info->numChannels, max_samples * info->numChannels);
switch (m_format) {
case AjmFormatEncoding::S16:
pcm_written = WriteOutputSamples<s16>(output, skip_samples * info->numChannels,
max_samples * info->numChannels);
pcm_written = output.Write(pcm);
break;
case AjmFormatEncoding::S32:
UNREACHABLE_MSG("NOT IMPLEMENTED");
break;
case AjmFormatEncoding::Float:
UNREACHABLE_MSG("NOT IMPLEMENTED");
pcm_written = WriteOutputSamples<float>(output, pcm);
break;
default:
UNREACHABLE();
@ -191,4 +215,4 @@ DecoderResult AjmAacDecoder::ProcessData(std::span<u8>& input, SparseOutputBuffe
return result;
}
} // namespace Libraries::Ajm
} // namespace Libraries::Ajm

View File

@ -52,22 +52,18 @@ private:
};
template <class T>
size_t WriteOutputSamples(SparseOutputBuffer& output, u32 skipped_pcm, u32 max_pcm) {
std::span<T> pcm_data{reinterpret_cast<T*>(m_pcm_buffer.data()),
m_pcm_buffer.size() / sizeof(T)};
pcm_data = pcm_data.subspan(skipped_pcm);
const auto pcm_size = std::min(u32(pcm_data.size()), max_pcm);
return output.Write(pcm_data.subspan(0, pcm_size));
}
size_t WriteOutputSamples(SparseOutputBuffer& output, std::span<const s16> pcm);
std::span<const s16> GetOuputPcm(u32 skipped_pcm, u32 max_pcm) const;
const AjmFormatEncoding m_format;
const AjmAacCodecFlags m_flags;
const u32 m_channels;
std::vector<u8> m_pcm_buffer;
std::vector<s16> m_pcm_buffer;
std::vector<float> m_resample_buffer;
u32 m_skip_frames = 0;
InitializeParameters m_init_params = {};
AAC_DECODER_INSTANCE* m_decoder = nullptr;
};
} // namespace Libraries::Ajm
} // namespace Libraries::Ajm

View File

@ -1,9 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "ajm_error.h"
#include "ajm_mp3.h"
#include "ajm_result.h"
#include "common/assert.h"
#include "core/libraries/ajm/ajm_error.h"
#include "core/libraries/ajm/ajm_mp3.h"
#include "core/libraries/error_codes.h"
extern "C" {
@ -122,7 +124,6 @@ void AjmMp3Decoder::Reset() {
avcodec_flush_buffers(m_codec_context);
m_header.reset();
m_frame_samples = 0;
m_frame_size = 0;
}
void AjmMp3Decoder::GetInfo(void* out_info) const {
@ -141,7 +142,7 @@ void AjmMp3Decoder::GetInfo(void* out_info) const {
u32 AjmMp3Decoder::GetMinimumInputSize() const {
// 4 bytes is for mp3 header that contains frame_size
return std::max<u32>(m_frame_size, 4);
return 4;
}
DecoderResult AjmMp3Decoder::ProcessData(std::span<u8>& in_buf, SparseOutputBuffer& output,
@ -149,12 +150,11 @@ DecoderResult AjmMp3Decoder::ProcessData(std::span<u8>& in_buf, SparseOutputBuff
DecoderResult result{};
AVPacket* pkt = av_packet_alloc();
if ((!m_header.has_value() || m_frame_samples == 0) && in_buf.size() >= 4) {
m_header = std::byteswap(*reinterpret_cast<u32*>(in_buf.data()));
AjmDecMp3ParseFrame info{};
ParseMp3Header(in_buf.data(), in_buf.size(), true, &info);
m_frame_samples = info.samples_per_channel;
m_frame_size = info.frame_size;
m_header = std::byteswap(*reinterpret_cast<u32*>(in_buf.data()));
AjmDecMp3ParseFrame info{};
ParseMp3Header(in_buf.data(), in_buf.size(), true, &info);
m_frame_samples = info.samples_per_channel;
if (info.total_samples != 0 || info.encoder_delay != 0) {
gapless.init = {
.total_samples = info.total_samples,
.skip_samples = static_cast<u16>(info.encoder_delay),
@ -163,6 +163,10 @@ DecoderResult AjmMp3Decoder::ProcessData(std::span<u8>& in_buf, SparseOutputBuff
gapless.current = gapless.init;
}
if (in_buf.size() < info.frame_size) {
result.result |= ORBIS_AJM_RESULT_PARTIAL_INPUT;
}
int ret = av_parser_parse2(m_parser, m_codec_context, &pkt->data, &pkt->size, in_buf.data(),
in_buf.size(), AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
ASSERT_MSG(ret >= 0, "Error while parsing {}", ret);
@ -424,11 +428,7 @@ int AjmMp3Decoder::ParseMp3Header(const u8* p_begin, u32 stream_size, int parse_
frame->encoder_delay = std::byteswap(*reinterpret_cast<const u16*>(p_fgh + 1));
frame->total_samples = std::byteswap(*reinterpret_cast<const u32*>(p_fgh + 3));
frame->ofl_type = AjmDecMp3OflType::Fgh;
} else {
LOG_ERROR(Lib_Ajm, "FGH header CRC is incorrect.");
}
} else {
LOG_ERROR(Lib_Ajm, "Could not find vendor header.");
}
}

View File

@ -99,7 +99,6 @@ private:
SwrContext* m_swr_context = nullptr;
std::optional<u32> m_header;
u32 m_frame_samples = 0;
u32 m_frame_size = 0;
};
} // namespace Libraries::Ajm

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
#include <cmath>
@ -345,7 +345,7 @@ int PS4_SYSV_ABI sceAppContentInitialize(const OrbisAppContentInitParam* initPar
if (addcont_count > 0) {
SystemService::OrbisSystemServiceEvent event{};
event.event_type = SystemService::OrbisSystemServiceEventType::EntitlementUpdate;
event.service_entitlement_update.user_id = 0;
event.service_entitlement_update.userId = 0;
event.service_entitlement_update.np_service_label = 0;
SystemService::PushSystemServiceEvent(event);
}

View File

@ -1,23 +1,264 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <shared_mutex>
#include "audioin_backend.h"
#include "audioin_error.h"
#include "common/logging/log.h"
#include "core/libraries/audio/audioin.h"
#include "core/libraries/audio/sdl_in.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/libs.h"
namespace Libraries::AudioIn {
static std::unique_ptr<SDLAudioIn> audio = std::make_unique<SDLAudioIn>();
std::array<std::shared_ptr<PortIn>, ORBIS_AUDIO_IN_NUM_PORTS> port_table{};
std::shared_mutex port_table_mutex;
std::mutex port_allocation_mutex;
int PS4_SYSV_ABI sceAudioInChangeAppModuleState() {
LOG_ERROR(Lib_AudioIn, "(STUBBED) called");
return ORBIS_OK;
static std::unique_ptr<AudioInBackend> audio;
/*
* Helper functions
**/
static int GetPortId(s32 handle) {
int port_id = handle & 0xFF;
if (port_id >= ORBIS_AUDIO_IN_NUM_PORTS) {
LOG_ERROR(Lib_AudioIn, "Invalid port");
return ORBIS_AUDIO_IN_ERROR_PORT_FULL;
}
if ((handle & 0x7f000000) != 0x30000000) {
LOG_ERROR(Lib_AudioIn, "Invalid handle format");
return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE;
}
return port_id;
}
static s32 GetPortType(s32 handle) {
return (handle >> 16) & 0xFF;
}
static int AllocatePort(OrbisAudioInType type) {
// TODO implement port type ranges if needed
for (int i = 0; i <= ORBIS_AUDIO_IN_NUM_PORTS; i++) {
std::shared_lock read_lock{port_table_mutex};
if (!port_table[i]) {
return i;
}
}
return -1;
}
/*
* sceAudioIn implementation
**/
static bool initOnce = false;
int PS4_SYSV_ABI sceAudioInOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type,
u32 index, u32 len, u32 freq, u32 param) {
LOG_INFO(Lib_AudioIn, "called, userId={}, type={}, index={}, len={}, freq={}, param={}", userId,
type, index, len, freq, param);
if (!initOnce) {
// sceAudioInInit doesn't seem to be called by most apps before sceAudioInOpen so we init
// here
audio = std::make_unique<SDLAudioIn>();
initOnce = true;
}
if (len == 0 || len > 2048) {
LOG_ERROR(Lib_AudioIn, "Invalid size");
return ORBIS_AUDIO_IN_ERROR_INVALID_SIZE;
}
// Validate parameters
OrbisAudioInType in_type = static_cast<OrbisAudioInType>(type);
OrbisAudioInParamFormat format = static_cast<OrbisAudioInParamFormat>(param);
if (format != OrbisAudioInParamFormat::S16Mono &&
format != OrbisAudioInParamFormat::S16Stereo) {
LOG_ERROR(Lib_AudioIn, "Invalid format");
return ORBIS_AUDIO_IN_ERROR_INVALID_PARAM;
}
if (freq != 16000 && freq != 48000) {
LOG_ERROR(Lib_AudioIn, "Invalid sample rate");
return ORBIS_AUDIO_IN_ERROR_INVALID_FREQ;
}
std::unique_lock lock{port_allocation_mutex};
// Allocate port
int port_id = AllocatePort(in_type);
if (port_id < 0) {
LOG_ERROR(Lib_AudioIn, "No free audio input ports available");
return ORBIS_AUDIO_IN_ERROR_PORT_FULL;
}
// Create port object
std::shared_ptr<PortIn> port;
try {
port = std::make_shared<PortIn>();
port->type = in_type;
port->format = format;
port->samples_num = len;
port->freq = freq;
// Determine channel count and sample size based on format
switch (format) {
case OrbisAudioInParamFormat::S16Mono:
port->channels_num = 1;
port->sample_size = 2;
break;
case OrbisAudioInParamFormat::S16Stereo:
port->channels_num = 2;
port->sample_size = 2;
break;
default:
LOG_ERROR(Lib_AudioIn, "Unsupported audio format: {}", static_cast<u32>(format));
return ORBIS_AUDIO_IN_ERROR_INVALID_PARAM;
}
// Open backend
port->impl = audio->Open(*port);
if (!port->impl) {
throw std::runtime_error("Failed to create audio backend");
}
} catch (const std::bad_alloc&) {
LOG_ERROR(Lib_AudioIn, "Failed to allocate memory for audio port");
return ORBIS_AUDIO_IN_ERROR_OUT_OF_MEMORY;
} catch (const std::exception& e) {
LOG_ERROR(Lib_AudioIn, "Failed to open audio input port: {}", e.what());
return ORBIS_AUDIO_IN_ERROR_NOT_OPENED;
}
// Store the port pointer with write lock
{
std::unique_lock write_lock{port_table_mutex};
port_table[port_id] = port;
}
// Create handle
s32 handle = (type << 16) | port_id | 0x30000000;
LOG_INFO(Lib_AudioIn, "Opened audio input port {}: type={}, samples={}, freq={}, format={}",
handle, static_cast<u32>(in_type), len, freq, static_cast<u32>(format));
return handle;
}
int PS4_SYSV_ABI sceAudioInHqOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type,
u32 index, u32 len, u32 freq, u32 param) {
LOG_INFO(Lib_AudioIn, "called, userId={}, type={}, index={}, len={}, freq={}, param={}", userId,
type, index, len, freq, param);
int result = sceAudioInOpen(userId, type, index, len, freq, param);
if (result < 0) {
LOG_ERROR(Lib_AudioIn, "Error returned {:#x}", result);
}
return result;
}
int PS4_SYSV_ABI sceAudioInClose(s32 handle) {
audio->AudioInClose(handle);
LOG_INFO(Lib_AudioIn, "called, handle={:#x}", handle);
int port_id = GetPortId(handle);
if (port_id < 0) {
LOG_ERROR(Lib_AudioIn, "Invalid port id");
return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE;
}
std::unique_lock lock{port_allocation_mutex};
std::shared_ptr<PortIn> port;
// Get and clear the port pointer with write lock
{
std::unique_lock write_lock{port_table_mutex};
port = std::move(port_table[port_id]);
if (!port) {
LOG_ERROR(Lib_AudioIn, "Port wasn't open {}", port_id);
return ORBIS_AUDIO_IN_ERROR_NOT_OPENED;
}
port_table[port_id].reset();
}
// Free resources
std::scoped_lock port_lock{port->mutex};
port->impl.reset();
LOG_INFO(Lib_AudioIn, "Closed audio input port {}", handle);
return ORBIS_OK;
}
int PS4_SYSV_ABI sceAudioInInput(s32 handle, void* dest) {
LOG_TRACE(Lib_AudioIn, "called, handle={:#x}, dest={}", handle, fmt::ptr(dest));
int port_id = GetPortId(handle);
if (port_id < 0) {
LOG_ERROR(Lib_AudioIn, "Invalid port id");
return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE;
}
if (!dest) {
LOG_ERROR(Lib_AudioIn, "Invalid output buffer pointer");
return ORBIS_AUDIO_IN_ERROR_INVALID_POINTER;
}
// Get port with read lock
std::shared_ptr<PortIn> port;
{
std::shared_lock read_lock{port_table_mutex};
if (port_id < 0 || port_id >= static_cast<int>(port_table.size())) {
LOG_ERROR(Lib_AudioIn, "Invalid port id: {}", port_id);
return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE;
}
port = port_table[port_id];
}
if (!port || !port->impl) {
LOG_ERROR(Lib_AudioIn, "Audio input port {} is not open", handle);
return ORBIS_AUDIO_IN_ERROR_NOT_OPENED;
}
std::scoped_lock lock{port->mutex};
return port->impl->Read(dest);
}
int PS4_SYSV_ABI sceAudioInGetSilentState(s32 handle) {
LOG_TRACE(Lib_AudioIn, "called, handle={:#x}", handle);
int port_id = GetPortId(handle);
if (port_id < 0) {
LOG_ERROR(Lib_AudioIn, "Invalid port id");
return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE;
}
// Get port with read lock
std::shared_ptr<PortIn> port;
{
std::shared_lock read_lock{port_table_mutex};
if (port_id < 0 || port_id >= static_cast<int>(port_table.size())) {
LOG_ERROR(Lib_AudioIn, "Invalid port id: {}", port_id);
return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE;
}
port = port_table[port_id];
}
if (!port || !port->impl) {
LOG_ERROR(Lib_AudioIn, "Audio input port {} is not open", handle);
return ORBIS_AUDIO_IN_ERROR_NOT_OPENED;
}
u32 silent_state = 0;
std::scoped_lock lock{port->mutex};
if (!port->impl->IsAvailable()) { // if no mic exist or is not available
silent_state |= ORBIS_AUDIO_IN_SILENT_STATE_DEVICE_NONE;
}
return silent_state;
}
/*
* Stubbed functions
**/
int PS4_SYSV_ABI sceAudioInChangeAppModuleState() {
LOG_ERROR(Lib_AudioIn, "(STUBBED) called");
return ORBIS_OK;
}
@ -91,20 +332,6 @@ int PS4_SYSV_ABI sceAudioInGetRerouteCount() {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceAudioInGetSilentState() {
LOG_ERROR(Lib_AudioIn, "(STUBBED) called");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceAudioInHqOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type,
u32 index, u32 len, u32 freq, u32 param) {
int result = audio->AudioInOpen(type, len, freq, param);
if (result < 0) {
LOG_ERROR(Lib_AudioIn, "Error returned {:#x}", result);
}
return result;
}
int PS4_SYSV_ABI sceAudioInHqOpenEx() {
LOG_ERROR(Lib_AudioIn, "(STUBBED) called");
return ORBIS_OK;
@ -115,10 +342,6 @@ int PS4_SYSV_ABI sceAudioInInit() {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceAudioInInput(s32 handle, void* dest) {
return audio->AudioInInput(handle, dest);
}
int PS4_SYSV_ABI sceAudioInInputs() {
LOG_ERROR(Lib_AudioIn, "(STUBBED) called");
return ORBIS_OK;
@ -129,15 +352,6 @@ int PS4_SYSV_ABI sceAudioInIsSharedDevice() {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceAudioInOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type,
u32 index, u32 len, u32 freq, u32 param) {
int result = audio->AudioInOpen(type, len, freq, param);
if (result < 0) {
LOG_ERROR(Lib_AudioIn, "Error returned {:#x}", result);
}
return result;
}
int PS4_SYSV_ABI sceAudioInOpenEx() {
LOG_ERROR(Lib_AudioIn, "(STUBBED) called");
return ORBIS_OK;

View File

@ -3,6 +3,7 @@
#pragma once
#include <mutex>
#include <core/libraries/system/userservice.h>
#include "common/types.h"
@ -12,10 +13,31 @@ class SymbolsResolver;
namespace Libraries::AudioIn {
class PortInBackend;
constexpr s32 ORBIS_AUDIO_IN_NUM_PORTS = 7;
enum class OrbisAudioInParamFormat : u32 { S16Mono = 0, S16Stereo = 2 };
enum class OrbisAudioInType : u32 { VoiceChat = 0, General = 1, VoiceRecognition = 5 };
constexpr int ORBIS_AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x00000001;
constexpr int ORBIS_AUDIO_IN_SILENT_STATE_PRIORITY_LOW = 0x00000002;
constexpr int ORBIS_AUDIO_IN_SILENT_STATE_USER_SETTING = 0x0000000;
constexpr int ORBIS_AUDIO_IN_SILENT_STATE_UNABLE_FORMAT = 0x00000008;
struct PortIn {
std::mutex mutex;
std::unique_ptr<PortInBackend> impl{};
OrbisAudioInType type;
OrbisAudioInParamFormat format;
u32 samples_num = 0;
u32 freq = 0;
u32 channels_num = 0;
u32 sample_size = 0;
};
int PS4_SYSV_ABI sceAudioInChangeAppModuleState();
int PS4_SYSV_ABI sceAudioInClose(s32 handle);
int PS4_SYSV_ABI sceAudioInCountPorts();
@ -32,7 +54,7 @@ int PS4_SYSV_ABI sceAudioInExtSetAecMode();
int PS4_SYSV_ABI sceAudioInGetGain();
int PS4_SYSV_ABI sceAudioInGetHandleStatusInfo();
int PS4_SYSV_ABI sceAudioInGetRerouteCount();
int PS4_SYSV_ABI sceAudioInGetSilentState();
int PS4_SYSV_ABI sceAudioInGetSilentState(s32 handle);
int PS4_SYSV_ABI sceAudioInHqOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type,
u32 index, u32 len, u32 freq, u32 param);
int PS4_SYSV_ABI sceAudioInHqOpenEx();

View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
namespace Libraries::AudioIn {
struct PortIn;
class PortInBackend {
public:
virtual ~PortInBackend() = default;
virtual int Read(void* out_buffer) = 0;
virtual void Clear() = 0;
virtual bool IsAvailable() = 0;
};
class AudioInBackend {
public:
AudioInBackend() = default;
virtual ~AudioInBackend() = default;
virtual std::unique_ptr<PortInBackend> Open(PortIn& port) = 0;
};
class SDLAudioIn final : public AudioInBackend {
public:
std::unique_ptr<PortInBackend> Open(PortIn& port) override;
};
} // namespace Libraries::AudioIn

View File

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/libraries/error_codes.h"
// AudioIn library
constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE = 0x80260101;
constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_SIZE = 0x80260102;
constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_FREQ = 0x80260103;
constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_TYPE = 0x80260104;
constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_POINTER = 0x80260105;
constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_PARAM = 0x80260106;
constexpr int ORBIS_AUDIO_IN_ERROR_PORT_FULL = 0x80260107;
constexpr int ORBIS_AUDIO_IN_ERROR_OUT_OF_MEMORY = 0x80260108;
constexpr int ORBIS_AUDIO_IN_ERROR_NOT_OPENED = 0x80260109;
constexpr int ORBIS_AUDIO_IN_ERROR_BUSY = 0x8026010A;
constexpr int ORBIS_AUDIO_IN_ERROR_SYSTEM_MEMORY = 0x8026010B;
constexpr int ORBIS_AUDIO_IN_ERROR_SYSTEM_IPC = 0x8026010C;

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,32 @@ class PortBackend;
// Main up to 8 ports, BGM 1 port, voice up to 4 ports,
// personal up to 4 ports, padspk up to 5 ports, aux 1 port
constexpr s32 SCE_AUDIO_OUT_NUM_PORTS = 22;
constexpr s32 SCE_AUDIO_OUT_VOLUME_0DB = 32768; // max volume value
constexpr s32 ORBIS_AUDIO_OUT_NUM_PORTS = 25;
constexpr s32 ORBIS_AUDIO_OUT_VOLUME_0DB = 32768; // max volume value
constexpr s32 ORBIS_AUDIO_OUT_MIXLEVEL_PADSPK_DEFAULT = 11626; // default -9db
constexpr s32 ORBIS_AUDIO_OUT_MIXLEVEL_PADSPK_0DB = 32768; // max volume
constexpr s32 ORBIS_AUDIO_OUT_PARAM_ATTR_RESTRICTED = 0x00010000;
constexpr s32 ORBIS_AUDIO_OUT_PARAM_ATTR_MIX_TO_MAIN = 0x00020000;
// Volume flags
constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_L_CH = (1u << 0);
constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_R_CH = (1u << 1);
constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_C_CH = (1u << 2);
constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_LFE_CH = (1u << 3);
constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_LS_CH = (1u << 4);
constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_RS_CH = (1u << 5);
constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_LE_CH = (1u << 6);
constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_RE_CH = (1u << 7);
// Port state constants
constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_UNKNOWN = 0x00;
constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_PRIMARY = 0x01;
constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_SECONDARY = 0x02;
constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_TERTIARY = 0x04;
constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_HEADPHONE = 0x40;
constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_EXTERNAL = 0x80;
enum class OrbisAudioOutPort {
Main = 0,
@ -53,6 +77,9 @@ union OrbisAudioOutParamExtendedInformation {
BitField<16, 4, OrbisAudioOutParamAttr> attributes;
BitField<20, 10, u32> reserve1;
BitField<31, 1, u32> unused;
u32 Unpack() const {
return *reinterpret_cast<const u32*>(this);
}
};
struct OrbisAudioOutOutputParam {
@ -70,6 +97,12 @@ struct OrbisAudioOutPortState {
u64 reserved64[2];
};
struct OrbisAudioOutSystemState {
float loudness;
u8 reserved8[4];
u64 reserved64[3];
};
struct AudioFormatInfo {
bool is_float;
u8 sample_size;
@ -77,6 +110,7 @@ struct AudioFormatInfo {
/// Layout array remapping channel indices, specified in this order:
/// FL, FR, FC, LFE, BL, BR, SL, SR
std::array<int, 8> channel_layout;
bool is_std;
[[nodiscard]] u16 FrameSize() const {
return sample_size * num_channels;
@ -87,100 +121,100 @@ struct PortOut {
std::mutex mutex;
std::unique_ptr<PortBackend> impl{};
void* output_buffer;
void* output_buffer = nullptr;
std::condition_variable_any output_cv;
bool output_ready;
bool output_ready = false;
Kernel::Thread output_thread{};
OrbisAudioOutPort type;
AudioFormatInfo format_info;
u32 sample_rate;
u32 buffer_frames;
u64 last_output_time;
u32 sample_rate = 48000;
u32 buffer_frames = 1024;
u64 last_output_time = 0;
std::array<s32, 8> volume;
[[nodiscard]] bool IsOpen() const {
return impl != nullptr;
}
s32 userId = 0;
s32 mixLevelPadSpk = ORBIS_AUDIO_OUT_MIXLEVEL_PADSPK_DEFAULT;
bool is_restricted = false;
bool is_mix_to_main = false;
[[nodiscard]] u32 BufferSize() const {
return buffer_frames * format_info.FrameSize();
}
};
int PS4_SYSV_ABI sceAudioOutDeviceIdOpen();
int PS4_SYSV_ABI sceAudioDeviceControlGet();
int PS4_SYSV_ABI sceAudioDeviceControlSet();
int PS4_SYSV_ABI sceAudioOutA3dControl();
int PS4_SYSV_ABI sceAudioOutA3dExit();
int PS4_SYSV_ABI sceAudioOutA3dInit();
int PS4_SYSV_ABI sceAudioOutAttachToApplicationByPid();
int PS4_SYSV_ABI sceAudioOutChangeAppModuleState();
int PS4_SYSV_ABI sceAudioOutClose(s32 handle);
int PS4_SYSV_ABI sceAudioOutDetachFromApplicationByPid();
int PS4_SYSV_ABI sceAudioOutExConfigureOutputMode();
int PS4_SYSV_ABI sceAudioOutExGetSystemInfo();
int PS4_SYSV_ABI sceAudioOutExPtClose();
int PS4_SYSV_ABI sceAudioOutExPtGetLastOutputTime();
int PS4_SYSV_ABI sceAudioOutExPtOpen();
int PS4_SYSV_ABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode();
int PS4_SYSV_ABI sceAudioOutGetFocusEnablePid();
int PS4_SYSV_ABI sceAudioOutGetHandleStatusInfo();
int PS4_SYSV_ABI sceAudioOutGetInfo();
int PS4_SYSV_ABI sceAudioOutGetInfoOpenNum();
int PS4_SYSV_ABI sceAudioOutGetLastOutputTime(s32 handle, u64* output_time);
int PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* state);
int PS4_SYSV_ABI sceAudioOutGetSimulatedBusUsableStatusByBusType();
int PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo();
int PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo2();
int PS4_SYSV_ABI sceAudioOutGetSparkVss();
int PS4_SYSV_ABI sceAudioOutGetSystemState();
int PS4_SYSV_ABI sceAudioOutInit();
int PS4_SYSV_ABI sceAudioOutInitIpmiGetSession();
int PS4_SYSV_ABI sceAudioOutMasteringGetState();
int PS4_SYSV_ABI sceAudioOutMasteringInit();
int PS4_SYSV_ABI sceAudioOutMasteringSetParam();
int PS4_SYSV_ABI sceAudioOutMasteringTerm();
int PS4_SYSV_ABI sceAudioOutMbusInit();
s32 PS4_SYSV_ABI sceAudioOutDeviceIdOpen();
s32 PS4_SYSV_ABI sceAudioDeviceControlGet();
s32 PS4_SYSV_ABI sceAudioDeviceControlSet();
s32 PS4_SYSV_ABI sceAudioOutA3dControl();
s32 PS4_SYSV_ABI sceAudioOutA3dExit();
s32 PS4_SYSV_ABI sceAudioOutA3dInit();
s32 PS4_SYSV_ABI sceAudioOutAttachToApplicationByPid();
s32 PS4_SYSV_ABI sceAudioOutChangeAppModuleState();
s32 PS4_SYSV_ABI sceAudioOutClose(s32 handle);
s32 PS4_SYSV_ABI sceAudioOutDetachFromApplicationByPid();
s32 PS4_SYSV_ABI sceAudioOutExConfigureOutputMode();
s32 PS4_SYSV_ABI sceAudioOutExGetSystemInfo();
s32 PS4_SYSV_ABI sceAudioOutExPtClose();
s32 PS4_SYSV_ABI sceAudioOutExPtGetLastOutputTime();
s32 PS4_SYSV_ABI sceAudioOutExPtOpen();
s32 PS4_SYSV_ABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode();
s32 PS4_SYSV_ABI sceAudioOutGetFocusEnablePid();
s32 PS4_SYSV_ABI sceAudioOutGetHandleStatusInfo();
s32 PS4_SYSV_ABI sceAudioOutGetInfo();
s32 PS4_SYSV_ABI sceAudioOutGetInfoOpenNum();
s32 PS4_SYSV_ABI sceAudioOutGetLastOutputTime(s32 handle, u64* output_time);
s32 PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* state);
s32 PS4_SYSV_ABI sceAudioOutGetSimulatedBusUsableStatusByBusType();
s32 PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo();
s32 PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo2();
s32 PS4_SYSV_ABI sceAudioOutGetSparkVss();
s32 PS4_SYSV_ABI sceAudioOutGetSystemState(OrbisAudioOutSystemState* state);
s32 PS4_SYSV_ABI sceAudioOutInit();
s32 PS4_SYSV_ABI sceAudioOutInitIpmiGetSession();
s32 PS4_SYSV_ABI sceAudioOutMasteringGetState();
s32 PS4_SYSV_ABI sceAudioOutMasteringInit(u32 flags);
s32 PS4_SYSV_ABI sceAudioOutMasteringSetParam();
s32 PS4_SYSV_ABI sceAudioOutMasteringTerm();
s32 PS4_SYSV_ABI sceAudioOutMbusInit();
s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id,
OrbisAudioOutPort port_type, s32 index, u32 length,
u32 sample_rate, OrbisAudioOutParamExtendedInformation param_type);
int PS4_SYSV_ABI sceAudioOutOpenEx();
s32 PS4_SYSV_ABI sceAudioOutOpenEx();
s32 PS4_SYSV_ABI sceAudioOutOutput(s32 handle, void* ptr);
s32 PS4_SYSV_ABI sceAudioOutOutputs(OrbisAudioOutOutputParam* param, u32 num);
int PS4_SYSV_ABI sceAudioOutPtClose();
int PS4_SYSV_ABI sceAudioOutPtGetLastOutputTime();
int PS4_SYSV_ABI sceAudioOutPtOpen();
int PS4_SYSV_ABI sceAudioOutSetConnections();
int PS4_SYSV_ABI sceAudioOutSetConnectionsForUser();
int PS4_SYSV_ABI sceAudioOutSetDevConnection();
int PS4_SYSV_ABI sceAudioOutSetHeadphoneOutMode();
int PS4_SYSV_ABI sceAudioOutSetJediJackVolume();
int PS4_SYSV_ABI sceAudioOutSetJediSpkVolume();
int PS4_SYSV_ABI sceAudioOutSetMainOutput();
int PS4_SYSV_ABI sceAudioOutSetMixLevelPadSpk();
int PS4_SYSV_ABI sceAudioOutSetMorpheusParam();
int PS4_SYSV_ABI sceAudioOutSetMorpheusWorkingMode();
int PS4_SYSV_ABI sceAudioOutSetPortConnections();
int PS4_SYSV_ABI sceAudioOutSetPortStatuses();
int PS4_SYSV_ABI sceAudioOutSetRecMode();
int PS4_SYSV_ABI sceAudioOutSetSparkParam();
int PS4_SYSV_ABI sceAudioOutSetUsbVolume();
s32 PS4_SYSV_ABI sceAudioOutPtClose();
s32 PS4_SYSV_ABI sceAudioOutPtGetLastOutputTime();
s32 PS4_SYSV_ABI sceAudioOutPtOpen();
s32 PS4_SYSV_ABI sceAudioOutSetConnections();
s32 PS4_SYSV_ABI sceAudioOutSetConnectionsForUser();
s32 PS4_SYSV_ABI sceAudioOutSetDevConnection();
s32 PS4_SYSV_ABI sceAudioOutSetHeadphoneOutMode();
s32 PS4_SYSV_ABI sceAudioOutSetJediJackVolume();
s32 PS4_SYSV_ABI sceAudioOutSetJediSpkVolume();
s32 PS4_SYSV_ABI sceAudioOutSetMainOutput();
s32 PS4_SYSV_ABI sceAudioOutSetMixLevelPadSpk(s32 handle, s32 mixLevel);
s32 PS4_SYSV_ABI sceAudioOutSetMorpheusParam();
s32 PS4_SYSV_ABI sceAudioOutSetMorpheusWorkingMode();
s32 PS4_SYSV_ABI sceAudioOutSetPortConnections();
s32 PS4_SYSV_ABI sceAudioOutSetPortStatuses();
s32 PS4_SYSV_ABI sceAudioOutSetRecMode();
s32 PS4_SYSV_ABI sceAudioOutSetSparkParam();
s32 PS4_SYSV_ABI sceAudioOutSetUsbVolume();
s32 PS4_SYSV_ABI sceAudioOutSetVolume(s32 handle, s32 flag, s32* vol);
int PS4_SYSV_ABI sceAudioOutSetVolumeDown();
int PS4_SYSV_ABI sceAudioOutStartAuxBroadcast();
int PS4_SYSV_ABI sceAudioOutStartSharePlay();
int PS4_SYSV_ABI sceAudioOutStopAuxBroadcast();
int PS4_SYSV_ABI sceAudioOutStopSharePlay();
int PS4_SYSV_ABI sceAudioOutSuspendResume();
int PS4_SYSV_ABI sceAudioOutSysConfigureOutputMode();
int PS4_SYSV_ABI sceAudioOutSysGetHdmiMonitorInfo();
int PS4_SYSV_ABI sceAudioOutSysGetSystemInfo();
int PS4_SYSV_ABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode();
int PS4_SYSV_ABI sceAudioOutSystemControlGet();
int PS4_SYSV_ABI sceAudioOutSystemControlSet();
int PS4_SYSV_ABI sceAudioOutSparkControlSetEqCoef();
int PS4_SYSV_ABI sceAudioOutSetSystemDebugState();
s32 PS4_SYSV_ABI sceAudioOutSetVolumeDown();
s32 PS4_SYSV_ABI sceAudioOutStartAuxBroadcast();
s32 PS4_SYSV_ABI sceAudioOutStartSharePlay();
s32 PS4_SYSV_ABI sceAudioOutStopAuxBroadcast();
s32 PS4_SYSV_ABI sceAudioOutStopSharePlay();
s32 PS4_SYSV_ABI sceAudioOutSuspendResume();
s32 PS4_SYSV_ABI sceAudioOutSysConfigureOutputMode();
s32 PS4_SYSV_ABI sceAudioOutSysGetHdmiMonitorInfo();
s32 PS4_SYSV_ABI sceAudioOutSysGetSystemInfo();
s32 PS4_SYSV_ABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode();
s32 PS4_SYSV_ABI sceAudioOutSystemControlGet();
s32 PS4_SYSV_ABI sceAudioOutSystemControlSet();
s32 PS4_SYSV_ABI sceAudioOutSparkControlSetEqCoef();
s32 PS4_SYSV_ABI sceAudioOutSetSystemDebugState();
void AdjustVol();
void RegisterLib(Core::Loader::SymbolsResolver* sym);

View File

@ -1,156 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <thread>
#include <SDL3/SDL_audio.h>
#include <SDL3/SDL_hints.h>
#include "common/config.h"
#include "common/logging/log.h"
#include "core/libraries/audio/audioout.h"
#include "core/libraries/audio/audioout_backend.h"
#define SDL_INVALID_AUDIODEVICEID 0 // Defined in SDL_audio.h but not made a macro
namespace Libraries::AudioOut {
class SDLPortBackend : public PortBackend {
public:
explicit SDLPortBackend(const PortOut& port)
: frame_size(port.format_info.FrameSize()), guest_buffer_size(port.BufferSize()) {
const SDL_AudioSpec fmt = {
.format = port.format_info.is_float ? SDL_AUDIO_F32LE : SDL_AUDIO_S16LE,
.channels = port.format_info.num_channels,
.freq = static_cast<int>(port.sample_rate),
};
// Determine port type
std::string port_name = port.type == OrbisAudioOutPort::PadSpk
? Config::getPadSpkOutputDevice()
: Config::getMainOutputDevice();
SDL_AudioDeviceID dev_id = SDL_INVALID_AUDIODEVICEID;
if (port_name == "None") {
stream = nullptr;
return;
} else if (port_name == "Default Device") {
dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
} else {
try {
SDL_AudioDeviceID* dev_array = SDL_GetAudioPlaybackDevices(nullptr);
for (; dev_array != 0;) {
std::string dev_name(SDL_GetAudioDeviceName(*dev_array));
if (dev_name == port_name) {
dev_id = *dev_array;
break;
} else {
dev_array++;
}
}
if (dev_id == SDL_INVALID_AUDIODEVICEID) {
LOG_WARNING(Lib_AudioOut, "Audio device not found: {}", port_name);
dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
}
} catch (const std::exception& e) {
LOG_ERROR(Lib_AudioOut, "Invalid audio output device: {}", port_name);
stream = nullptr;
return;
}
}
// Open the audio stream
stream = SDL_OpenAudioDeviceStream(dev_id, &fmt, nullptr, nullptr);
if (stream == nullptr) {
LOG_ERROR(Lib_AudioOut, "Failed to create SDL audio stream: {}", SDL_GetError());
return;
}
CalculateQueueThreshold();
if (!SDL_SetAudioStreamInputChannelMap(stream, port.format_info.channel_layout.data(),
port.format_info.num_channels)) {
LOG_ERROR(Lib_AudioOut, "Failed to configure SDL audio stream channel map: {}",
SDL_GetError());
SDL_DestroyAudioStream(stream);
stream = nullptr;
return;
}
if (!SDL_ResumeAudioStreamDevice(stream)) {
LOG_ERROR(Lib_AudioOut, "Failed to resume SDL audio stream: {}", SDL_GetError());
SDL_DestroyAudioStream(stream);
stream = nullptr;
return;
}
SDL_SetAudioStreamGain(stream, Config::getVolumeSlider() / 100.0f);
}
~SDLPortBackend() override {
if (!stream) {
return;
}
SDL_DestroyAudioStream(stream);
stream = nullptr;
}
void Output(void* ptr) override {
if (!stream) {
return;
}
// AudioOut library manages timing, but we still need to guard against the SDL
// audio queue stalling, which may happen during device changes, for example.
// Otherwise, latency may grow over time unbounded.
if (const auto queued = SDL_GetAudioStreamQueued(stream); queued >= queue_threshold) {
LOG_INFO(Lib_AudioOut, "SDL audio queue backed up ({} queued, {} threshold), clearing.",
queued, queue_threshold);
SDL_ClearAudioStream(stream);
// Recalculate the threshold in case this happened because of a device change.
CalculateQueueThreshold();
}
if (!SDL_PutAudioStreamData(stream, ptr, static_cast<int>(guest_buffer_size))) {
LOG_ERROR(Lib_AudioOut, "Failed to output to SDL audio stream: {}", SDL_GetError());
}
}
void SetVolume(const std::array<int, 8>& ch_volumes) override {
if (!stream) {
return;
}
// SDL does not have per-channel volumes, for now just take the maximum of the channels.
const auto vol = *std::ranges::max_element(ch_volumes);
if (!SDL_SetAudioStreamGain(stream, static_cast<float>(vol) / SCE_AUDIO_OUT_VOLUME_0DB *
Config::getVolumeSlider() / 100.0f)) {
LOG_WARNING(Lib_AudioOut, "Failed to change SDL audio stream volume: {}",
SDL_GetError());
}
}
private:
void CalculateQueueThreshold() {
SDL_AudioSpec discard;
int sdl_buffer_frames;
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(stream), &discard,
&sdl_buffer_frames)) {
LOG_WARNING(Lib_AudioOut, "Failed to get SDL audio stream buffer size: {}",
SDL_GetError());
sdl_buffer_frames = 0;
}
const auto sdl_buffer_size = sdl_buffer_frames * frame_size;
const auto new_threshold = std::max(guest_buffer_size, sdl_buffer_size) * 4;
if (host_buffer_size != sdl_buffer_size || queue_threshold != new_threshold) {
host_buffer_size = sdl_buffer_size;
queue_threshold = new_threshold;
LOG_INFO(Lib_AudioOut,
"SDL audio buffers: guest = {} bytes, host = {} bytes, threshold = {} bytes",
guest_buffer_size, host_buffer_size, queue_threshold);
}
}
u32 frame_size;
u32 guest_buffer_size;
u32 host_buffer_size{};
u32 queue_threshold{};
SDL_AudioStream* stream{};
};
std::unique_ptr<PortBackend> SDLAudioOut::Open(PortOut& port) {
return std::make_unique<SDLPortBackend>(port);
}
} // namespace Libraries::AudioOut

View File

@ -0,0 +1,135 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <SDL3/SDL.h>
#include <common/config.h>
#include <common/logging/log.h>
#include "audioin.h"
#include "audioin_backend.h"
namespace Libraries::AudioIn {
class SDLInPortBackend : public PortInBackend {
public:
explicit SDLInPortBackend(const PortIn& port) : port(port) {
SDL_AudioFormat sampleFormat = SDL_AUDIO_S16; // PS4 uses S16 format
SDL_AudioSpec fmt;
SDL_zero(fmt);
fmt.format = sampleFormat;
fmt.channels = static_cast<Uint8>(port.channels_num);
fmt.freq = static_cast<int>(port.freq);
std::string micDevStr = Config::getMicDevice();
uint32_t devId = 0;
if (micDevStr == "None") {
nullDevice = true;
LOG_INFO(Lib_AudioIn, "Audio input disabled by configuration");
} else if (micDevStr == "Default Device") {
devId = SDL_AUDIO_DEVICE_DEFAULT_RECORDING;
LOG_INFO(Lib_AudioIn, "Using default audio input device");
} else {
try {
devId = static_cast<uint32_t>(std::stoul(micDevStr));
LOG_INFO(Lib_AudioIn, "Using audio input device ID: {}", devId);
} catch (const std::exception& e) {
nullDevice = true;
LOG_WARNING(Lib_AudioIn, "Invalid device ID '{}', disabling input", micDevStr);
}
}
if (!nullDevice) {
stream = SDL_OpenAudioDeviceStream(devId, &fmt, nullptr, nullptr);
if (stream) {
if (SDL_ResumeAudioStreamDevice(stream)) {
LOG_INFO(Lib_AudioIn, "Audio input opened: {} Hz, {} channels, format {}",
port.freq, port.channels_num, static_cast<u32>(port.format));
} else {
SDL_DestroyAudioStream(stream);
stream = nullptr;
LOG_ERROR(Lib_AudioIn, "Failed to resume audio input stream");
}
} else {
LOG_ERROR(Lib_AudioIn, "Failed to open audio input device: {}", SDL_GetError());
}
}
// Allocate internal buffer for null device simulation
if (!stream) {
const size_t bufferSize = port.samples_num * port.sample_size * port.channels_num;
internal_buffer = std::malloc(bufferSize);
if (internal_buffer) {
// Fill with silence
std::memset(internal_buffer, 0, bufferSize);
LOG_INFO(Lib_AudioIn, "Created null audio input buffer of {} bytes", bufferSize);
}
}
}
~SDLInPortBackend() override {
if (stream) {
SDL_DestroyAudioStream(stream);
}
if (internal_buffer) {
std::free(internal_buffer);
internal_buffer = nullptr;
}
}
int Read(void* out_buffer) override {
const int bytesToRead = port.samples_num * port.sample_size * port.channels_num;
if (stream) {
// Read from actual audio device
int attempts = 0;
while (SDL_GetAudioStreamAvailable(stream) < bytesToRead) {
SDL_Delay(1);
if (++attempts > 1000) {
return 0;
}
}
const int bytesRead = SDL_GetAudioStreamData(stream, out_buffer, bytesToRead);
if (bytesRead < 0) {
LOG_ERROR(Lib_AudioIn, "Audio input read error: {}", SDL_GetError());
return 0;
}
const int framesRead = bytesRead / (port.sample_size * port.channels_num);
return framesRead;
} else if (internal_buffer) {
// Return silence from null device buffer
std::memcpy(out_buffer, internal_buffer, bytesToRead);
return port.samples_num;
} else {
// No device available
return 0;
}
}
void Clear() override {
if (stream) {
SDL_ClearAudioStream(stream);
}
}
bool IsAvailable() override {
if (nullDevice) {
return false;
} else {
return true;
}
}
private:
const PortIn& port;
SDL_AudioStream* stream = nullptr;
void* internal_buffer = nullptr;
bool nullDevice = false;
};
std::unique_ptr<PortInBackend> SDLAudioIn::Open(PortIn& port) {
return std::make_unique<SDLInPortBackend>(port);
}
} // namespace Libraries::AudioIn

View File

@ -0,0 +1,600 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <atomic>
#include <cstring>
#include <memory>
#include <thread>
#include <SDL3/SDL_audio.h>
#include <SDL3/SDL_hints.h>
#include "common/config.h"
#include "common/logging/log.h"
#include "core/libraries/audio/audioout.h"
#include "core/libraries/audio/audioout_backend.h"
#include "core/libraries/kernel/threads.h"
// SIMD support detection
#if defined(__x86_64__) || defined(_M_X64)
#include <immintrin.h>
#define HAS_SSE2
#endif
#define SDL_INVALID_AUDIODEVICEID 0
namespace Libraries::AudioOut {
// Volume constants
constexpr float VOLUME_0DB = 32768.0f; // 1 << 15
constexpr float INV_VOLUME_0DB = 1.0f / VOLUME_0DB;
constexpr float VOLUME_EPSILON = 0.001f;
// Timing constants
constexpr u64 VOLUME_CHECK_INTERVAL_US = 50000; // Check every 50ms
constexpr u64 MIN_SLEEP_THRESHOLD_US = 10;
constexpr u64 TIMING_RESYNC_THRESHOLD_US = 100000; // Resync if >100ms behind
// Queue management
constexpr u32 QUEUE_MULTIPLIER = 4;
// Memory alignment for SIMD
constexpr size_t AUDIO_BUFFER_ALIGNMENT = 32;
// Channel positions
enum ChannelPos : u8 {
FL = 0,
FR = 1,
FC = 2,
LF = 3,
SL = 4,
SR = 5,
BL = 6,
BR = 7,
STD_SL = 6,
STD_SR = 7,
STD_BL = 4,
STD_BR = 5
};
class SDLPortBackend : public PortBackend {
public:
explicit SDLPortBackend(const PortOut& port)
: frame_size(port.format_info.FrameSize()), guest_buffer_size(port.BufferSize()),
buffer_frames(port.buffer_frames), sample_rate(port.sample_rate),
num_channels(port.format_info.num_channels), is_float(port.format_info.is_float),
is_std(port.format_info.is_std), channel_layout(port.format_info.channel_layout) {
if (!Initialize(port.type)) {
LOG_ERROR(Lib_AudioOut, "Failed to initialize SDL audio backend");
}
}
~SDLPortBackend() override {
Cleanup();
}
void Output(void* ptr) override {
if (!stream || !internal_buffer || !convert) [[unlikely]] {
return;
}
if (ptr == nullptr) [[unlikely]] {
return;
}
UpdateVolumeIfChanged();
const u64 current_time = Kernel::sceKernelGetProcessTime();
convert(ptr, internal_buffer, buffer_frames, nullptr);
HandleTiming(current_time);
if ((output_count++ & 0xF) == 0) { // Check every 16 outputs
ManageAudioQueue();
}
if (!SDL_PutAudioStreamData(stream, internal_buffer, internal_buffer_size)) [[unlikely]] {
LOG_ERROR(Lib_AudioOut, "Failed to output to SDL audio stream: {}", SDL_GetError());
}
last_output_time.store(current_time, std::memory_order_release);
}
void SetVolume(const std::array<int, 8>& ch_volumes) override {
if (!stream) [[unlikely]] {
return;
}
float max_channel_gain = 0.0f;
const u32 channels_to_check = std::min(num_channels, 8u);
for (u32 i = 0; i < channels_to_check; i++) {
const float channel_gain = static_cast<float>(ch_volumes[i]) * INV_VOLUME_0DB;
max_channel_gain = std::max(max_channel_gain, channel_gain);
}
const float slider_gain = Config::getVolumeSlider() * 0.01f; // Faster than /100.0f
const float total_gain = max_channel_gain * slider_gain;
const float current = current_gain.load(std::memory_order_acquire);
if (std::abs(total_gain - current) < VOLUME_EPSILON) {
return;
}
// Apply volume change
if (SDL_SetAudioStreamGain(stream, total_gain)) {
current_gain.store(total_gain, std::memory_order_release);
LOG_DEBUG(Lib_AudioOut,
"Set combined audio gain to {:.3f} (channel: {:.3f}, slider: {:.3f})",
total_gain, max_channel_gain, slider_gain);
} else {
LOG_ERROR(Lib_AudioOut, "Failed to set audio stream gain: {}", SDL_GetError());
}
}
u64 GetLastOutputTime() const {
return last_output_time.load(std::memory_order_acquire);
}
private:
bool Initialize(OrbisAudioOutPort type) {
// Calculate timing parameters
period_us = (1000000ULL * buffer_frames + sample_rate / 2) / sample_rate;
// Allocate aligned internal buffer for SIMD operations
internal_buffer_size = buffer_frames * sizeof(float) * num_channels;
#ifdef _WIN32
internal_buffer = _aligned_malloc(internal_buffer_size, AUDIO_BUFFER_ALIGNMENT);
#else
if (posix_memalign(&internal_buffer, AUDIO_BUFFER_ALIGNMENT, internal_buffer_size) != 0) {
internal_buffer = nullptr;
}
#endif
if (!internal_buffer) {
LOG_ERROR(Lib_AudioOut, "Failed to allocate aligned audio buffer of size {}",
internal_buffer_size);
return false;
}
// Initialize current gain
current_gain.store(Config::getVolumeSlider() * 0.01f, std::memory_order_relaxed);
if (!SelectConverter()) {
FreeAlignedBuffer();
return false;
}
// Open SDL device
if (!OpenDevice(type)) {
FreeAlignedBuffer();
return false;
}
CalculateQueueThreshold();
return true;
}
void Cleanup() {
if (stream) {
SDL_DestroyAudioStream(stream);
stream = nullptr;
}
FreeAlignedBuffer();
}
void FreeAlignedBuffer() {
if (internal_buffer) {
#ifdef _WIN32
_aligned_free(internal_buffer);
#else
free(internal_buffer);
#endif
internal_buffer = nullptr;
}
}
void UpdateVolumeIfChanged() {
const u64 current_time = Kernel::sceKernelGetProcessTime();
if (current_time - last_volume_check_time < VOLUME_CHECK_INTERVAL_US) {
return;
}
last_volume_check_time = current_time;
const float config_volume = Config::getVolumeSlider() * 0.01f;
const float stored_gain = current_gain.load(std::memory_order_acquire);
// Only update if the difference is significant
if (std::abs(config_volume - stored_gain) > VOLUME_EPSILON) {
if (SDL_SetAudioStreamGain(stream, config_volume)) {
current_gain.store(config_volume, std::memory_order_release);
LOG_DEBUG(Lib_AudioOut, "Updated audio gain to {:.3f}", config_volume);
} else {
LOG_ERROR(Lib_AudioOut, "Failed to set audio stream gain: {}", SDL_GetError());
}
}
}
void HandleTiming(u64 current_time) {
if (next_output_time == 0) [[unlikely]] {
// First output - set initial timing
next_output_time = current_time + period_us;
return;
}
const s64 time_diff = static_cast<s64>(current_time - next_output_time);
if (time_diff > static_cast<s64>(TIMING_RESYNC_THRESHOLD_US)) [[unlikely]] {
// We're far behind - resync
next_output_time = current_time + period_us;
} else if (time_diff < 0) {
// We're ahead of schedule - wait
const u64 time_to_wait = static_cast<u64>(-time_diff);
next_output_time += period_us;
if (time_to_wait > MIN_SLEEP_THRESHOLD_US) {
// Sleep for most of the wait period
const u64 sleep_duration = time_to_wait - MIN_SLEEP_THRESHOLD_US;
std::this_thread::sleep_for(std::chrono::microseconds(sleep_duration));
}
} else {
// Slightly behind or on time - just advance
next_output_time += period_us;
}
}
void ManageAudioQueue() {
const auto queued = SDL_GetAudioStreamQueued(stream);
if (queued >= queue_threshold) [[unlikely]] {
LOG_DEBUG(Lib_AudioOut, "Clearing backed up audio queue ({} >= {})", queued,
queue_threshold);
SDL_ClearAudioStream(stream);
CalculateQueueThreshold();
}
}
bool OpenDevice(OrbisAudioOutPort type) {
const SDL_AudioSpec fmt = {
.format = SDL_AUDIO_F32LE,
.channels = static_cast<u8>(num_channels),
.freq = static_cast<int>(sample_rate),
};
// Determine device
const std::string device_name = GetDeviceName(type);
const SDL_AudioDeviceID dev_id = SelectAudioDevice(device_name, type);
if (dev_id == SDL_INVALID_AUDIODEVICEID) {
return false;
}
// Create audio stream
stream = SDL_OpenAudioDeviceStream(dev_id, &fmt, nullptr, nullptr);
if (!stream) {
LOG_ERROR(Lib_AudioOut, "Failed to create SDL audio stream: {}", SDL_GetError());
return false;
}
// Configure channel mapping
if (!ConfigureChannelMap()) {
SDL_DestroyAudioStream(stream);
stream = nullptr;
return false;
}
// Set initial volume
const float initial_gain = current_gain.load(std::memory_order_relaxed);
if (!SDL_SetAudioStreamGain(stream, initial_gain)) {
LOG_WARNING(Lib_AudioOut, "Failed to set initial audio gain: {}", SDL_GetError());
}
// Start playback
if (!SDL_ResumeAudioStreamDevice(stream)) {
LOG_ERROR(Lib_AudioOut, "Failed to resume audio stream: {}", SDL_GetError());
SDL_DestroyAudioStream(stream);
stream = nullptr;
return false;
}
LOG_INFO(Lib_AudioOut, "Opened audio device: {} ({} Hz, {} ch, gain: {:.3f})", device_name,
sample_rate, num_channels, initial_gain);
return true;
}
SDL_AudioDeviceID SelectAudioDevice(const std::string& device_name, OrbisAudioOutPort type) {
if (device_name == "None") {
LOG_INFO(Lib_AudioOut, "Audio device disabled for port type {}",
static_cast<int>(type));
return SDL_INVALID_AUDIODEVICEID;
}
if (device_name.empty() || device_name == "Default Device") {
return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
}
// Search for specific device
int num_devices = 0;
SDL_AudioDeviceID* dev_array = SDL_GetAudioPlaybackDevices(&num_devices);
if (!dev_array) {
LOG_WARNING(Lib_AudioOut, "No audio devices found, using default");
return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
}
SDL_AudioDeviceID selected_device = SDL_INVALID_AUDIODEVICEID;
for (int i = 0; i < num_devices; i++) {
const char* dev_name = SDL_GetAudioDeviceName(dev_array[i]);
if (dev_name && device_name == dev_name) {
selected_device = dev_array[i];
break;
}
}
SDL_free(dev_array);
if (selected_device == SDL_INVALID_AUDIODEVICEID) {
LOG_WARNING(Lib_AudioOut, "Audio device '{}' not found, using default", device_name);
return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
}
return selected_device;
}
bool ConfigureChannelMap() {
if (num_channels == 0) {
return true;
}
std::vector<int> channel_map(num_channels);
if (is_std && num_channels == 8) {
// Standard 8CH layout requires remapping
channel_map = {FL, FR, FC, LF, STD_SL, STD_SR, STD_BL, STD_BR};
} else {
std::copy_n(channel_layout.begin(), num_channels, channel_map.begin());
}
if (!SDL_SetAudioStreamInputChannelMap(stream, channel_map.data(), num_channels)) {
LOG_ERROR(Lib_AudioOut, "Failed to set channel map: {}", SDL_GetError());
return false;
}
return true;
}
std::string GetDeviceName(OrbisAudioOutPort type) const {
switch (type) {
case OrbisAudioOutPort::Main:
case OrbisAudioOutPort::Bgm:
return Config::getMainOutputDevice();
case OrbisAudioOutPort::PadSpk:
return Config::getPadSpkOutputDevice();
default:
return Config::getMainOutputDevice();
}
}
bool SelectConverter() {
if (is_float) {
switch (num_channels) {
case 1:
convert = &ConvertF32Mono;
break;
case 2:
convert = &ConvertF32Stereo;
break;
case 8:
convert = is_std ? &ConvertF32Std8CH : &ConvertF32_8CH;
break;
default:
LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels);
return false;
}
} else {
switch (num_channels) {
case 1:
convert = &ConvertS16Mono;
break;
case 2:
#if defined(HAS_SSE2)
convert = &ConvertS16StereoSIMD;
#else
convert = &ConvertS16Stereo;
#endif
break;
case 8:
#if defined(HAS_SSE2)
convert = &ConvertS16_8CH_SIMD;
#else
convert = &ConvertS16_8CH;
#endif
break;
default:
LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels);
return false;
}
}
return true;
}
void CalculateQueueThreshold() {
if (!stream) {
return;
}
SDL_AudioSpec discard;
int sdl_buffer_frames = 0;
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(stream), &discard,
&sdl_buffer_frames)) {
LOG_WARNING(Lib_AudioOut, "Failed to get SDL buffer size: {}", SDL_GetError());
}
const u32 sdl_buffer_size = sdl_buffer_frames * sizeof(float) * num_channels;
queue_threshold = std::max(guest_buffer_size, sdl_buffer_size) * QUEUE_MULTIPLIER;
LOG_DEBUG(Lib_AudioOut, "Audio queue threshold: {} bytes (SDL buffer: {} frames)",
queue_threshold, sdl_buffer_frames);
}
using ConverterFunc = void (*)(const void* src, void* dst, u32 frames, const float* volumes);
static void ConvertS16Mono(const void* src, void* dst, u32 frames, const float*) {
const s16* s = static_cast<const s16*>(src);
float* d = static_cast<float*>(dst);
for (u32 i = 0; i < frames; i++) {
d[i] = s[i] * INV_VOLUME_0DB;
}
}
static void ConvertS16Stereo(const void* src, void* dst, u32 frames, const float*) {
const s16* s = static_cast<const s16*>(src);
float* d = static_cast<float*>(dst);
const u32 num_samples = frames << 1; // * 2
for (u32 i = 0; i < num_samples; i++) {
d[i] = s[i] * INV_VOLUME_0DB;
}
}
#ifdef HAS_SSE2
static void ConvertS16StereoSIMD(const void* src, void* dst, u32 frames, const float*) {
const s16* s = static_cast<const s16*>(src);
float* d = static_cast<float*>(dst);
const __m128 scale = _mm_set1_ps(INV_VOLUME_0DB);
const u32 num_samples = frames << 1;
u32 i = 0;
// Process 8 samples at a time (4 stereo frames)
for (; i + 8 <= num_samples; i += 8) {
// Load 8 s16 values
__m128i s16_vals = _mm_loadu_si128(reinterpret_cast<const __m128i*>(&s[i]));
// Convert to 32-bit integers
__m128i s32_lo = _mm_cvtepi16_epi32(s16_vals);
__m128i s32_hi = _mm_cvtepi16_epi32(_mm_srli_si128(s16_vals, 8));
// Convert to float and scale
__m128 f_lo = _mm_mul_ps(_mm_cvtepi32_ps(s32_lo), scale);
__m128 f_hi = _mm_mul_ps(_mm_cvtepi32_ps(s32_hi), scale);
// Store results
_mm_storeu_ps(&d[i], f_lo);
_mm_storeu_ps(&d[i + 4], f_hi);
}
// Handle remaining samples
for (; i < num_samples; i++) {
d[i] = s[i] * INV_VOLUME_0DB;
}
}
#endif
static void ConvertS16_8CH(const void* src, void* dst, u32 frames, const float*) {
const s16* s = static_cast<const s16*>(src);
float* d = static_cast<float*>(dst);
const u32 num_samples = frames << 3; // * 8
for (u32 i = 0; i < num_samples; i++) {
d[i] = s[i] * INV_VOLUME_0DB;
}
}
#ifdef HAS_SSE2
static void ConvertS16_8CH_SIMD(const void* src, void* dst, u32 frames, const float*) {
const s16* s = static_cast<const s16*>(src);
float* d = static_cast<float*>(dst);
const __m128 scale = _mm_set1_ps(INV_VOLUME_0DB);
const u32 num_samples = frames << 3;
u32 i = 0;
// Process 8 samples at a time
for (; i + 8 <= num_samples; i += 8) {
__m128i s16_vals = _mm_loadu_si128(reinterpret_cast<const __m128i*>(&s[i]));
__m128i s32_lo = _mm_cvtepi16_epi32(s16_vals);
__m128i s32_hi = _mm_cvtepi16_epi32(_mm_srli_si128(s16_vals, 8));
__m128 f_lo = _mm_mul_ps(_mm_cvtepi32_ps(s32_lo), scale);
__m128 f_hi = _mm_mul_ps(_mm_cvtepi32_ps(s32_hi), scale);
_mm_storeu_ps(&d[i], f_lo);
_mm_storeu_ps(&d[i + 4], f_hi);
}
for (; i < num_samples; i++) {
d[i] = s[i] * INV_VOLUME_0DB;
}
}
#endif
static void ConvertF32Mono(const void* src, void* dst, u32 frames, const float*) {
std::memcpy(dst, src, frames * sizeof(float));
}
static void ConvertF32Stereo(const void* src, void* dst, u32 frames, const float*) {
std::memcpy(dst, src, frames * 2 * sizeof(float));
}
static void ConvertF32_8CH(const void* src, void* dst, u32 frames, const float*) {
std::memcpy(dst, src, frames * 8 * sizeof(float));
}
static void ConvertF32Std8CH(const void* src, void* dst, u32 frames, const float*) {
const float* s = static_cast<const float*>(src);
float* d = static_cast<float*>(dst);
// Channel remapping for standard 8CH layout
for (u32 i = 0; i < frames; i++) {
const u32 offset = i << 3; // * 8
d[offset + FL] = s[offset + FL];
d[offset + FR] = s[offset + FR];
d[offset + FC] = s[offset + FC];
d[offset + LF] = s[offset + LF];
d[offset + SL] = s[offset + STD_SL];
d[offset + SR] = s[offset + STD_SR];
d[offset + BL] = s[offset + STD_BL];
d[offset + BR] = s[offset + STD_BR];
}
}
// Audio format parameters
const u32 frame_size;
const u32 guest_buffer_size;
const u32 buffer_frames;
const u32 sample_rate;
const u32 num_channels;
const bool is_float;
const bool is_std;
const std::array<int, 8> channel_layout;
alignas(64) u64 period_us{0};
alignas(64) std::atomic<u64> last_output_time{0};
u64 next_output_time{0};
u64 last_volume_check_time{0};
u32 output_count{0};
// Buffers
u32 internal_buffer_size{0};
void* internal_buffer{nullptr};
// Converter function pointer
ConverterFunc convert{nullptr};
// Volume management
alignas(64) std::atomic<float> current_gain{1.0f};
// SDL audio stream
SDL_AudioStream* stream{nullptr};
u32 queue_threshold{0};
};
std::unique_ptr<PortBackend> SDLAudioOut::Open(PortOut& port) {
return std::make_unique<SDLPortBackend>(port);
}
} // namespace Libraries::AudioOut

View File

@ -1,139 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <common/config.h>
#include <common/logging/log.h>
#include "sdl_in.h"
int SDLAudioIn::AudioInit() {
return SDL_InitSubSystem(SDL_INIT_AUDIO);
}
int SDLAudioIn::AudioInOpen(int type, uint32_t samples_num, uint32_t freq, uint32_t format) {
std::scoped_lock lock{m_mutex};
for (int id = 0; id < static_cast<int>(portsIn.size()); ++id) {
auto& port = portsIn[id];
if (!port.isOpen) {
port.isOpen = true;
port.type = type;
port.samples_num = samples_num;
port.freq = freq;
port.format = format;
SDL_AudioFormat sampleFormat;
switch (format) {
case Libraries::AudioIn::ORBIS_AUDIO_IN_PARAM_FORMAT_S16_MONO:
sampleFormat = SDL_AUDIO_S16;
port.channels_num = 1;
port.sample_size = 2;
break;
case Libraries::AudioIn::ORBIS_AUDIO_IN_PARAM_FORMAT_S16_STEREO:
sampleFormat = SDL_AUDIO_S16;
port.channels_num = 2;
port.sample_size = 2;
break;
default:
port.isOpen = false;
return ORBIS_AUDIO_IN_ERROR_INVALID_PORT;
}
SDL_AudioSpec fmt;
SDL_zero(fmt);
fmt.format = sampleFormat;
fmt.channels = port.channels_num;
fmt.freq = port.freq;
std::string micDevStr = Config::getMicDevice();
uint32_t devId;
bool nullDevice = false;
if (micDevStr == "None") {
nullDevice = true;
} else if (micDevStr == "Default Device") {
devId = SDL_AUDIO_DEVICE_DEFAULT_RECORDING;
} else {
try {
devId = static_cast<uint32_t>(std::stoul(micDevStr));
} catch (const std::exception& e) {
nullDevice = true;
}
}
port.stream =
nullDevice ? nullptr : SDL_OpenAudioDeviceStream(devId, &fmt, nullptr, nullptr);
if (!port.stream) {
// if stream is null, either due to configuration disabling the input,
// or no input devices present in the system, still return a valid id
// as some games require that (e.g. L.A. Noire)
return id + 1;
}
if (SDL_ResumeAudioStreamDevice(port.stream) == false) {
SDL_DestroyAudioStream(port.stream);
port = {};
return ORBIS_AUDIO_IN_ERROR_STREAM_FAIL;
}
return id + 1;
}
}
return ORBIS_AUDIO_IN_ERROR_INVALID_PORT;
}
int SDLAudioIn::AudioInInput(int handle, void* out_buffer) {
std::scoped_lock lock{m_mutex};
if (handle < 1 || handle > static_cast<int>(portsIn.size()) || !out_buffer)
return ORBIS_AUDIO_IN_ERROR_INVALID_PORT;
auto& port = portsIn[handle - 1];
if (!port.isOpen)
return ORBIS_AUDIO_IN_ERROR_INVALID_PORT;
const int bytesToRead = port.samples_num * port.sample_size * port.channels_num;
if (out_buffer == nullptr) {
int attempts = 0;
while (SDL_GetAudioStreamAvailable(port.stream) > 0) {
SDL_Delay(1);
if (++attempts > 1000) {
return ORBIS_AUDIO_IN_ERROR_TIMEOUT;
}
}
return 0; // done
}
int attempts = 0;
while (SDL_GetAudioStreamAvailable(port.stream) < bytesToRead) {
SDL_Delay(1);
if (++attempts > 1000) {
return ORBIS_AUDIO_IN_ERROR_TIMEOUT;
}
}
const int bytesRead = SDL_GetAudioStreamData(port.stream, out_buffer, bytesToRead);
if (bytesRead < 0) {
// SDL_GetAudioStreamData failed
LOG_ERROR(Lib_AudioIn, "AudioInInput error: {}", SDL_GetError());
return ORBIS_AUDIO_IN_ERROR_STREAM_FAIL;
}
const int framesRead = bytesRead / (port.sample_size * port.channels_num);
return framesRead;
}
void SDLAudioIn::AudioInClose(int handle) {
std::scoped_lock lock{m_mutex};
if (handle < 1 || handle > (int)portsIn.size())
return;
auto& port = portsIn[handle - 1];
if (!port.isOpen)
return;
SDL_DestroyAudioStream(port.stream);
port = {};
}

View File

@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <mutex>
#include <SDL3/SDL.h>
namespace Libraries::AudioIn {
enum OrbisAudioInParam {
ORBIS_AUDIO_IN_PARAM_FORMAT_S16_MONO = 0,
ORBIS_AUDIO_IN_PARAM_FORMAT_S16_STEREO = 2
};
}
#define ORBIS_AUDIO_IN_ERROR_INVALID_PORT -1
#define ORBIS_AUDIO_IN_ERROR_TIMEOUT -2
#define ORBIS_AUDIO_IN_ERROR_STREAM_FAIL -3
class SDLAudioIn {
public:
int AudioInit();
int AudioInOpen(int type, uint32_t samples_num, uint32_t freq, uint32_t format);
int AudioInInput(int handle, void* out_buffer);
void AudioInClose(int handle);
private:
struct AudioInPort {
bool isOpen = false;
int type = 0;
uint32_t samples_num = 0;
uint32_t freq = 0;
int channels_num = 0;
int sample_size = 0;
uint32_t format = 0;
SDL_AudioStream* stream = nullptr;
};
std::array<AudioInPort, 8> portsIn;
std::mutex m_mutex;
};

View File

@ -29,10 +29,10 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) {
return AudioOut::sceAudioOutClose(handle);
}
s32 PS4_SYSV_ABI
sceAudio3dAudioOutOpen(const OrbisAudio3dPortId port_id, const OrbisUserServiceUserId user_id,
s32 type, const s32 index, const u32 len, const u32 freq,
const AudioOut::OrbisAudioOutParamExtendedInformation param) {
s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(
const OrbisAudio3dPortId port_id, const Libraries::UserService::OrbisUserServiceUserId user_id,
s32 type, const s32 index, const u32 len, const u32 freq,
const AudioOut::OrbisAudioOutParamExtendedInformation param) {
LOG_INFO(Lib_Audio3d,
"called, port_id = {}, user_id = {}, type = {}, index = {}, len = {}, freq = {}",
port_id, user_id, type, index, len, freq);
@ -422,7 +422,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetStatus() {
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceAudio3dPortOpen(const OrbisUserServiceUserId user_id,
s32 PS4_SYSV_ABI sceAudio3dPortOpen(const Libraries::UserService::OrbisUserServiceUserId user_id,
const OrbisAudio3dOpenParameters* parameters,
OrbisAudio3dPortId* port_id) {
LOG_INFO(Lib_Audio3d, "called, user_id = {}, parameters = {}, id = {}", user_id,

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -15,8 +15,6 @@ class SymbolsResolver;
namespace Libraries::Audio3d {
using OrbisUserServiceUserId = s32;
enum class OrbisAudio3dRate : u32 {
ORBIS_AUDIO3D_RATE_48000 = 0,
};
@ -91,7 +89,8 @@ struct Audio3dState {
};
s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(s32 handle);
s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id, OrbisUserServiceUserId user_id,
s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id,
Libraries::UserService::OrbisUserServiceUserId user_id,
s32 type, s32 index, u32 len, u32 freq,
AudioOut::OrbisAudioOutParamExtendedInformation param);
s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(s32 handle, void* ptr);
@ -127,7 +126,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(OrbisAudio3dPortId port_id, u32* qu
u32* queue_available);
s32 PS4_SYSV_ABI sceAudio3dPortGetState();
s32 PS4_SYSV_ABI sceAudio3dPortGetStatus();
s32 PS4_SYSV_ABI sceAudio3dPortOpen(OrbisUserServiceUserId user_id,
s32 PS4_SYSV_ABI sceAudio3dPortOpen(Libraries::UserService::OrbisUserServiceUserId user_id,
const OrbisAudio3dOpenParameters* parameters,
OrbisAudio3dPortId* port_id);
s32 PS4_SYSV_ABI sceAudio3dPortPush(OrbisAudio3dPortId port_id, OrbisAudio3dBlocking blocking);

View File

@ -30,7 +30,15 @@ AvPlayerSourceType GetSourceType(std::string_view path) {
}
// schema://server.domain/path/to/file.ext/and/beyond -> .ext/and/beyond
auto ext = name.substr(name.rfind('.'));
// Find extension dot
auto dot_pos = name.rfind('.');
if (dot_pos == std::string_view::npos) {
return AvPlayerSourceType::Unknown;
}
// Extract extension (".ext/anything" or ".ext")
auto ext = name.substr(dot_pos);
if (ext.empty()) {
return AvPlayerSourceType::Unknown;
}

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
#include "common/logging/log.h"
@ -16,7 +16,7 @@ s32 PS4_SYSV_ABI sceCompanionHttpdAddHeader(const char* key, const char* value,
}
s32 PS4_SYSV_ABI
sceCompanionHttpdGet2ndScreenStatus(Libraries::UserService::OrbisUserServiceUserId) {
sceCompanionHttpdGet2ndScreenStatus(Libraries::UserService::OrbisUserServiceUserId userId) {
LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called");
return ORBIS_OK;
}

View File

@ -29,10 +29,9 @@ u32 PS4_SYSV_ABI getEvent(sceCompanionUtilContext* ctx, sceCompanionUtilEvent* o
}
s32 PS4_SYSV_ABI sceCompanionUtilGetEvent(sceCompanionUtilEvent* outEvent) {
sceCompanionUtilContext* ctx = nullptr;
u32 ret = getEvent(ctx, outEvent, 1);
u32 ret = ORBIS_COMPANION_UTIL_NO_EVENT;
LOG_DEBUG(Lib_CompanionUtil, "(STUBBED) called ret: {}", ret);
LOG_DEBUG(Lib_CompanionUtil, "(STUBBED) called ret: {:#x}", ret);
return ret;
}

View File

@ -1,9 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
#include "core/libraries/system/userservice.h"
namespace Core::Loader {
class SymbolsResolver;
@ -15,11 +16,11 @@ struct OrbisGameLiveStreamingStatus {
bool isOnAir;
u8 align[3];
u32 spectatorCounts;
s32 userId;
Libraries::UserService::OrbisUserServiceUserId userId;
u8 reserved[60];
};
struct OrbisGameLiveStreamingStatus2 {
s32 userId;
Libraries::UserService::OrbisUserServiceUserId userId;
bool isOnAir;
u8 align[3];
u32 spectatorCounts;

View File

@ -115,9 +115,199 @@ Error PS4_SYSV_ABI sceImeDialogGetPanelSize(const OrbisImeDialogParam* param, u3
return Error::OK;
}
int PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended() {
LOG_ERROR(Lib_ImeDialog, "(STUBBED) called");
return ORBIS_OK;
Error PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(const OrbisImeDialogParam* param,
const OrbisImeParamExtended* extended,
u32* width, u32* height) {
if (!param || !width || !height) {
return Error::INVALID_ADDRESS;
}
// Check parameter bounds
if (static_cast<uint32_t>(param->type) > 4) {
return Error::INVALID_ARG;
}
if (extended) {
// Check panel priority for full panel mode (Accent = 3)
if (extended->priority == OrbisImePanelPriority::Accent) {
// Full panel mode - return maximum size
if ((param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) !=
OrbisImeOption::DEFAULT) {
*width = 2560; // For 4K/5K displays
*height = 1440;
} else {
*width = 1920;
*height = 1080;
}
LOG_DEBUG(Lib_ImeDialog, "Full panel mode: width={}, height={}", *width, *height);
return Error::OK;
}
}
// First get the base panel size from the basic function
Error result = sceImeDialogGetPanelSize(param, width, height);
if (result != Error::OK) {
return result;
}
// Adjust based on IME type
switch (param->type) {
case OrbisImeType::Default:
case OrbisImeType::BasicLatin:
case OrbisImeType::Url:
case OrbisImeType::Mail:
// Standard IME types
if ((param->option & OrbisImeOption::PASSWORD) != OrbisImeOption::DEFAULT) {
*height = *height + 20;
}
if ((param->option & OrbisImeOption::MULTILINE) != OrbisImeOption::DEFAULT) {
*height = *height * 3 / 2;
}
break;
case OrbisImeType::Number:
*width = *width * 3 / 4;
*height = *height * 2 / 3;
break;
default:
// Unknown type, use default size
break;
}
// Apply extended options if provided
if (extended) {
// Handle extended option flags
if ((extended->option & OrbisImeExtOption::PRIORITY_FULL_WIDTH) !=
OrbisImeExtOption::DEFAULT) {
// Full width priority
bool use_2k = (param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) !=
OrbisImeOption::DEFAULT;
*width = use_2k ? 1200 : 800;
LOG_DEBUG(Lib_ImeDialog, "Full width priority: width={}", *width);
}
if ((extended->option & OrbisImeExtOption::PRIORITY_FIXED_PANEL) !=
OrbisImeExtOption::DEFAULT) {
// Fixed panel size
*width = 600;
*height = 400;
LOG_DEBUG(Lib_ImeDialog, "Fixed panel: width={}, height={}", *width, *height);
}
switch (extended->priority) {
case OrbisImePanelPriority::Alphabet:
*width = 600;
*height = 400;
break;
case OrbisImePanelPriority::Symbol:
*width = 500;
*height = 300;
break;
case OrbisImePanelPriority::Accent:
// Already handled
break;
case OrbisImePanelPriority::Default:
default:
// Use calculated sizes
break;
}
if ((extended->option & OrbisImeExtOption::INIT_EXT_KEYBOARD_MODE) !=
OrbisImeExtOption::DEFAULT) {
if (extended->ext_keyboard_mode != 0) {
// Check for high-res mode flags
if ((extended->ext_keyboard_mode &
static_cast<uint32_t>(
OrbisImeInitExtKeyboardMode::INPUT_METHOD_STATE_FULL_WIDTH)) != 0) {
*width = *width * 5 / 4;
}
// Check for format characters enabled
if ((extended->ext_keyboard_mode &
static_cast<uint32_t>(
OrbisImeInitExtKeyboardMode::ENABLE_FORMAT_CHARACTERS)) != 0) {
*height = *height + 30;
}
}
}
// Check for accessibility mode
if ((extended->option & OrbisImeExtOption::ENABLE_ACCESSIBILITY) !=
OrbisImeExtOption::DEFAULT) {
*width = *width * 5 / 4; // 25% larger for accessibility
*height = *height * 5 / 4;
LOG_DEBUG(Lib_ImeDialog, "Accessibility mode: width={}, height={}", *width, *height);
}
// Check for forced accessibility panel
if ((extended->option & OrbisImeExtOption::ACCESSIBILITY_PANEL_FORCED) !=
OrbisImeExtOption::DEFAULT) {
*width = 800;
*height = 600;
LOG_DEBUG(Lib_ImeDialog, "Forced accessibility panel: width={}, height={}", *width,
*height);
}
}
if ((param->option & static_cast<OrbisImeOption>(0x8)) != OrbisImeOption::DEFAULT) { //?
*width *= 2;
*height *= 2;
LOG_DEBUG(Lib_ImeDialog, "Size mode: width={}, height={}", *width, *height);
}
// Adjust for supported languages if specified
if (param->supported_languages != static_cast<OrbisImeLanguage>(0)) {
// Check if CJK languages are supported (need larger panel)
OrbisImeLanguage cjk_mask = OrbisImeLanguage::JAPANESE | OrbisImeLanguage::KOREAN |
OrbisImeLanguage::SIMPLIFIED_CHINESE |
OrbisImeLanguage::TRADITIONAL_CHINESE;
if ((param->supported_languages & cjk_mask) != static_cast<OrbisImeLanguage>(0)) {
*width = *width * 5 / 4; // 25% wider for CJK input
*height = *height * 6 / 5; // 20% taller
LOG_DEBUG(Lib_ImeDialog, "CJK language support: width={}, height={}", *width, *height);
}
// Check if Arabic is supported (right-to-left layout)
if ((param->supported_languages & OrbisImeLanguage::ARABIC) !=
static_cast<OrbisImeLanguage>(0)) {
*width = *width * 11 / 10; // 10% wider for Arabic
LOG_DEBUG(Lib_ImeDialog, "Arabic language support: width={}", *width);
}
}
// Ensure minimum sizes
const uint32_t min_width = 200;
const uint32_t min_height = 100;
if (*width < min_width)
*width = min_width;
if (*height < min_height)
*height = min_height;
// Ensure maximum sizes (don't exceed screen bounds)
bool use_2k_coords =
(param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT;
const uint32_t max_width = use_2k_coords ? 2560 : 1920;
const uint32_t max_height = use_2k_coords ? 1440 : 1080;
if (*width > max_width)
*width = max_width;
if (*height > max_height)
*height = max_height;
// Check for fixed position option
if ((param->option & OrbisImeOption::FIXED_POSITION) != OrbisImeOption::DEFAULT) {
if (*width > 800)
*width = 800;
if (*height > 600)
*height = 600;
}
LOG_DEBUG(Lib_ImeDialog, "Final panel size: width={}, height={}", *width, *height);
return Error::OK;
}
Error PS4_SYSV_ABI sceImeDialogGetResult(OrbisImeDialogResult* result) {

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
@ -37,7 +37,9 @@ int PS4_SYSV_ABI sceImeDialogGetCurrentStarState();
int PS4_SYSV_ABI sceImeDialogGetPanelPositionAndForm();
Error PS4_SYSV_ABI sceImeDialogGetPanelSize(const OrbisImeDialogParam* param, u32* width,
u32* height);
int PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended();
Error PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(const OrbisImeDialogParam* param,
const OrbisImeParamExtended* extended,
u32* width, u32* height);
Error PS4_SYSV_ABI sceImeDialogGetResult(OrbisImeDialogResult* result);
OrbisImeDialogStatus PS4_SYSV_ABI sceImeDialogGetStatus();
Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExtended* extended);

View File

@ -34,6 +34,7 @@
#include <winsock2.h>
#else
#include <sys/select.h>
#include <sys/stat.h>
#endif
namespace D = Core::Devices;
@ -751,6 +752,30 @@ s32 PS4_SYSV_ABI fstat(s32 fd, OrbisKernelStat* sb) {
sb->st_size = file->f.GetSize();
sb->st_blksize = 512;
sb->st_blocks = (sb->st_size + 511) / 512;
#if defined(__linux__) || defined(__FreeBSD__)
struct stat filestat = {};
stat(file->f.GetPath().c_str(), &filestat);
sb->st_atim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_atim);
sb->st_mtim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_mtim);
sb->st_ctim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_ctim);
#elif defined(__APPLE__)
struct stat filestat = {};
stat(file->f.GetPath().c_str(), &filestat);
sb->st_atim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_atimespec);
sb->st_mtim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_mtimespec);
sb->st_ctim = *reinterpret_cast<OrbisKernelTimespec*>(&filestat.st_ctimespec);
#else
const auto ft = std::filesystem::last_write_time(file->f.GetPath());
const auto sctp = std::chrono::time_point_cast<std::chrono::nanoseconds>(
ft - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now());
const auto secs = std::chrono::time_point_cast<std::chrono::seconds>(sctp);
const auto nsecs = std::chrono::duration_cast<std::chrono::nanoseconds>(sctp - secs);
sb->st_mtim.tv_sec = static_cast<int64_t>(secs.time_since_epoch().count());
sb->st_mtim.tv_nsec = static_cast<int64_t>(nsecs.count());
sb->st_atim = sb->st_mtim;
sb->st_ctim = sb->st_mtim;
#endif
// TODO incomplete
break;
}
@ -1262,7 +1287,8 @@ s32 PS4_SYSV_ABI posix_select(s32 nfds, fd_set_posix* readfds, fd_set_posix* wri
if (file->type == Core::FileSys::FileType::Regular ||
file->type == Core::FileSys::FileType::Device) {
// Disk files always ready
if (want_read) {
// For devices, stdin (fd 0) is never read-ready.
if (want_read && i != 0) {
FD_SET_POSIX(i, &read_ready);
}
if (want_write) {

View File

@ -65,6 +65,9 @@ constexpr int ORBIS_KERNEL_O_DSYNC = 0x1000;
constexpr int ORBIS_KERNEL_O_DIRECT = 0x00010000;
constexpr int ORBIS_KERNEL_O_DIRECTORY = 0x00020000;
s32 PS4_SYSV_ABI posix_open(const char* path, s32 flags, u16 mode);
s32 PS4_SYSV_ABI posix_close(s32 fd);
s64 PS4_SYSV_ABI posix_lseek(s32 fd, s64 offset, s32 whence);
s64 PS4_SYSV_ABI sceKernelWrite(s32 fd, const void* buf, u64 nbytes);
s64 PS4_SYSV_ABI sceKernelRead(s32 fd, void* buf, u64 nbytes);
s64 PS4_SYSV_ABI sceKernelPread(s32 fd, void* buf, u64 nbytes, s64 offset);

View File

@ -89,21 +89,31 @@ s32 PS4_SYSV_ABI sceKernelAllocateMainDirectMemory(u64 len, u64 alignment, s32 m
}
s32 PS4_SYSV_ABI sceKernelCheckedReleaseDirectMemory(u64 start, u64 len) {
LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len);
if (!Common::Is16KBAligned(start) || !Common::Is16KBAligned(len)) {
LOG_ERROR(Kernel_Vmm, "Misaligned start or length, start = {:#x}, length = {:#x}", start,
len);
return ORBIS_KERNEL_ERROR_EINVAL;
}
if (len == 0) {
return ORBIS_OK;
}
LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len);
auto* memory = Core::Memory::Instance();
memory->Free(start, len);
return ORBIS_OK;
return memory->Free(start, len, true);
}
s32 PS4_SYSV_ABI sceKernelReleaseDirectMemory(u64 start, u64 len) {
LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len);
if (!Common::Is16KBAligned(start) || !Common::Is16KBAligned(len)) {
LOG_ERROR(Kernel_Vmm, "Misaligned start or length, start = {:#x}, length = {:#x}", start,
len);
return ORBIS_KERNEL_ERROR_EINVAL;
}
if (len == 0) {
return ORBIS_OK;
}
auto* memory = Core::Memory::Instance();
memory->Free(start, len);
memory->Free(start, len, false);
return ORBIS_OK;
}

View File

@ -191,6 +191,26 @@ s32 PS4_SYSV_ABI sceKernelGetModuleInfo(s32 handle, Core::OrbisKernelModuleInfo*
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceKernelGetModuleInfo2(s32 handle, Core::OrbisKernelModuleInfo* info) {
if (info == nullptr) {
return ORBIS_KERNEL_ERROR_EFAULT;
}
if (info->st_size != sizeof(Core::OrbisKernelModuleInfo)) {
return ORBIS_KERNEL_ERROR_EINVAL;
}
auto* linker = Common::Singleton<Core::Linker>::Instance();
auto* module = linker->GetModule(handle);
if (module == nullptr) {
return ORBIS_KERNEL_ERROR_ESRCH;
}
if (module->IsSystemLib()) {
return ORBIS_KERNEL_ERROR_EPERM;
}
*info = module->GetModuleInfo();
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceKernelGetModuleInfoInternal(s32 handle, Core::OrbisKernelModuleInfoEx* info) {
if (info == nullptr) {
return ORBIS_KERNEL_ERROR_EFAULT;
@ -230,6 +250,31 @@ s32 PS4_SYSV_ABI sceKernelGetModuleList(s32* handles, u64 num_array, u64* out_co
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceKernelGetModuleList2(s32* handles, u64 num_array, u64* out_count) {
if (handles == nullptr || out_count == nullptr) {
return ORBIS_KERNEL_ERROR_EFAULT;
}
auto* linker = Common::Singleton<Core::Linker>::Instance();
u64 id = 0;
u64 index = 0;
auto* module = linker->GetModule(id);
while (module != nullptr && index < num_array) {
if (!module->IsSystemLib()) {
handles[index++] = id;
}
id++;
module = linker->GetModule(id);
}
if (index == num_array && module != nullptr) {
return ORBIS_KERNEL_ERROR_ENOMEM;
}
*out_count = index;
return ORBIS_OK;
}
s32 PS4_SYSV_ABI exit(s32 status) {
UNREACHABLE_MSG("Exiting with status code {}", status);
return 0;
@ -249,8 +294,11 @@ void RegisterProcess(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("RpQJJVKTiFM", "libkernel", 1, "libkernel", sceKernelGetModuleInfoForUnwind);
LIB_FUNCTION("f7KBOafysXo", "libkernel", 1, "libkernel", sceKernelGetModuleInfoFromAddr);
LIB_FUNCTION("kUpgrXIrz7Q", "libkernel", 1, "libkernel", sceKernelGetModuleInfo);
LIB_FUNCTION("QgsKEUfkqMA", "libkernel", 1, "libkernel", sceKernelGetModuleInfo2);
LIB_FUNCTION("QgsKEUfkqMA", "libkernel_module_info", 1, "libkernel", sceKernelGetModuleInfo2);
LIB_FUNCTION("HZO7xOos4xc", "libkernel", 1, "libkernel", sceKernelGetModuleInfoInternal);
LIB_FUNCTION("IuxnUuXk6Bg", "libkernel", 1, "libkernel", sceKernelGetModuleList);
LIB_FUNCTION("ZzzC3ZGVAkc", "libkernel", 1, "libkernel", sceKernelGetModuleList2);
LIB_FUNCTION("6Z83sYWFlA8", "libkernel", 1, "libkernel", exit);
}

View File

@ -28,6 +28,16 @@ int PS4_SYSV_ABI posix_pthread_create(PthreadT* thread, const PthreadAttrT* attr
int PS4_SYSV_ABI posix_pthread_join(PthreadT pthread, void** thread_return);
int PS4_SYSV_ABI posix_pthread_mutexattr_init(PthreadMutexAttrT* attr);
int PS4_SYSV_ABI posix_pthread_mutexattr_settype(PthreadMutexAttrT* attr, PthreadMutexType type);
int PS4_SYSV_ABI posix_pthread_mutexattr_destroy(PthreadMutexAttrT* attr);
int PS4_SYSV_ABI scePthreadMutexInit(PthreadMutexT* mutex, const PthreadMutexAttrT* mutex_attr,
const char* name);
int PS4_SYSV_ABI posix_pthread_mutex_lock(PthreadMutexT* mutex);
int PS4_SYSV_ABI posix_pthread_mutex_unlock(PthreadMutexT* mutex);
int PS4_SYSV_ABI posix_pthread_mutex_destroy(PthreadMutexT* mutex);
void RegisterThreads(Core::Loader::SymbolsResolver* sym);
class Thread {

View File

@ -261,7 +261,6 @@ int PS4_SYSV_ABI posix_pthread_mutex_lock(PthreadMutexT* mutex) {
int PS4_SYSV_ABI posix_pthread_mutex_timedlock(PthreadMutexT* mutex,
const OrbisKernelTimespec* abstime) {
CHECK_AND_INIT_MUTEX
UNREACHABLE();
return (*mutex)->Lock(abstime);
}
@ -378,7 +377,8 @@ int PS4_SYSV_ABI posix_pthread_mutexattr_getkind_np(PthreadMutexAttrT attr) {
}
int PS4_SYSV_ABI posix_pthread_mutexattr_settype(PthreadMutexAttrT* attr, PthreadMutexType type) {
if (attr == nullptr || *attr == nullptr || type >= PthreadMutexType::Max) {
if (attr == nullptr || *attr == nullptr || type < PthreadMutexType::ErrorCheck ||
type >= PthreadMutexType::Max) {
return POSIX_EINVAL;
}
(*attr)->m_type = type;

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/assert.h"
@ -14,12 +14,6 @@
namespace Libraries::Kernel {
constexpr int PthreadInheritSched = 4;
constexpr int ORBIS_KERNEL_PRIO_FIFO_DEFAULT = 700;
constexpr int ORBIS_KERNEL_PRIO_FIFO_HIGHEST = 256;
constexpr int ORBIS_KERNEL_PRIO_FIFO_LOWEST = 767;
extern PthreadAttr PthreadAttrDefault;
void _thread_cleanupspecific();
@ -231,7 +225,7 @@ int PS4_SYSV_ABI posix_pthread_create_name_np(PthreadT* thread, const PthreadAtt
new_thread->attr = *(*attr);
new_thread->attr.cpusetsize = 0;
}
if (new_thread->attr.sched_inherit == PthreadInheritSched) {
if (curthread != nullptr && new_thread->attr.sched_inherit == PthreadInheritSched) {
if (True(curthread->attr.flags & PthreadAttrFlags::ScopeSystem)) {
new_thread->attr.flags |= PthreadAttrFlags::ScopeSystem;
} else {
@ -325,6 +319,7 @@ PthreadT PS4_SYSV_ABI posix_pthread_self() {
}
void PS4_SYSV_ABI posix_pthread_set_name_np(PthreadT thread, const char* name) {
LOG_INFO(Kernel_Pthread, "called, new name: {}", name);
Common::SetCurrentThreadName(name);
}

View File

@ -24,6 +24,12 @@ class SymbolsResolver;
namespace Libraries::Kernel {
constexpr int PthreadInheritSched = 4;
constexpr int ORBIS_KERNEL_PRIO_FIFO_DEFAULT = 700;
constexpr int ORBIS_KERNEL_PRIO_FIFO_HIGHEST = 256;
constexpr int ORBIS_KERNEL_PRIO_FIFO_LOWEST = 767;
struct Pthread;
enum class PthreadMutexFlags : u32 {

View File

@ -24,9 +24,9 @@ static constexpr std::array<PthreadPrio, 3> ThrPriorities = {{
}};
PthreadAttr PthreadAttrDefault = {
.sched_policy = SchedPolicy::Fifo,
.sched_inherit = 0,
.prio = 0,
.sched_policy = SchedPolicy::Other,
.sched_inherit = PthreadInheritSched,
.prio = ORBIS_KERNEL_PRIO_FIFO_DEFAULT,
.suspend = false,
.flags = PthreadAttrFlags::ScopeSystem,
.stackaddr_attr = nullptr,

View File

@ -11,6 +11,7 @@
#include "libc_internal_math.h"
#include "libc_internal_memory.h"
#include "libc_internal_str.h"
#include "libc_internal_threads.h"
#include "printf.h"
namespace Libraries::LibcInternal {
@ -20,5 +21,11 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
RegisterlibSceLibcInternalStr(sym);
RegisterlibSceLibcInternalMemory(sym);
RegisterlibSceLibcInternalIo(sym);
RegisterlibSceLibcInternalThreads(sym);
}
void ForceRegisterLib(Core::Loader::SymbolsResolver* sym) {
// Used to forcibly enable HLEs for broken LLE functions.
ForceRegisterlibSceLibcInternalIo(sym);
}
} // namespace Libraries::LibcInternal

View File

@ -15,4 +15,5 @@ namespace Libraries::LibcInternal {
// so everything is just in the .cpp file
void RegisterLib(Core::Loader::SymbolsResolver* sym);
void ForceRegisterLib(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::LibcInternal

View File

@ -3,21 +3,484 @@
#include <cstdarg>
#include <cstdio>
#include <map>
#include <common/va_ctx.h>
#include "common/alignment.h"
#include "common/assert.h"
#include "common/logging/log.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/kernel/file_system.h"
#include "core/libraries/kernel/kernel.h"
#include "core/libraries/kernel/posix_error.h"
#include "core/libraries/libc_internal/libc_internal_io.h"
#include "core/libraries/libc_internal/libc_internal_threads.h"
#include "core/libraries/libs.h"
#include "libc_internal_io.h"
#include "printf.h"
namespace Libraries::LibcInternal {
int PS4_SYSV_ABI internal_snprintf(char* s, size_t n, VA_ARGS) {
s32 PS4_SYSV_ABI internal_snprintf(char* s, u64 n, VA_ARGS) {
VA_CTX(ctx);
return snprintf_ctx(s, n, &ctx);
}
std::map<s32, OrbisFILE*> g_files{};
// Constants for tracking accurate file indexes.
// Since the file struct is exposed to the application, accuracy is important.
static constexpr s32 g_initial_files = 5;
static constexpr s32 g_max_files = 0x100;
OrbisFILE* PS4_SYSV_ABI internal__Fofind() {
u64 index = g_initial_files;
while (index != g_max_files) {
OrbisFILE* file = g_files[index];
// If file doesn't exist, create it.
if (file == nullptr) {
file = new OrbisFILE();
if (file == nullptr) {
return nullptr;
}
// Store new file in the array, initialize default values, and return it.
g_files[index] = file;
file->_Mode = 0x80;
file->_Idx = index;
return file;
}
// Special case, files with mode 0 are returned?
if (file->_Mode == 0) {
file->_Mode = 0xff7f;
return file;
}
index++;
}
return nullptr;
}
void PS4_SYSV_ABI internal__Lockfilelock(OrbisFILE* file) {
if (file != nullptr && file->_Mutex != nullptr) {
internal__Mtxlock(&file->_Mutex);
}
}
void PS4_SYSV_ABI internal__Unlockfilelock(OrbisFILE* file) {
if (file != nullptr && file->_Mutex != nullptr) {
internal__Mtxunlock(&file->_Mutex);
}
}
OrbisFILE* PS4_SYSV_ABI internal__Foprep(const char* path, const char* mode, OrbisFILE* file,
s32 fd, s32 s_mode, s32 flag) {
if (file == nullptr) {
*Kernel::__Error() = POSIX_ENOMEM;
}
// Preserve mode and index
Libraries::Kernel::PthreadMutexT mtx = file->_Mutex;
Libraries::Kernel::PthreadMutexT* mtx_ptr = &file->_Mutex;
u8 file_index = file->_Idx;
u16 file_mode = file->_Mode & 0x80;
// Real library does a memcpy using a static global FILE object.
// This stored file is just zeros, with the only exception being a handle of -1.
memset(file, 0, sizeof(OrbisFILE));
file->_Handle = -1;
// Not sure what this magic is for, but I'll replicate it.
u8* ptr = &file->_Cbuf;
// Note: this field is supposed to be a pthread mutex.
// Since we don't export pthread HLEs for other functions, I'll avoid handling this for now.
file->_Mutex = nullptr;
file->_Idx = file_index;
file->_Buf = ptr;
file->_Bend = &file->unk2;
file->_Next = ptr;
file->_Rend = ptr;
file->_WRend = ptr;
file->_Wend = ptr;
file->_WWend = ptr;
file->_Rback = ptr;
file->_WRback = &file->unk1;
// Parse inputted mode string
const char* mode_str = mode;
u16 calc_mode = 0;
u16 access_mode = 0;
if (mode_str[0] == 'r') {
calc_mode = 1 | file_mode;
} else if (mode_str[0] == 'w') {
calc_mode = 0x1a | file_mode;
} else if (mode_str[0] == 'a') {
calc_mode = 0x16 | file_mode;
} else {
// Closes the file and returns EINVAL.
file->_Mode = file_mode;
if (flag == 0) {
internal__Mtxinit(mtx_ptr, nullptr);
} else {
file->_Mutex = mtx;
internal__Unlockfilelock(file);
}
internal_fclose(file);
*Kernel::__Error() = POSIX_EINVAL;
return nullptr;
}
file->_Mode = calc_mode;
do {
// This is all basically straight from decomp, need to cleanup at some point.
if (mode_str[1] == '+') {
file_mode = 3;
if ((~calc_mode & 3) == 0) {
break;
}
} else if (mode_str[1] != 'b') {
file_mode = 0x20;
if ((calc_mode & 0x20) != 0) {
break;
}
}
mode_str++;
calc_mode = file_mode | calc_mode;
file->_Mode = calc_mode;
} while (true);
if (path == nullptr && fd >= 0) {
// I guess this is for some internal behavior?
file->_Handle = fd;
} else {
fd = internal__Fopen(path, calc_mode, s_mode == 0x55);
file->_Handle = fd;
}
// Error case
if (fd < 0) {
// Closes the file, but ensures errno is unchanged.
if (flag == 0) {
internal__Mtxinit(mtx_ptr, nullptr);
} else {
file->_Mutex = mtx;
internal__Unlockfilelock(file);
}
s32 old_errno = *Kernel::__Error();
internal_fclose(file);
*Kernel::__Error() = old_errno;
return nullptr;
}
if (flag == 0) {
char mtx_name[0x20];
std::snprintf(mtx_name, 0x20, "FileFD:0x%08X", fd);
internal__Mtxinit(mtx_ptr, mtx_name);
} else {
file->_Mutex = mtx;
}
return file;
}
s32 PS4_SYSV_ABI internal__Fopen(const char* path, u16 mode, bool flag) {
u32 large_mode = mode;
u16 open_mode = 0600;
if (!flag) {
open_mode = 0666;
}
// Straight from decomp, should probably get cleaned up at some point.
s32 creat_flag = large_mode << 5 & 0x200;
s32 excl_flag = large_mode << 5 & 0x800;
s32 misc_flags = (large_mode & 8) * 0x80 + (large_mode & 4) * 2;
// Real library has an array for this, where large_mode & 3 is used as an index.
// That array has values [0, 0, 1, 2], so this call should match the result.
s32 access_flag = std::max<s32>((large_mode & 3) - 1, 0);
s32 open_flags = creat_flag | misc_flags | excl_flag | access_flag;
return Libraries::Kernel::posix_open(path, open_flags, open_mode);
}
OrbisFILE* PS4_SYSV_ABI internal_fopen(const char* path, const char* mode) {
std::scoped_lock lk{g_file_mtx};
LOG_INFO(Lib_LibcInternal, "called, path {}, mode {}", path, mode);
OrbisFILE* file = internal__Fofind();
return internal__Foprep(path, mode, file, -1, 0, 0);
}
s32 PS4_SYSV_ABI internal_fflush(OrbisFILE* file) {
if (file == nullptr) {
std::scoped_lock lk{g_file_mtx};
s32 fflush_result = 0;
for (auto& file : g_files) {
s32 res = internal_fflush(file.second);
if (res < 0) {
fflush_result = -1;
}
}
return fflush_result;
}
if ((file->_Mode & 0x2000) != 0) {
internal__Lockfilelock(file);
u16 file_mode = file->_Mode;
u8* file_buf_start = file->_Buf;
u8* file_buf_end = file->_Next;
while (file_buf_start < file_buf_end) {
u64 size_to_write = static_cast<u64>(file_buf_end - file_buf_start);
s32 write_bytes =
Libraries::Kernel::sceKernelWrite(file->_Handle, file_buf_start, size_to_write);
if (write_bytes < 1) {
file_buf_start = file->_Buf;
file->_Next = file_buf_start;
file->_Wend = file_buf_start;
file->_WWend = file_buf_start;
u8* off_mode = reinterpret_cast<u8*>(&file->_Mode) + 1;
*off_mode = *off_mode | 2;
internal__Unlockfilelock(file);
return -1;
}
file_buf_end = file->_Next;
file_buf_start += write_bytes;
}
file->_Next = file_buf_start;
file->_Wend = file_buf_start;
file->_WWend = file_buf_start;
file->_Mode = file_mode & 0xdfff;
internal__Unlockfilelock(file);
}
return 0;
}
s64 PS4_SYSV_ABI internal__Nnl(OrbisFILE* file, u8* val1, u8* val2) {
if (val1 < val2) {
return val2 - val1;
}
return 0;
}
s32 PS4_SYSV_ABI internal__Fspos(OrbisFILE* file, Orbisfpos_t* file_pos, s64 offset, s32 whence) {
if ((file->_Mode & 3) == 0) {
return -1;
}
if (internal_fflush(file) != 0) {
return -1;
}
if (whence >= 3) {
*Libraries::Kernel::__Error() = POSIX_EINVAL;
return -1;
}
if (file_pos != nullptr) {
offset = offset + file_pos->_Off;
}
if (whence == 1 && (file->_Mode & 0x1000) != 0) {
s64 val1 = internal__Nnl(file, file->_Rback, &file->_Cbuf);
u8* rsave_ptr = file->_Rsave;
if (rsave_ptr == nullptr) {
rsave_ptr = file->_Rend;
}
s64 val2 = internal__Nnl(file, file->_Next, rsave_ptr);
s64 val3 = internal__Nnl(file, file->_Next, file->_WRend);
offset = offset - (val1 + val2 + val3);
}
s64 result = 0;
if (whence == 2 || (whence == 1 && offset != 0) || (whence == 0 && offset != -1)) {
result = Libraries::Kernel::posix_lseek(file->_Handle, offset, whence);
}
if (result == -1) {
return -1;
}
u16 file_mode = file->_Mode;
if ((file_mode & 0x3000) != 0) {
u8* file_buf = file->_Buf;
file->_Next = file_buf;
file->_Rend = file_buf;
file->_WRend = file_buf;
file->_Wend = file_buf;
file->_WWend = file_buf;
file->_Rback = &file->_Cbuf;
file->_WRback = &file->unk1;
file->_Rsave = nullptr;
}
if (file_pos != nullptr) {
std::memcpy(&file->_Wstate, &file_pos->_Wstate, sizeof(Orbis_Mbstatet));
}
file->_Mode = file_mode & 0xceff;
return 0;
}
s32 PS4_SYSV_ABI internal_fseek(OrbisFILE* file, s64 offset, s32 whence) {
internal__Lockfilelock(file);
LOG_TRACE(Lib_LibcInternal, "called, file handle {:#x}, offset {:#x}, whence {:#x}",
file->_Handle, offset, whence);
s32 result = internal__Fspos(file, nullptr, offset, whence);
internal__Unlockfilelock(file);
return result;
}
s32 PS4_SYSV_ABI internal__Frprep(OrbisFILE* file) {
if (file->_Rend > file->_Next) {
return 1;
}
if ((file->_Mode & 0x100) != 0) {
return 0;
}
u16 mode = file->_Mode;
if ((mode & 0xa001) != 1) {
// Lot of magic here, might be valuable to figure out what this does.
file->_Mode = (((mode ^ 0x8000) >> 0xf) << 0xe) | mode | 0x200;
return -1;
}
u8* file_buf = file->_Buf;
if ((mode & 0x800) == 0 && file_buf == &file->_Cbuf) {
// Allocate a new file buffer, for now, we'll use host malloc to create it.
// When we have an HLE for malloc, that should be used instead.
u8* new_buffer = std::bit_cast<u8*>(std::malloc(0x10000));
if (new_buffer == nullptr) {
file->_Buf = file_buf;
file->_Bend = file_buf + 1;
} else {
file->_Mode = file->_Mode | 0x40;
file->_Buf = new_buffer;
file->_Bend = new_buffer + 0x10000;
file->_WRend = new_buffer;
file->_WWend = new_buffer;
file_buf = new_buffer;
}
}
file->_Next = file_buf;
file->_Rend = file_buf;
file->_Wend = file_buf;
// Intentional shrinking here, library treats value as 32-bit.
s32 read_result =
Libraries::Kernel::sceKernelRead(file->_Handle, file_buf, file->_Bend - file_buf);
if (read_result < 0) {
u8* off_mode = reinterpret_cast<u8*>(&file->_Mode) + 1;
*off_mode = *off_mode | 0x42;
return -1;
} else if (read_result != 0) {
file->_Mode = file->_Mode | 0x5000;
file->_Rend = file->_Rend + read_result;
return 1;
}
file->_Mode = (file->_Mode & 0xaeff) | 0x4100;
return 0;
}
u64 PS4_SYSV_ABI internal_fread(char* ptr, u64 size, u64 nmemb, OrbisFILE* file) {
if (size == 0 || nmemb == 0) {
return 0;
}
internal__Lockfilelock(file);
LOG_TRACE(Lib_LibcInternal, "called, file handle {:#x}, size {:#x}, nmemb {:#x}", file->_Handle,
size, nmemb);
s64 total_size = size * nmemb;
s64 remaining_size = total_size;
if ((file->_Mode & 0x4000) != 0) {
while (remaining_size != 0) {
u8* rback_ptr = file->_Rback;
if (&file->_Cbuf <= rback_ptr) {
break;
}
file->_Rback = rback_ptr + 1;
*ptr = *rback_ptr;
ptr++;
remaining_size--;
}
}
while (remaining_size != 0) {
u8* file_ptr = file->_Rsave;
if (file_ptr == nullptr) {
file_ptr = file->_Rend;
} else {
file->_Rend = file_ptr;
file->_Rsave = nullptr;
}
u8* src = file->_Next;
if (file_ptr <= src) {
s32 res = internal__Frprep(file);
if (res < 1) {
internal__Unlockfilelock(file);
return (total_size - remaining_size) / size;
}
src = file->_Next;
file_ptr = file->_Rend;
}
u64 copy_bytes = std::min<u64>(file_ptr - src, remaining_size);
std::memcpy(ptr, src, copy_bytes);
file->_Next += copy_bytes;
ptr += copy_bytes;
remaining_size -= copy_bytes;
}
internal__Unlockfilelock(file);
return (total_size - remaining_size) / size;
}
void PS4_SYSV_ABI internal__Fofree(OrbisFILE* file) {
u8* cbuf_ptr = &file->_Cbuf;
s8 trunc_mode = static_cast<s8>(file->_Mode);
file->_Mode = 0;
file->_Handle = -1;
file->_Buf = cbuf_ptr;
file->_Next = cbuf_ptr;
file->_Rend = cbuf_ptr;
file->_WRend = cbuf_ptr;
file->_Wend = cbuf_ptr;
file->_WWend = cbuf_ptr;
file->_Rback = cbuf_ptr;
file->_WRback = &file->unk1;
if (trunc_mode < 0) {
// Remove file from vector
g_files.erase(file->_Idx);
internal__Mtxdst(&file->_Mutex);
free(file);
}
}
s32 PS4_SYSV_ABI internal_fclose(OrbisFILE* file) {
if (file == nullptr) {
return -1;
}
LOG_INFO(Lib_LibcInternal, "called, file handle {:#x}", file->_Handle);
if ((file->_Mode & 3) == 0 || file->_Handle < 0) {
std::scoped_lock lk{g_file_mtx};
internal__Fofree(file);
*Libraries::Kernel::__Error() = POSIX_EBADF;
} else {
s32 fflush_result = internal_fflush(file);
std::scoped_lock lk{g_file_mtx};
if ((file->_Mode & 0x40) != 0) {
std::free(file->_Buf);
}
file->_Buf = nullptr;
s32 close_result = Libraries::Kernel::posix_close(file->_Handle);
internal__Fofree(file);
// Need to figure out what exactly this means.
return ~-(close_result == 0) | fflush_result;
}
return 0;
}
void RegisterlibSceLibcInternalIo(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("eLdDw6l0-bU", "libSceLibcInternal", 1, "libSceLibcInternal", internal_snprintf);
LIB_FUNCTION("MUjC4lbHrK4", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fflush);
LIB_FUNCTION("xGT4Mc55ViQ", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Fofind);
LIB_FUNCTION("dREVnZkAKRE", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Foprep);
LIB_FUNCTION("sQL8D-jio7U", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Fopen);
LIB_FUNCTION("A+Y3xfrWLLo", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Fspos);
LIB_FUNCTION("Ss3108pBuZY", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Nnl);
LIB_FUNCTION("9s3P+LCvWP8", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Frprep);
LIB_FUNCTION("jVDuvE3s5Bs", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Fofree);
LIB_FUNCTION("vZkmJmvqueY", "libSceLibcInternal", 1, "libSceLibcInternal",
internal__Lockfilelock);
LIB_FUNCTION("0x7rx8TKy2Y", "libSceLibcInternal", 1, "libSceLibcInternal",
internal__Unlockfilelock);
}
void ForceRegisterlibSceLibcInternalIo(Core::Loader::SymbolsResolver* sym) {
// Goal is to be minimally intrusive here to allow LLE for printf/stdout writes.
LIB_FUNCTION("xeYO4u7uyJ0", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fopen);
LIB_FUNCTION("rQFVBXp-Cxg", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fseek);
LIB_FUNCTION("lbB+UlZqVG0", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fread);
LIB_FUNCTION("uodLYyUip20", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fclose);
}
} // namespace Libraries::LibcInternal

View File

@ -3,12 +3,96 @@
#pragma once
#include <mutex>
#include "common/types.h"
#include "core/libraries/kernel/threads.h"
namespace Core::Loader {
class SymbolsResolver;
}
namespace Libraries::LibcInternal {
static std::recursive_mutex g_file_mtx{};
union Orbis__mbstate_t {
u8 __mbstate8[128];
s64 _mbstateL;
};
struct Orbis_Mbstatet {
u64 _Wchar;
u16 _Byte, _State;
s32 : 32;
};
struct Orbisfpos_t {
s64 _Off;
Orbis_Mbstatet _Wstate;
};
struct Orbis__sbuf {
u8* _base;
s32 _size;
};
struct OrbisFILE {
u16 _Mode;
u8 _Idx;
s32 _Handle;
u8 *_Buf, *_Bend, *_Next;
u8 *_Rend, *_Wend, *_Rback;
u16 *_WRback, _WBack[2];
u16 unk1;
u8 *_Rsave, *_WRend, *_WWend;
Orbis_Mbstatet _Wstate;
u8* _Tmpnam;
u8 _Back[6], _Cbuf;
u8 unk2;
Libraries::Kernel::PthreadMutexT _Mutex;
u8* _p;
s32 _r;
s32 _w;
s16 _flags;
s16 _file;
Orbis__sbuf _bf;
s32 _lbfsize;
void* _cookie;
s32 PS4_SYSV_ABI (*_close)(void*);
s32 PS4_SYSV_ABI (*_read)(void*, char*, s32);
Orbisfpos_t PS4_SYSV_ABI (*_seek)(void*, Orbisfpos_t, s32);
s32 (*_write)(void*, const char*, s32);
Orbis__sbuf _ub;
u8* _up;
s32 _ur;
u8 _ubuf[3];
u8 _nbuf[1];
Orbis__sbuf _lb;
s32 _blksize;
Orbisfpos_t _offset;
void* _fl_mutex;
void* _fl_owner;
s32 _fl_count;
s32 _orientation;
Orbis__mbstate_t _mbstate;
};
s32 PS4_SYSV_ABI internal_snprintf(char* s, u64 n, VA_ARGS);
void PS4_SYSV_ABI internal__Lockfilelock(OrbisFILE* file);
void PS4_SYSV_ABI internal__Unlockfilelock(OrbisFILE* file);
OrbisFILE* PS4_SYSV_ABI internal__Fofind();
OrbisFILE* PS4_SYSV_ABI internal__Foprep(const char* path, const char* mode, OrbisFILE* file,
s32 fd, s32 flag1, s32 flag2);
s32 PS4_SYSV_ABI internal__Fopen(const char* path, u16 mode, bool flag);
OrbisFILE* PS4_SYSV_ABI internal_fopen(const char* path, const char* mode);
s64 PS4_SYSV_ABI internal__Nnl(OrbisFILE* file, u8* val1, u8* val2);
s32 PS4_SYSV_ABI internal__Fspos(OrbisFILE* file, Orbisfpos_t* file_pos, s64 offset, s32 whence);
s32 PS4_SYSV_ABI internal_fflush(OrbisFILE* file);
s32 PS4_SYSV_ABI internal_fseek(OrbisFILE* file, s64 offset, s32 whence);
s32 PS4_SYSV_ABI internal__Frprep(OrbisFILE* file);
u64 PS4_SYSV_ABI internal_fread(char* ptr, u64 size, u64 nmemb, OrbisFILE* file);
s32 PS4_SYSV_ABI internal_fclose(OrbisFILE* file);
void RegisterlibSceLibcInternalIo(Core::Loader::SymbolsResolver* sym);
void ForceRegisterlibSceLibcInternalIo(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::LibcInternal

View File

@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/libraries/kernel/threads.h"
#include "core/libraries/libc_internal/libc_internal_threads.h"
#include "core/libraries/libs.h"
namespace Libraries::LibcInternal {
void getMutexName(char* buf, u64 size, const char* name) {
if (name != nullptr) {
std::snprintf(buf, size, "SceLibcI_%s", name);
} else {
std::snprintf(buf, size, "SceLibcI");
}
}
s32 PS4_SYSV_ABI internal__Mtxinit(Libraries::Kernel::PthreadMutexT* mtx, const char* name) {
char mtx_name[0x20];
getMutexName(mtx_name, sizeof(mtx_name), name);
Libraries::Kernel::PthreadMutexAttrT attr{};
s32 result = Libraries::Kernel::posix_pthread_mutexattr_init(&attr);
if (result != 0) {
return 1;
}
result = Libraries::Kernel::posix_pthread_mutexattr_settype(
&attr, Libraries::Kernel::PthreadMutexType::Recursive);
if (result == 0) {
s32 mtx_init_result = Libraries::Kernel::scePthreadMutexInit(mtx, &attr, mtx_name);
result = Libraries::Kernel::posix_pthread_mutexattr_destroy(&attr);
if (mtx_init_result == 0 && result == 0) {
return 0;
}
} else {
Libraries::Kernel::posix_pthread_mutexattr_destroy(&attr);
}
return 1;
}
s32 PS4_SYSV_ABI internal__Mtxlock(Libraries::Kernel::PthreadMutexT* mtx) {
s32 result = Libraries::Kernel::posix_pthread_mutex_lock(mtx);
return result != 0;
}
s32 PS4_SYSV_ABI internal__Mtxunlock(Libraries::Kernel::PthreadMutexT* mtx) {
s32 result = Libraries::Kernel::posix_pthread_mutex_unlock(mtx);
return result != 0;
}
s32 PS4_SYSV_ABI internal__Mtxdst(Libraries::Kernel::PthreadMutexT* mtx) {
s32 result = Libraries::Kernel::posix_pthread_mutex_destroy(mtx);
return result != 0;
}
void RegisterlibSceLibcInternalThreads(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("z7STeF6abuU", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Mtxinit);
LIB_FUNCTION("pE4Ot3CffW0", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Mtxlock);
LIB_FUNCTION("cMwgSSmpE5o", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Mtxunlock);
LIB_FUNCTION("LaPaA6mYA38", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Mtxdst);
}
} // namespace Libraries::LibcInternal

View File

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include "common/types.h"
#include "core/libraries/kernel/threads.h"
namespace Core::Loader {
class SymbolsResolver;
}
namespace Libraries::LibcInternal {
s32 PS4_SYSV_ABI internal__Mtxinit(Libraries::Kernel::PthreadMutexT* mtx, const char* name);
s32 PS4_SYSV_ABI internal__Mtxlock(Libraries::Kernel::PthreadMutexT* mtx);
s32 PS4_SYSV_ABI internal__Mtxunlock(Libraries::Kernel::PthreadMutexT* mtx);
s32 PS4_SYSV_ABI internal__Mtxdst(Libraries::Kernel::PthreadMutexT* mtx);
void RegisterlibSceLibcInternalThreads(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::LibcInternal

View File

@ -0,0 +1,266 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <png.h>
#include "common/logging/log.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/libpng/pngenc.h"
#include "core/libraries/libs.h"
#include "pngenc_error.h"
namespace Libraries::PngEnc {
struct PngHandler {
png_structp png_ptr;
png_infop info_ptr;
};
struct PngWriter {
u8* cursor;
u8* start;
size_t capacity;
bool cancel_write;
};
static inline int MapPngFilter(u16 filter) {
if (filter == (u16)OrbisPngEncFilterType::All) {
return PNG_ALL_FILTERS;
}
int f = 0;
if (filter & (u16)OrbisPngEncFilterType::None)
f |= PNG_FILTER_NONE;
if (filter & (u16)OrbisPngEncFilterType::Sub)
f |= PNG_FILTER_SUB;
if (filter & (u16)OrbisPngEncFilterType::Up)
f |= PNG_FILTER_UP;
if (filter & (u16)OrbisPngEncFilterType::Average)
f |= PNG_FILTER_AVG;
if (filter & (u16)OrbisPngEncFilterType::Paeth)
f |= PNG_FILTER_PAETH;
return f;
}
void PngWriteFn(png_structp png_ptr, png_bytep data, size_t length) {
PngWriter* ctx = (PngWriter*)png_get_io_ptr(png_ptr);
if ((size_t)(ctx->cursor - ctx->start) + length > ctx->capacity) {
LOG_ERROR(Lib_Png, "PNG output buffer too small");
ctx->cancel_write = true;
return;
}
memcpy(ctx->cursor, data, length);
ctx->cursor += length;
}
void PngFlushFn(png_structp png_ptr) {}
void PngEncError(png_structp png_ptr, png_const_charp error_message) {
LOG_ERROR(Lib_Png, "PNG error {}", error_message);
}
void PngEncWarning(png_structp png_ptr, png_const_charp error_message) {
LOG_ERROR(Lib_Png, "PNG warning {}", error_message);
}
s32 PS4_SYSV_ABI scePngEncCreate(const OrbisPngEncCreateParam* param, void* memoryAddress,
u32 memorySize, OrbisPngEncHandle* handle) {
if (param == nullptr || param->attribute != 0) {
LOG_ERROR(Lib_Png, "Invalid param");
return ORBIS_PNG_ENC_ERROR_INVALID_ADDR;
}
if (memoryAddress == nullptr) {
LOG_ERROR(Lib_Png, "Invalid memory address");
return ORBIS_PNG_ENC_ERROR_INVALID_ADDR;
}
if (param->max_image_width - 1 > 1000000) {
LOG_ERROR(Lib_Png, "Invalid Size, width = {}", param->max_image_width);
return ORBIS_PNG_ENC_ERROR_INVALID_SIZE;
}
auto pngh = (PngHandler*)memoryAddress;
pngh->png_ptr =
png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, PngEncError, PngEncWarning);
if (pngh->png_ptr == nullptr)
return ORBIS_PNG_ENC_ERROR_FATAL;
pngh->info_ptr = png_create_info_struct(pngh->png_ptr);
if (pngh->info_ptr == nullptr) {
png_destroy_write_struct(&pngh->png_ptr, nullptr);
return false;
}
*handle = pngh;
return ORBIS_OK;
}
s32 PS4_SYSV_ABI scePngEncDelete(OrbisPngEncHandle handle) {
auto pngh = (PngHandler*)handle;
png_destroy_write_struct(&pngh->png_ptr, &pngh->info_ptr);
return ORBIS_OK;
}
s32 PS4_SYSV_ABI scePngEncEncode(OrbisPngEncHandle handle, const OrbisPngEncEncodeParam* param,
OrbisPngEncOutputInfo* outputInfo) {
LOG_TRACE(Lib_Png, "called png addr = {}, image addr = {}, image size = {}",
(void*)param->png_mem_addr, (void*)param->image_mem_addr, param->image_mem_size);
if (handle == nullptr) {
LOG_ERROR(Lib_Png, "Invalid handle");
return ORBIS_PNG_ENC_ERROR_INVALID_HANDLE;
}
if (param == nullptr) {
LOG_ERROR(Lib_Png, "Invalid param");
return ORBIS_PNG_ENC_ERROR_INVALID_PARAM;
}
if (param->image_mem_addr == nullptr || param->png_mem_addr == nullptr) {
LOG_ERROR(Lib_Png, "Invalid input or output address");
return ORBIS_PNG_ENC_ERROR_INVALID_ADDR;
}
if (param->png_mem_size == 0 || param->image_mem_size == 0 || param->image_height == 0 ||
param->image_width == 0) {
LOG_ERROR(Lib_Png, "Invalid Size");
return ORBIS_PNG_ENC_ERROR_INVALID_SIZE;
}
auto pngh = (PngHandler*)handle;
if (setjmp(png_jmpbuf(pngh->png_ptr))) {
LOG_ERROR(Lib_Png, "LibPNG aborted encode");
return ORBIS_PNG_ENC_ERROR_FATAL;
}
int png_color_type = PNG_COLOR_TYPE_RGB;
if (param->color_space == OrbisPngEncColorSpace::RGBA) {
png_color_type |= PNG_COLOR_MASK_ALPHA;
}
int png_interlace_type = PNG_INTERLACE_NONE;
int png_compression_type = PNG_COMPRESSION_TYPE_DEFAULT;
int png_filter_method = PNG_FILTER_TYPE_DEFAULT;
PngWriter writer{};
writer.cursor = param->png_mem_addr;
writer.start = param->png_mem_addr;
writer.capacity = param->png_mem_size;
png_set_write_fn(pngh->png_ptr, &writer, PngWriteFn, PngFlushFn);
png_set_IHDR(pngh->png_ptr, pngh->info_ptr, param->image_width, param->image_height,
param->bit_depth, png_color_type, png_interlace_type, png_compression_type,
png_filter_method);
if (param->pixel_format == OrbisPngEncPixelFormat::B8G8R8A8) {
png_set_bgr(pngh->png_ptr);
}
png_set_compression_level(pngh->png_ptr, std::clamp<u16>(param->compression_level, 0, 9));
png_set_filter(pngh->png_ptr, 0, MapPngFilter(param->filter_type));
png_write_info(pngh->png_ptr, pngh->info_ptr);
int channels = 4;
size_t row_stride = param->image_width * channels;
uint32_t processed_height = 0;
if (param->color_space == OrbisPngEncColorSpace::RGBA) {
for (; processed_height < param->image_height; ++processed_height) {
png_bytep row = (png_bytep)param->image_mem_addr + processed_height * row_stride;
png_write_row(pngh->png_ptr, row);
if (outputInfo != nullptr) {
outputInfo->processed_height = processed_height;
}
if (writer.cancel_write) {
LOG_ERROR(Lib_Png, "Ran out of room to write PNG");
return ORBIS_PNG_ENC_ERROR_DATA_OVERFLOW;
}
}
} else {
// our input data is always rgba but when outputting without an alpha channel, libpng
// expects the input to not have alpha either, i couldn't find a way around this easily?
// png_strip_alpha is for reading and set_background wasn't working, this seems fine...?
std::vector<uint8_t> rgb_row(param->image_width * 3);
for (; processed_height < param->image_height; ++processed_height) {
const unsigned char* src =
param->image_mem_addr + processed_height * param->image_pitch;
uint8_t* dst = rgb_row.data();
for (uint32_t x = 0; x < param->image_width; ++x) {
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
src += 4; // skip reading alpha channel
dst += 3;
}
png_write_row(pngh->png_ptr, rgb_row.data());
if (outputInfo != nullptr) {
outputInfo->processed_height = processed_height;
}
if (writer.cancel_write) {
LOG_ERROR(Lib_Png, "Ran out of room to write PNG");
return ORBIS_PNG_ENC_ERROR_DATA_OVERFLOW;
}
}
}
png_write_flush(pngh->png_ptr);
png_write_end(pngh->png_ptr, pngh->info_ptr);
if (outputInfo != nullptr) {
outputInfo->data_size = writer.cursor - writer.start;
outputInfo->processed_height = processed_height;
}
return writer.cursor - writer.start;
}
s32 PS4_SYSV_ABI scePngEncQueryMemorySize(const OrbisPngEncCreateParam* param) {
if (param == nullptr) {
LOG_ERROR(Lib_Png, "Invalid Address");
return ORBIS_PNG_ENC_ERROR_INVALID_ADDR;
}
if (param->attribute != 0 || param->max_filter_number > 5) {
LOG_ERROR(Lib_Png, "Invalid Param, attribute = {}, max_filter_number = {}",
param->attribute, param->max_filter_number);
return ORBIS_PNG_ENC_ERROR_INVALID_PARAM;
}
if (param->max_image_width - 1 > 1000000) {
LOG_ERROR(Lib_Png, "Invalid Size, width = {}", param->max_image_width);
return ORBIS_PNG_ENC_ERROR_INVALID_SIZE;
}
return sizeof(PngHandler);
}
void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("7aGTPfrqT9s", "libScePngEnc", 1, "libScePngEnc", scePngEncCreate);
LIB_FUNCTION("RUrWdwTWZy8", "libScePngEnc", 1, "libScePngEnc", scePngEncDelete);
LIB_FUNCTION("xgDjJKpcyHo", "libScePngEnc", 1, "libScePngEnc", scePngEncEncode);
LIB_FUNCTION("9030RnBDoh4", "libScePngEnc", 1, "libScePngEnc", scePngEncQueryMemorySize);
};
} // namespace Libraries::PngEnc

View File

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
namespace Core::Loader {
class SymbolsResolver;
}
namespace Libraries::PngEnc {
enum class OrbisPngEncAttribute { None = 0 };
enum class OrbisPngEncColorSpace : u16 { RGB = 3, RGBA = 19 };
enum class OrbisPngEncPixelFormat : u16 { R8G8B8A8 = 0, B8G8R8A8 };
enum class OrbisPngEncFilterType : u16 {
None = 0,
Sub = 1,
Up = 2,
Average = 4,
Paeth = 8,
All = 15
};
struct OrbisPngEncCreateParam {
u32 this_size;
u32 attribute;
u32 max_image_width;
u32 max_filter_number;
};
struct OrbisPngEncEncodeParam {
const u8* image_mem_addr;
u8* png_mem_addr;
u32 image_mem_size;
u32 png_mem_size;
u32 image_width;
u32 image_height;
u32 image_pitch;
OrbisPngEncPixelFormat pixel_format;
OrbisPngEncColorSpace color_space;
u16 bit_depth;
u16 clut_number;
u16 filter_type;
u16 compression_level;
};
struct OrbisPngEncOutputInfo {
u32 data_size;
u32 processed_height;
};
using OrbisPngEncHandle = void*;
s32 PS4_SYSV_ABI scePngEncCreate(const OrbisPngEncCreateParam* param, void* memoryAddress,
u32 memorySize, OrbisPngEncHandle* handle);
s32 PS4_SYSV_ABI scePngEncDelete(OrbisPngEncHandle handle);
s32 PS4_SYSV_ABI scePngEncEncode(OrbisPngEncHandle, const OrbisPngEncEncodeParam* param,
OrbisPngEncOutputInfo* outputInfo);
s32 PS4_SYSV_ABI scePngEncQueryMemorySize(const OrbisPngEncCreateParam* param);
void RegisterLib(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::PngEnc

View File

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/libraries/error_codes.h"
// PngEnc library
constexpr int ORBIS_PNG_ENC_ERROR_INVALID_ADDR = 0x80690101;
constexpr int ORBIS_PNG_ENC_ERROR_INVALID_SIZE = 0x80690102;
constexpr int ORBIS_PNG_ENC_ERROR_INVALID_PARAM = 0x80690103;
constexpr int ORBIS_PNG_ENC_ERROR_INVALID_HANDLE = 0x80690104;
constexpr int ORBIS_PNG_ENC_ERROR_DATA_OVERFLOW = 0x80690110;
constexpr int ORBIS_PNG_ENC_ERROR_FATAL = 0x80690120;

View File

@ -35,11 +35,14 @@
#include "core/libraries/np/np_commerce.h"
#include "core/libraries/np/np_common.h"
#include "core/libraries/np/np_manager.h"
#include "core/libraries/np/np_matching2.h"
#include "core/libraries/np/np_partner.h"
#include "core/libraries/np/np_party.h"
#include "core/libraries/np/np_profile_dialog.h"
#include "core/libraries/np/np_score.h"
#include "core/libraries/np/np_sns_facebook_dialog.h"
#include "core/libraries/np/np_trophy.h"
#include "core/libraries/np/np_tus.h"
#include "core/libraries/np/np_web_api.h"
#include "core/libraries/np/np_web_api2.h"
#include "core/libraries/pad/pad.h"
@ -77,6 +80,7 @@ namespace Libraries {
void InitHLELibs(Core::Loader::SymbolsResolver* sym) {
LOG_INFO(Lib_Kernel, "Initializing HLE libraries");
Libraries::Kernel::RegisterLib(sym);
Libraries::LibcInternal::ForceRegisterLib(sym);
Libraries::GnmDriver::RegisterLib(sym);
Libraries::VideoOut::RegisterLib(sym);
Libraries::UserService::RegisterLib(sym);
@ -97,6 +101,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) {
Libraries::Np::NpCommerce::RegisterLib(sym);
Libraries::Np::NpCommon::RegisterLib(sym);
Libraries::Np::NpManager::RegisterLib(sym);
Libraries::Np::NpMatching2::RegisterLib(sym);
Libraries::Np::NpScore::RegisterLib(sym);
Libraries::Np::NpTrophy::RegisterLib(sym);
Libraries::Np::NpWebApi::RegisterLib(sym);
@ -105,6 +110,8 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) {
Libraries::Np::NpSnsFacebookDialog::RegisterLib(sym);
Libraries::Np::NpAuth::RegisterLib(sym);
Libraries::Np::NpParty::RegisterLib(sym);
Libraries::Np::NpPartner::RegisterLib(sym);
Libraries::Np::NpTus::RegisterLib(sym);
Libraries::ScreenShot::RegisterLib(sym);
Libraries::AppContent::RegisterLib(sym);
Libraries::PngDec::RegisterLib(sym);

View File

@ -430,8 +430,8 @@ int PS4_SYSV_ABI sceHttpParseStatusLine(const char* statusLine, u64 lineLen, int
return index + 1;
}
int PS4_SYSV_ABI sceHttpReadData() {
LOG_ERROR(Lib_Http, "(STUBBED) called");
int PS4_SYSV_ABI sceHttpReadData(s32 reqId, void* data, u64 size) {
LOG_ERROR(Lib_Http, "(STUBBED) called reqId = {} size = {}", reqId, size);
return ORBIS_OK;
}

View File

@ -91,7 +91,7 @@ int PS4_SYSV_ABI sceHttpParseResponseHeader(const char* header, u64 headerLen, c
int PS4_SYSV_ABI sceHttpParseStatusLine(const char* statusLine, u64 lineLen, int32_t* httpMajorVer,
int32_t* httpMinorVer, int32_t* responseCode,
const char** reasonPhrase, u64* phraseLen);
int PS4_SYSV_ABI sceHttpReadData();
int PS4_SYSV_ABI sceHttpReadData(s32 reqId, void* data, u64 size);
int PS4_SYSV_ABI sceHttpRedirectCacheFlush();
int PS4_SYSV_ABI sceHttpRemoveRequestHeader();
int PS4_SYSV_ABI sceHttpRequestGetAllHeaders();

View File

@ -655,10 +655,17 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or
switch (file->type) {
case Core::FileSys::FileType::Socket: {
auto native_handle = file->socket->Native();
if (!native_handle) {
// P2P socket, cannot be added to epoll
LOG_ERROR(Lib_Net, "P2P socket cannot be added to epoll (unimplemented)");
*sceNetErrnoLoc() = ORBIS_NET_EBADF;
return ORBIS_NET_ERROR_EBADF;
}
epoll_event native_event = {.events = ConvertEpollEventsIn(event->events),
.data = {.fd = id}};
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_ADD, *file->socket->Native(),
&native_event) == 0);
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_ADD, *native_handle, &native_event) == 0);
epoll->events.emplace_back(id, *event);
break;
}
@ -696,10 +703,17 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or
switch (file->type) {
case Core::FileSys::FileType::Socket: {
auto native_handle = file->socket->Native();
if (!native_handle) {
// P2P socket, cannot be modified in epoll
LOG_ERROR(Lib_Net, "P2P socket cannot be modified in epoll (unimplemented)");
*sceNetErrnoLoc() = ORBIS_NET_EBADF;
return ORBIS_NET_ERROR_EBADF;
}
epoll_event native_event = {.events = ConvertEpollEventsIn(event->events),
.data = {.fd = id}};
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_MOD, *file->socket->Native(),
&native_event) == 0);
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_MOD, *native_handle, &native_event) == 0);
*it = {id, *event};
break;
}
@ -731,8 +745,15 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or
switch (file->type) {
case Core::FileSys::FileType::Socket: {
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_DEL, *file->socket->Native(), nullptr) ==
0);
auto native_handle = file->socket->Native();
if (!native_handle) {
// P2P socket, cannot be removed from epoll
LOG_ERROR(Lib_Net, "P2P socket cannot be removed from epoll (unimplemented)");
*sceNetErrnoLoc() = ORBIS_NET_EBADF;
return ORBIS_NET_ERROR_EBADF;
}
ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_DEL, *native_handle, nullptr) == 0);
epoll->events.erase(it);
break;
}
@ -782,6 +803,7 @@ int PS4_SYSV_ABI sceNetEpollDestroy(OrbisNetId epollid) {
LOG_DEBUG(Lib_Net, "called, epollid = {} ({})", epollid, file->epoll->name);
file->epoll->Destroy();
FDTable::Instance()->DeleteHandle(epollid);
return ORBIS_OK;
}

View File

@ -335,6 +335,7 @@ int PS4_SYSV_ABI sys_socketclose(OrbisNetId s) {
LOG_DEBUG(Lib_Net, "s = {} ({})", s, file->m_guest_name);
int returncode = file->socket->Close();
if (returncode >= 0) {
FDTable::Instance()->DeleteHandle(s);
return returncode;
}
LOG_ERROR(Lib_Net, "error code returned: {}", (u32)*Libraries::Kernel::__Error());

View File

@ -123,7 +123,9 @@ s32 GetAuthorizationCode(s32 req_id, const OrbisNpAuthGetAuthorizationCodeParame
// Not sure what values are expected here, so zeroing these for now.
std::memset(auth_code, 0, sizeof(OrbisNpAuthorizationCode));
*issuer_id = 0;
if (issuer_id != nullptr) {
*issuer_id = 0;
}
return ORBIS_OK;
}

View File

@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
#include "core/libraries/np/np_types.h"
#include "core/libraries/system/userservice.h"
namespace Core::Loader {
class SymbolsResolver;
@ -31,7 +32,7 @@ struct OrbisNpAuthGetAuthorizationCodeParameter {
struct OrbisNpAuthGetAuthorizationCodeParameterA {
u64 size;
s32 user_id;
Libraries::UserService::OrbisUserServiceUserId user_id;
u8 padding[4];
const OrbisNpClientId* client_id;
const char* scope;
@ -47,7 +48,7 @@ struct OrbisNpAuthGetIdTokenParameter {
struct OrbisNpAuthGetIdTokenParameterA {
u64 size;
s32 user_id;
Libraries::UserService::OrbisUserServiceUserId user_id;
u8 padding[4];
const OrbisNpClientId* client_id;
const OrbisNpClientSecret* client_secret;

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

View File

@ -14,4 +14,12 @@ constexpr int ORBIS_NP_ERROR_INVALID_SIZE = 0x80550011;
constexpr int ORBIS_NP_ERROR_ABORTED = 0x80550012;
constexpr int ORBIS_NP_ERROR_REQUEST_MAX = 0x80550013;
constexpr int ORBIS_NP_ERROR_REQUEST_NOT_FOUND = 0x80550014;
constexpr int ORBIS_NP_ERROR_INVALID_ID = 0x80550015;
constexpr int ORBIS_NP_ERROR_INVALID_ID = 0x80550015;
constexpr int ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT = 0x80550704;
constexpr int ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS = 0x80550706;
constexpr int ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = 0x8055070c;
constexpr int ORBIS_NP_COMMUNITY_ERROR_INVALID_ID = 0x8055070e;
constexpr int ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT = 0x80550714;
constexpr int ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID = 0x80550718;
constexpr int ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_NPID = 0x80550719;

View File

@ -1,7 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <mutex>
#include <variant>
#include "common/config.h"
#include "common/logging/log.h"
@ -17,6 +19,9 @@ static bool g_signed_in = false;
static s32 g_active_requests = 0;
static std::mutex g_request_mutex;
static std::map<std::string, std::function<void()>> g_np_callbacks;
static std::mutex g_np_callbacks_mutex;
// Internal types for storing request-related information
enum class NpRequestState {
None = 0,
@ -665,6 +670,19 @@ s32 PS4_SYSV_ABI sceNpGetState(Libraries::UserService::OrbisUserServiceUserId us
return ORBIS_OK;
}
s32 PS4_SYSV_ABI
sceNpGetUserIdByAccountId(u64 account_id, Libraries::UserService::OrbisUserServiceUserId* user_id) {
if (user_id == nullptr) {
return ORBIS_NP_ERROR_INVALID_ARGUMENT;
}
if (!g_signed_in) {
return ORBIS_NP_ERROR_SIGNED_OUT;
}
*user_id = 1;
LOG_DEBUG(Lib_NpManager, "userid({}) = {}", account_id, *user_id);
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNpHasSignedUp(Libraries::UserService::OrbisUserServiceUserId user_id,
bool* has_signed_up) {
LOG_DEBUG(Lib_NpManager, "called");
@ -682,11 +700,25 @@ struct NpStateCallbackForNpToolkit {
NpStateCallbackForNpToolkit NpStateCbForNp;
struct NpStateCallback {
std::variant<OrbisNpStateCallback, OrbisNpStateCallbackA> func;
void* userdata;
};
NpStateCallback NpStateCb;
s32 PS4_SYSV_ABI sceNpCheckCallback() {
if (!g_signed_in) {
return ORBIS_NP_ERROR_NOT_INITIALIZED;
}
LOG_DEBUG(Lib_NpManager, "(STUBBED) called");
std::scoped_lock lk{g_np_callbacks_mutex};
for (auto i : g_np_callbacks) {
(i.second)();
}
return ORBIS_OK;
}
@ -695,6 +727,40 @@ s32 PS4_SYSV_ABI sceNpCheckCallbackForLib() {
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNpRegisterStateCallback(OrbisNpStateCallback callback, void* userdata) {
static s32 id = 0;
LOG_ERROR(Lib_NpManager, "(STUBBED) called, userdata = {}", userdata);
NpStateCb.func = callback;
NpStateCb.userdata = userdata;
return id;
}
s32 PS4_SYSV_ABI sceNpRegisterStateCallbackA(OrbisNpStateCallbackA callback, void* userdata) {
static s32 id = 0;
LOG_ERROR(Lib_NpManager, "(STUBBED) called, userdata = {}", userdata);
NpStateCb.func = callback;
NpStateCb.userdata = userdata;
return id;
}
struct NpReachabilityStateCallback {
OrbisNpReachabilityStateCallback func;
void* userdata;
};
NpReachabilityStateCallback NpReachabilityCb;
s32 PS4_SYSV_ABI sceNpRegisterNpReachabilityStateCallback(OrbisNpReachabilityStateCallback callback,
void* userdata) {
static s32 id = 0;
LOG_ERROR(Lib_NpManager, "(STUBBED) called");
NpReachabilityCb.func = callback;
NpReachabilityCb.userdata = userdata;
return id;
}
s32 PS4_SYSV_ABI sceNpRegisterStateCallbackForToolkit(OrbisNpStateCallbackForNpToolkit callback,
void* userdata) {
static s32 id = 0;
@ -704,6 +770,22 @@ s32 PS4_SYSV_ABI sceNpRegisterStateCallbackForToolkit(OrbisNpStateCallbackForNpT
return id;
}
void RegisterNpCallback(std::string key, std::function<void()> cb) {
std::scoped_lock lk{g_np_callbacks_mutex};
LOG_DEBUG(Lib_NpManager, "registering callback processing for {}", key);
g_np_callbacks.emplace(key, cb);
}
void DeregisterNpCallback(std::string key) {
std::scoped_lock lk{g_np_callbacks_mutex};
LOG_DEBUG(Lib_NpManager, "deregistering callback processing for {}", key);
g_np_callbacks.erase(key);
}
void RegisterLib(Core::Loader::SymbolsResolver* sym) {
g_signed_in = Config::getPSNSignedIn();
@ -742,9 +824,14 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("p-o74CnoNzY", "libSceNpManager", 1, "libSceNpManager", sceNpGetNpId);
LIB_FUNCTION("XDncXQIJUSk", "libSceNpManager", 1, "libSceNpManager", sceNpGetOnlineId);
LIB_FUNCTION("eQH7nWPcAgc", "libSceNpManager", 1, "libSceNpManager", sceNpGetState);
LIB_FUNCTION("VgYczPGB5ss", "libSceNpManager", 1, "libSceNpManager", sceNpGetUserIdByAccountId);
LIB_FUNCTION("Oad3rvY-NJQ", "libSceNpManager", 1, "libSceNpManager", sceNpHasSignedUp);
LIB_FUNCTION("3Zl8BePTh9Y", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallback);
LIB_FUNCTION("JELHf4xPufo", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallbackForLib);
LIB_FUNCTION("VfRSmPmj8Q8", "libSceNpManager", 1, "libSceNpManager",
sceNpRegisterStateCallback);
LIB_FUNCTION("hw5KNqAAels", "libSceNpManager", 1, "libSceNpManager",
sceNpRegisterNpReachabilityStateCallback);
LIB_FUNCTION("JELHf4xPufo", "libSceNpManagerForToolkit", 1, "libSceNpManager",
sceNpCheckCallbackForLib);
LIB_FUNCTION("0c7HbXRKUt4", "libSceNpManagerForToolkit", 1, "libSceNpManager",

View File

@ -3,6 +3,8 @@
#pragma once
#include <functional>
#include "common/types.h"
#include "core/libraries/np/np_error.h"
#include "core/libraries/np/np_types.h"
@ -23,20 +25,28 @@ enum class OrbisNpState : u32 {
SignedIn = 2,
};
using OrbisNpStateCallbackForNpToolkit = PS4_SYSV_ABI void (*)(s32 userId, OrbisNpState state,
void* userdata);
enum class OrbisNpGamePresenseStatus {
Offline = 0,
Online = 1,
};
enum class OrbisNpReachabilityState {
Unavailable = 0,
Available = 1,
Reachable = 2,
};
using OrbisNpStateCallback =
PS4_SYSV_ABI void (*)(Libraries::UserService::OrbisUserServiceUserId userId, OrbisNpState state,
OrbisNpId* npId, void* userdata);
using OrbisNpStateCallbackA = PS4_SYSV_ABI void (*)(
Libraries::UserService::OrbisUserServiceUserId userId, OrbisNpState state, void* userdata);
using OrbisNpStateCallbackForNpToolkit = PS4_SYSV_ABI void (*)(
Libraries::UserService::OrbisUserServiceUserId userId, OrbisNpState state, void* userdata);
using OrbisNpReachabilityStateCallback =
PS4_SYSV_ABI void (*)(Libraries::UserService::OrbisUserServiceUserId userId,
OrbisNpReachabilityState state, void* userdata);
enum class OrbisNpGamePresenseStatus {
Offline = 0,
Online = 1,
};
struct OrbisNpCountryCode {
char country_code[2];
char end;
@ -80,5 +90,13 @@ struct OrbisNpCreateAsyncRequestParameter {
u8 padding[4];
};
void RegisterNpCallback(std::string key, std::function<void()> cb);
void DeregisterNpCallback(std::string key);
s32 PS4_SYSV_ABI sceNpGetNpId(Libraries::UserService::OrbisUserServiceUserId user_id,
OrbisNpId* np_id);
s32 PS4_SYSV_ABI sceNpGetOnlineId(Libraries::UserService::OrbisUserServiceUserId user_id,
OrbisNpOnlineId* online_id);
void RegisterLib(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::Np::NpManager

View File

@ -0,0 +1,812 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <deque>
#include <mutex>
#include "common/config.h"
#include "common/logging/log.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/libs.h"
#include "core/libraries/np/np_manager.h"
#include "core/libraries/np/np_matching2.h"
#include "core/libraries/np/np_types.h"
#include "core/libraries/system/userservice.h"
namespace Libraries::Np::NpMatching2 {
static bool g_initialized = false;
static OrbisNpMatching2ContextId contextId = 1;
struct NpMatching2ContextEvent {
OrbisNpMatching2ContextId contextId;
OrbisNpMatching2Event event;
OrbisNpMatching2EventCause cause;
int errorCode;
};
struct NpMatching2LobbyEvent {
OrbisNpMatching2ContextId contextId;
OrbisNpMatching2LobbyId lobbyId;
OrbisNpMatching2Event event;
void* data;
};
struct NpMatching2RoomEvent {
OrbisNpMatching2ContextId contextId;
OrbisNpMatching2RoomId roomId;
OrbisNpMatching2Event event;
void* data;
};
static std::mutex g_events_mutex;
static std::deque<NpMatching2ContextEvent> g_ctx_events;
static std::deque<NpMatching2LobbyEvent> g_lobby_events;
static std::deque<NpMatching2RoomEvent> g_room_events;
static std::mutex g_responses_mutex;
static std::deque<std::function<void()>> g_responses;
struct OrbisNpMatching2CreateContextParameter {
Libraries::Np::OrbisNpId* npId;
void* npCommunicationId;
void* npPassphrase;
Libraries::Np::OrbisNpServiceLabel serviceLabel;
u64 size;
};
static_assert(sizeof(OrbisNpMatching2CreateContextParameter) == 0x28);
int PS4_SYSV_ABI sceNpMatching2CreateContext(const OrbisNpMatching2CreateContextParameter* param,
OrbisNpMatching2ContextId* ctxId) {
LOG_DEBUG(Lib_NpMatching2, "called, npId = {}, serviceLabel = {}, size = {}",
param->npId->handle.data, param->serviceLabel, param->size);
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!param || param->size != 0x28 || !ctxId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
*ctxId = contextId++;
return ORBIS_OK;
}
struct OrbisNpMatching2CreateContextParameterA {
Libraries::UserService::OrbisUserServiceUserId userId;
Libraries::Np::OrbisNpServiceLabel serviceLabel;
u64 size;
};
static_assert(sizeof(OrbisNpMatching2CreateContextParameterA) == 16);
int PS4_SYSV_ABI sceNpMatching2CreateContextA(const OrbisNpMatching2CreateContextParameterA* param,
OrbisNpMatching2ContextId* ctxId) {
LOG_DEBUG(Lib_NpMatching2, "called, userId = {}, serviceLabel = {}, size = {}", param->userId,
param->serviceLabel, param->size);
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!param || param->size != 0x10 || !ctxId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
*ctxId = contextId++;
return ORBIS_OK;
}
using OrbisNpMatching2RequestCallback = PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId,
OrbisNpMatching2RequestId,
OrbisNpMatching2Event, int, void*,
void*);
struct OrbisNpMatching2RequestOptParam {
OrbisNpMatching2RequestCallback callback;
void* arg;
u32 timeout;
u16 appId;
u8 dummy[2];
};
static std::optional<OrbisNpMatching2RequestOptParam> defaultRequestOptParam = std::nullopt;
auto GetOptParam(OrbisNpMatching2RequestOptParam* requestOpt) {
return requestOpt ? *requestOpt
: (defaultRequestOptParam ? defaultRequestOptParam
: std::optional<OrbisNpMatching2RequestOptParam>{});
}
struct OrbisNpMatching2CreateJoinRoomRequestA {
u16 maxSlot;
OrbisNpMatching2TeamId teamId;
u8 pad[5];
OrbisNpMatching2Flags flags;
OrbisNpMatching2WorldId worldId;
OrbisNpMatching2LobbyId lobbyId;
void* roomPasswd;
void* passwdSlotMask;
void* groupConfig;
u64 groupConfigs;
void* joinGroupLabel;
Libraries::Np::OrbisNpAccountId* allowedUser;
u64 allowedUsers;
Libraries::Np::OrbisNpAccountId* blockedUser;
u64 blockedUsers;
void* internalBinAttr;
u64 internalBinAttrs;
void* externalSearchIntAttr;
u64 externalSearchIntAttrs;
void* externalSearchBinAttr;
u64 externalSearchBinAttrs;
void* externalBinAttr;
u64 externalBinAttrs;
void* memberInternalBinAttr;
u64 memberInternalBinAttrs;
void* signalingParam;
};
static_assert(sizeof(OrbisNpMatching2CreateJoinRoomRequestA) == 184);
struct OrbisNpMatching2RoomDataInternal {
u16 publicSlots;
u16 privateSlots;
u16 openPublicSlots;
u16 openPrivateSlots;
u16 maxSlot;
OrbisNpMatching2ServerId serverId;
OrbisNpMatching2WorldId worldId;
OrbisNpMatching2LobbyId lobbyId;
OrbisNpMatching2RoomId roomId;
u64 passwdSlotMask;
u64 joinedSlotMask;
void* roomGroup;
u64 roomGroups;
OrbisNpMatching2Flags flags;
u8 pad[4];
void* internalBinAttr;
u64 internalBinAttrs;
};
struct OrbisNpMatching2RoomMemberDataInternalA {
OrbisNpMatching2RoomMemberDataInternalA* next;
u64 joinDateTicks;
Libraries::Np::OrbisNpPeerAddressA user;
Libraries::Np::OrbisNpOnlineId onlineId;
u8 pad[4];
OrbisNpMatching2RoomMemberId memberId;
OrbisNpMatching2TeamId teamId;
OrbisNpMatching2NatType natType;
OrbisNpMatching2Flags flags;
void* roomGroup;
void* roomMemberInternalBinAttr;
u64 roomMemberInternalBinAttrs;
};
struct OrbisNpMatching2RoomMemberDataInternalListA {
OrbisNpMatching2RoomMemberDataInternalA* members;
u64 membersNum;
OrbisNpMatching2RoomMemberDataInternalA* me;
OrbisNpMatching2RoomMemberDataInternalA* owner;
};
struct OrbisNpMatching2CreateJoinRoomResponseA {
OrbisNpMatching2RoomDataInternal* roomData;
OrbisNpMatching2RoomMemberDataInternalListA members;
};
int PS4_SYSV_ABI sceNpMatching2CreateJoinRoomA(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2CreateJoinRoomRequestA* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
LOG_DEBUG(Lib_NpMatching2,
"maxSlot = {}, teamId = {}, worldId = {}, lobbyId = {}, groupConfig = {}, "
"joinGroupLabel = {}",
request->maxSlot, request->teamId, request->worldId, request->lobbyId,
request->groupConfig, request->joinGroupLabel);
static OrbisNpMatching2RequestId id = 10;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
auto requestCopy = *request;
g_responses.emplace_back([=]() {
Libraries::Np::OrbisNpOnlineId onlineId{};
if (NpManager::sceNpGetOnlineId(1, &onlineId) != ORBIS_OK) {
return;
}
OrbisNpMatching2RoomMemberDataInternalA me{
nullptr,
0,
{0xace104e, Libraries::Np::OrbisNpPlatformType::PS4},
onlineId,
{0, 0, 0, 0},
1,
requestCopy.teamId,
1,
0,
nullptr,
nullptr,
0};
OrbisNpMatching2RoomDataInternal room{requestCopy.maxSlot,
0,
static_cast<u16>(requestCopy.maxSlot - 1u),
0,
15,
0xac,
requestCopy.worldId,
requestCopy.lobbyId,
0x10,
0,
0,
nullptr,
0,
0,
{0, 0, 0, 0},
nullptr,
0};
OrbisNpMatching2CreateJoinRoomResponseA resp{&room, {&me, 1, &me, &me}};
optParam->callback(ctxId, reqIdCopy,
ORBIS_NP_MATCHING2_REQUEST_EVENT_CREATE_JOIN_ROOM_A, 0, &resp,
optParam->arg);
});
}
return ORBIS_OK;
}
using OrbisNpMatching2ContextCallback = PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId contextId,
OrbisNpMatching2Event event,
OrbisNpMatching2EventCause cause,
int errorCode, void* userdata);
std::function<void(const NpMatching2ContextEvent*)> npMatching2ContextCallback = nullptr;
int PS4_SYSV_ABI sceNpMatching2RegisterContextCallback(OrbisNpMatching2ContextCallback callback,
void* userdata) {
LOG_DEBUG(Lib_NpMatching2, "called, userdata = {}", userdata);
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
npMatching2ContextCallback = [callback, userdata](auto arg) {
callback(arg->contextId, arg->event, arg->cause, arg->errorCode, userdata);
};
return ORBIS_OK;
}
using OrbisNpMatching2LobbyEventCallback =
PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId contextId, OrbisNpMatching2LobbyId lobbyId,
OrbisNpMatching2Event event, void* data, void* userdata);
std::function<void(const NpMatching2LobbyEvent*)> npMatching2LobbyCallback = nullptr;
int PS4_SYSV_ABI sceNpMatching2RegisterLobbyEventCallback(
OrbisNpMatching2ContextId ctxId, OrbisNpMatching2LobbyEventCallback callback, void* userdata) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata);
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
npMatching2LobbyCallback = [callback, userdata](auto arg) {
callback(arg->contextId, arg->lobbyId, arg->event, arg->data, userdata);
};
return ORBIS_OK;
}
using OrbisNpMatching2RoomEventCallback = PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId contextId,
OrbisNpMatching2RoomId roomId,
OrbisNpMatching2Event event,
void* data, void* userdata);
std::function<void(const NpMatching2RoomEvent*)> npMatching2RoomCallback = nullptr;
int PS4_SYSV_ABI sceNpMatching2RegisterRoomEventCallback(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2RoomEventCallback callback,
void* userdata) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata);
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
npMatching2RoomCallback = [callback, userdata](auto arg) {
callback(arg->contextId, arg->roomId, arg->event, arg->data, userdata);
};
return ORBIS_OK;
}
struct OrbisNpMatching2SignalingEvent {
OrbisNpMatching2ContextId contextId;
OrbisNpMatching2RoomId roomId;
OrbisNpMatching2RoomMemberId roomMemberId;
OrbisNpMatching2Event event;
int errorCode;
};
using OrbisNpMatching2SignalingCallback =
PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId contextId, OrbisNpMatching2RoomId roomId,
OrbisNpMatching2RoomMemberId roomMemberId, OrbisNpMatching2Event event,
int errorCode, void* userdata);
std::function<void(const OrbisNpMatching2SignalingEvent*)> npMatching2SignalingCallback = nullptr;
int PS4_SYSV_ABI sceNpMatching2RegisterSignalingCallback(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2SignalingCallback callback,
void* userdata) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata);
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
npMatching2SignalingCallback = [callback, userdata](auto arg) {
callback(arg->contextId, arg->roomId, arg->roomMemberId, arg->event, arg->errorCode,
userdata);
};
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2ContextStart(OrbisNpMatching2ContextId ctxId, u64 timeout) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, timeout = {}", ctxId, timeout);
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
std::scoped_lock lk{g_events_mutex};
if (Config::getIsConnectedToNetwork() && Config::getPSNSignedIn()) {
g_ctx_events.emplace_back(ctxId, ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED,
ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ACTION, 0);
} else {
// error confirmed with a real console disconnected from the internet
constexpr int ORBIS_NET_ERROR_RESOLVER_ETIMEDOUT = 0x804101e2;
g_ctx_events.emplace_back(ctxId, ORBIS_NP_MATCHING2_CONTEXT_EVENT_START_OVER,
ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ERROR,
ORBIS_NET_ERROR_RESOLVER_ETIMEDOUT);
}
return ORBIS_OK;
}
void ProcessEvents() {
{
std::scoped_lock lk{g_events_mutex};
if (npMatching2ContextCallback) {
while (!g_ctx_events.empty()) {
npMatching2ContextCallback(&g_ctx_events.front());
g_ctx_events.pop_front();
}
}
if (npMatching2LobbyCallback) {
while (!g_lobby_events.empty()) {
npMatching2LobbyCallback(&g_lobby_events.front());
g_lobby_events.pop_front();
}
}
if (npMatching2RoomCallback) {
while (!g_room_events.empty()) {
npMatching2RoomCallback(&g_room_events.front());
g_room_events.pop_front();
}
}
}
std::scoped_lock lk{g_responses_mutex};
while (!g_responses.empty()) {
(g_responses.front())();
g_responses.pop_front();
}
}
struct OrbisNpMatching2InitializeParameter {
u64 poolSize;
//
};
int PS4_SYSV_ABI sceNpMatching2Initialize(OrbisNpMatching2InitializeParameter* param) {
LOG_DEBUG(Lib_NpMatching2, "called");
if (g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_ALREADY_INITIALIZED;
}
g_initialized = true;
Libraries::Np::NpManager::RegisterNpCallback("NpMatching2", ProcessEvents);
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2Terminate() {
LOG_DEBUG(Lib_NpMatching2, "called");
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
g_initialized = false;
Libraries::Np::NpManager::DeregisterNpCallback("NpMatching2");
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2SetDefaultRequestOptParam(
OrbisNpMatching2ContextId ctxId, OrbisNpMatching2RequestOptParam* requestOpt) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}", ctxId);
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!requestOpt) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
defaultRequestOptParam = *requestOpt;
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2GetServerId(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2ServerId* serverId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}", ctxId);
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!serverId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
*serverId = 0xac;
return ORBIS_OK;
}
struct OrbisNpMatching2GetWorldInfoListRequest {
OrbisNpMatching2ServerId serverId;
};
struct OrbisNpMatching2World {
OrbisNpMatching2World* next;
OrbisNpMatching2WorldId worldId;
u32 lobbiesNum;
u32 maxLobbyMembersNum;
u32 lobbyMembersNum;
u32 roomsNum;
u32 roomMembersNum;
u8 pad[3];
};
struct OrbisNpMatching2GetWorldInfoListResponse {
OrbisNpMatching2World* world;
u64 worldNum;
};
int PS4_SYSV_ABI sceNpMatching2GetWorldInfoList(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2GetWorldInfoListRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, request.serverId = {}, requestOpt = {}", ctxId,
request ? request->serverId : 0xFFFF, fmt::ptr(requestOpt));
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 1;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
auto reqIdCopy = *requestId;
std::scoped_lock lk{g_responses_mutex};
g_responses.emplace_back([=]() {
OrbisNpMatching2World w{nullptr, 1, 10, 0, 10, 0, {}};
OrbisNpMatching2GetWorldInfoListResponse resp{&w, 1};
optParam->callback(ctxId, reqIdCopy,
ORBIS_NP_MATCHING2_REQUEST_EVENT_GET_WORLD_INFO_LIST, 0, &resp,
optParam->arg);
});
}
return ORBIS_OK;
}
struct OrbisNpMatching2PresenceOptionData {
u8 data[16];
u64 len;
};
struct OrbisNpMatching2LeaveRoomRequest {
OrbisNpMatching2RoomId roomId;
OrbisNpMatching2PresenceOptionData optData;
};
int PS4_SYSV_ABI sceNpMatching2LeaveRoom(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2LeaveRoomRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 500;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_LEAVE_ROOM, 0,
nullptr, optParam->arg);
});
}
return ORBIS_OK;
}
struct OrbisNpMatching2RangeFilter {
u32 start;
u32 max;
};
struct OrbisNpMatching2SearchRoomRequest {
int option;
OrbisNpMatching2WorldId worldId;
OrbisNpMatching2LobbyId lobbyId;
OrbisNpMatching2RangeFilter rangeFilter;
OrbisNpMatching2Flags flags1;
OrbisNpMatching2Flags flags2;
void* intFilter;
u64 intFilters;
void* binFilter;
u64 binFilters;
OrbisNpMatching2AttributeId* attr;
u64 attrs;
};
struct OrbisNpMatching2Range {
u32 start;
u32 total;
u32 results;
u8 pad[4];
};
struct OrbisNpMatching2SearchRoomResponseA {
OrbisNpMatching2Range range;
void* roomDataExt;
};
int PS4_SYSV_ABI sceNpMatching2SearchRoom(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2SearchRoomRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 1;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
auto requestCopy = *request;
g_responses.emplace_back([=]() {
OrbisNpMatching2SearchRoomResponseA resp{{0, 0, 0, {}}, nullptr};
optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_SEARCH_ROOM_A, 0,
&resp, optParam->arg);
});
}
return ORBIS_OK;
}
struct OrbisNpMatching2SetUserInfoRequest {
OrbisNpMatching2ServerId serverId;
u8 padding[6];
void* userBinAttr;
u64 userBinAttrs;
};
int PS4_SYSV_ABI sceNpMatching2SetUserInfo(OrbisNpMatching2ContextId ctxId,
OrbisNpMatching2SetUserInfoRequest* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 100;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_USER_INFO, 0,
nullptr, optParam->arg);
});
}
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2SendRoomMessage(OrbisNpMatching2ContextId ctxId, void* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 1000;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_SEND_ROOM_MESSAGE,
0, nullptr, optParam->arg);
});
}
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2SetRoomDataExternal(OrbisNpMatching2ContextId ctxId, void* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 800;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy,
ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_EXTERNAL, 0, nullptr,
optParam->arg);
});
}
return ORBIS_OK;
}
int PS4_SYSV_ABI sceNpMatching2SetRoomDataInternal(OrbisNpMatching2ContextId ctxId, void* request,
OrbisNpMatching2RequestOptParam* requestOpt,
OrbisNpMatching2RequestId* requestId) {
LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt));
if (!g_initialized) {
return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED;
}
if (!request || !requestId) {
return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT;
}
static OrbisNpMatching2RequestId id = 200;
*requestId = id++;
if (auto optParam = GetOptParam(requestOpt); optParam) {
LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout,
optParam->appId);
std::scoped_lock lk{g_responses_mutex};
auto reqIdCopy = *requestId;
g_responses.emplace_back([=]() {
optParam->callback(ctxId, reqIdCopy,
ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_INTERNAL, 0, nullptr,
optParam->arg);
});
}
return ORBIS_OK;
}
void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("10t3e5+JPnU", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2Initialize);
LIB_FUNCTION("Mqp3lJ+sjy4", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2Terminate);
LIB_FUNCTION("YfmpW719rMo", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2CreateContext);
LIB_FUNCTION("ajvzc8e2upo", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2CreateContextA);
LIB_FUNCTION("V6KSpKv9XJE", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2CreateJoinRoomA);
LIB_FUNCTION("fQQfP87I7hs", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2RegisterContextCallback);
LIB_FUNCTION("4Nj7u5B5yCA", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2RegisterLobbyEventCallback);
LIB_FUNCTION("p+2EnxmaAMM", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2RegisterRoomEventCallback);
LIB_FUNCTION("0UMeWRGnZKA", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2RegisterSignalingCallback);
LIB_FUNCTION("7vjNQ6Z1op0", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2ContextStart);
LIB_FUNCTION("LhCPctIICxQ", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2GetServerId);
LIB_FUNCTION("rJNPJqDCpiI", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2GetWorldInfoList);
LIB_FUNCTION("BD6kfx442Do", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2LeaveRoom);
LIB_FUNCTION("+8e7wXLmjds", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2SetDefaultRequestOptParam);
LIB_FUNCTION("VqZX7POg2Mk", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2SearchRoom);
LIB_FUNCTION("Iw2h0Jrrb5U", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2SendRoomMessage);
LIB_FUNCTION("meEjIdbjAA0", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2SetUserInfo);
LIB_FUNCTION("q7GK98-nYSE", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2SetRoomDataExternal);
LIB_FUNCTION("S9D8JSYIrjE", "libSceNpMatching2", 1, "libSceNpMatching2",
sceNpMatching2SetRoomDataInternal);
};
} // namespace Libraries::Np::NpMatching2

View File

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
namespace Core::Loader {
class SymbolsResolver;
}
namespace Libraries::Np::NpMatching2 {
using OrbisNpMatching2AttributeId = u16;
using OrbisNpMatching2ContextId = u16;
using OrbisNpMatching2Event = u16;
using OrbisNpMatching2EventCause = u8;
using OrbisNpMatching2Flags = u32;
using OrbisNpMatching2LobbyId = u64;
using OrbisNpMatching2NatType = u8;
using OrbisNpMatching2RequestId = u16;
using OrbisNpMatching2RoomId = u64;
using OrbisNpMatching2RoomMemberId = u16;
using OrbisNpMatching2ServerId = u16;
using OrbisNpMatching2TeamId = u8;
using OrbisNpMatching2WorldId = u32;
constexpr int ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED = 0x80550c01;
constexpr int ORBIS_NP_MATCHING2_ERROR_ALREADY_INITIALIZED = 0x80550c02;
constexpr int ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT = 0x80550c15;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_CREATE_JOIN_ROOM = 0x0101;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_CREATE_JOIN_ROOM_A = 0x7101;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_GET_WORLD_INFO_LIST = 0x0002;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SEARCH_ROOM_A = 0x7106;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_LEAVE_ROOM = 0x0103;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SEND_ROOM_MESSAGE = 0x0108;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_EXTERNAL = 0x0004;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_INTERNAL = 0x1106;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_USER_INFO = 0x0007;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_CONTEXT_EVENT_START_OVER = 0x6F01;
constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED = 0x6F02;
constexpr OrbisNpMatching2EventCause ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ERROR = 10;
constexpr OrbisNpMatching2EventCause ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ACTION = 11;
void RegisterLib(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::Np::NpMatching2

View File

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <mutex>
#include "common/logging/log.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/libs.h"
#include "core/libraries/np/np_partner.h"
#include "core/libraries/np/np_partner_error.h"
#include "core/libraries/system/userservice.h"
namespace Libraries::Np::NpPartner {
static bool g_library_init = false;
std::mutex g_library_mutex{};
/**
* Terminates the library
*/
s32 PS4_SYSV_ABI Func_A4CC5784DA33517F() {
LOG_ERROR(Lib_NpPartner, "(STUBBED) called");
if (!g_library_init) {
return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED;
}
std::scoped_lock lk{g_library_mutex};
g_library_init = false;
return ORBIS_OK;
}
/**
* Aborts requests started by Func_F8E9DB52CD425743
*/
s32 PS4_SYSV_ABI Func_A507D84D91F39CC7() {
LOG_ERROR(Lib_NpPartner, "(STUBBED) called");
if (!g_library_init) {
return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED;
}
// Request logic is unimplemented, so this does nothing.
return ORBIS_OK;
}
/**
* Initializes the library
*/
s32 PS4_SYSV_ABI Func_EC2C48E74FF19429() {
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) {
LOG_ERROR(Lib_NpPartner, "(STUBBED) called");
if (!g_library_init) {
return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED;
}
if (result == nullptr) {
return ORBIS_NP_PARTNER_ERROR_INVALID_ARGUMENT;
}
std::scoped_lock lk{g_library_mutex};
// In the real library, this creates and sends a request that checks for EA subscription,
// then waits for the request to return a response, and returns that response.
// NP signed out likely returns an error, but I haven't figured out the error code yet.
// For now, stub having no subscription.
*result = false;
return ORBIS_OK;
}
void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("pMxXhNozUX8", "libSceNpPartner001", 1, "libSceNpPartner001",
Func_A4CC5784DA33517F);
LIB_FUNCTION("pQfYTZHznMc", "libSceNpPartner001", 1, "libSceNpPartner001",
Func_A507D84D91F39CC7);
LIB_FUNCTION("7CxI50-xlCk", "libSceNpPartner001", 1, "libSceNpPartner001",
Func_EC2C48E74FF19429);
LIB_FUNCTION("+OnbUs1CV0M", "libSceNpPartner001", 1, "libSceNpPartner001",
Func_F8E9DB52CD425743);
};
} // namespace Libraries::Np::NpPartner

View File

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
namespace Core::Loader {
class SymbolsResolver;
}
namespace Libraries::Np::NpPartner {
void RegisterLib(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::Np::NpPartner

View File

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/libraries/error_codes.h"
constexpr int ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED = 0x819d0001;
constexpr int ORBIS_NP_PARTNER_ERROR_INVALID_ARGUMENT = 0x819d0002;

Some files were not shown because too many files have changed in this diff Show More