shadPS4 is an early PlayStation 4 emulator for Windows, Linux and macOS written in C++.
The emulator is still early in development, so don't expect a flawless experience. Nonetheless, the emulator can already run a number of commercial games.
@@ -37,6 +38,12 @@
Game
+
+ https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.14.0
+
+
+ https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.13.0
+
https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.12.5
diff --git a/documents/Docker Builder/.devcontainer/devcontainer.json b/documents/Docker Builder/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..1139ffa33
--- /dev/null
+++ b/documents/Docker Builder/.devcontainer/devcontainer.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/documents/Docker Builder/.docker/Dockerfile b/documents/Docker Builder/.docker/Dockerfile
new file mode 100644
index 000000000..6ca9b2da5
--- /dev/null
+++ b/documents/Docker Builder/.docker/Dockerfile
@@ -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
\ No newline at end of file
diff --git a/documents/Docker Builder/docker-compose.yml b/documents/Docker Builder/docker-compose.yml
new file mode 100644
index 000000000..39efefa72
--- /dev/null
+++ b/documents/Docker Builder/docker-compose.yml
@@ -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
diff --git a/documents/Screenshots/Windows/vscode-ext-1.png b/documents/Screenshots/Windows/vscode-ext-1.png
new file mode 100644
index 000000000..b8427b80b
Binary files /dev/null and b/documents/Screenshots/Windows/vscode-ext-1.png differ
diff --git a/documents/Screenshots/Windows/vscode-ext-2.png b/documents/Screenshots/Windows/vscode-ext-2.png
new file mode 100644
index 000000000..082e478a4
Binary files /dev/null and b/documents/Screenshots/Windows/vscode-ext-2.png differ
diff --git a/documents/Screenshots/Windows/vscode-ext-3.png b/documents/Screenshots/Windows/vscode-ext-3.png
new file mode 100644
index 000000000..c362d6490
Binary files /dev/null and b/documents/Screenshots/Windows/vscode-ext-3.png differ
diff --git a/documents/building-docker.md b/documents/building-docker.md
new file mode 100644
index 000000000..84d238751
--- /dev/null
+++ b/documents/building-docker.md
@@ -0,0 +1,100 @@
+
+
+# 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
+```
+
+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 don’t 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.
\ No newline at end of file
diff --git a/documents/building-windows.md b/documents/building-windows.md
index 88c5b6830..8251189ff 100644
--- a/documents/building-windows.md
+++ b/documents/building-windows.md
@@ -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._
+
+
+
+
+
+
+
+### 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/)
diff --git a/externals/CLI11 b/externals/CLI11
new file mode 160000
index 000000000..bf5a16a26
--- /dev/null
+++ b/externals/CLI11
@@ -0,0 +1 @@
+Subproject commit bf5a16a26a34a9a7ad75f4a7705585e44675fef0
diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt
index eb3723f2c..db03e7679 100644
--- a/externals/CMakeLists.txt
+++ b/externals/CMakeLists.txt
@@ -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()
@@ -258,9 +259,19 @@ if (WIN32)
add_subdirectory(ext-wepoll)
endif()
+if (NOT TARGET fdk-aac)
+add_subdirectory(aacdec)
+endif()
+
#nlohmann json
set(JSON_BuildTests OFF CACHE INTERNAL "")
add_subdirectory(json)
# miniz
add_subdirectory(miniz)
+
+# cli11
+set(CLI11_BUILD_TESTS OFF CACHE BOOL "" FORCE)
+set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
+
+add_subdirectory(CLI11)
\ No newline at end of file
diff --git a/externals/MoltenVK b/externals/MoltenVK
index f168dec05..f79c6c569 160000
--- a/externals/MoltenVK
+++ b/externals/MoltenVK
@@ -1 +1 @@
-Subproject commit f168dec05998ab0ca09a400bab6831a95c0bdb2e
+Subproject commit f79c6c5690d3ee06ec3a00d11a8b1bab4aa1d030
diff --git a/externals/aacdec/CMakeLists.txt b/externals/aacdec/CMakeLists.txt
new file mode 100644
index 000000000..2adfa032b
--- /dev/null
+++ b/externals/aacdec/CMakeLists.txt
@@ -0,0 +1,154 @@
+# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set(AACDEC_SRC
+ fdk-aac/libAACdec/src/FDK_delay.cpp
+ fdk-aac/libAACdec/src/aac_ram.cpp
+ fdk-aac/libAACdec/src/aac_rom.cpp
+ fdk-aac/libAACdec/src/aacdec_drc.cpp
+ fdk-aac/libAACdec/src/aacdec_hcr.cpp
+ fdk-aac/libAACdec/src/aacdec_hcr_bit.cpp
+ fdk-aac/libAACdec/src/aacdec_hcrs.cpp
+ fdk-aac/libAACdec/src/aacdec_pns.cpp
+ fdk-aac/libAACdec/src/aacdec_tns.cpp
+ fdk-aac/libAACdec/src/aacdecoder.cpp
+ fdk-aac/libAACdec/src/aacdecoder_lib.cpp
+ fdk-aac/libAACdec/src/block.cpp
+ fdk-aac/libAACdec/src/channel.cpp
+ fdk-aac/libAACdec/src/channelinfo.cpp
+ fdk-aac/libAACdec/src/conceal.cpp
+ fdk-aac/libAACdec/src/ldfiltbank.cpp
+ fdk-aac/libAACdec/src/pulsedata.cpp
+ fdk-aac/libAACdec/src/rvlc.cpp
+ fdk-aac/libAACdec/src/rvlcbit.cpp
+ fdk-aac/libAACdec/src/rvlcconceal.cpp
+ fdk-aac/libAACdec/src/stereo.cpp
+ fdk-aac/libAACdec/src/usacdec_ace_d4t64.cpp
+ fdk-aac/libAACdec/src/usacdec_ace_ltp.cpp
+ fdk-aac/libAACdec/src/usacdec_acelp.cpp
+ fdk-aac/libAACdec/src/usacdec_fac.cpp
+ fdk-aac/libAACdec/src/usacdec_lpc.cpp
+ fdk-aac/libAACdec/src/usacdec_lpd.cpp
+ fdk-aac/libAACdec/src/usacdec_rom.cpp
+)
+
+set(FDK_SRC
+ fdk-aac/libFDK/src/FDK_bitbuffer.cpp
+ fdk-aac/libFDK/src/FDK_core.cpp
+ fdk-aac/libFDK/src/FDK_crc.cpp
+ fdk-aac/libFDK/src/FDK_decorrelate.cpp
+ fdk-aac/libFDK/src/FDK_hybrid.cpp
+ fdk-aac/libFDK/src/FDK_lpc.cpp
+ fdk-aac/libFDK/src/FDK_matrixCalloc.cpp
+ fdk-aac/libFDK/src/FDK_qmf_domain.cpp
+ fdk-aac/libFDK/src/FDK_tools_rom.cpp
+ fdk-aac/libFDK/src/FDK_trigFcts.cpp
+ fdk-aac/libFDK/src/autocorr2nd.cpp
+ fdk-aac/libFDK/src/dct.cpp
+ fdk-aac/libFDK/src/fft.cpp
+ fdk-aac/libFDK/src/fft_rad2.cpp
+ fdk-aac/libFDK/src/fixpoint_math.cpp
+ fdk-aac/libFDK/src/huff_nodes.cpp
+ fdk-aac/libFDK/src/mdct.cpp
+ fdk-aac/libFDK/src/nlc_dec.cpp
+ fdk-aac/libFDK/src/qmf.cpp
+ fdk-aac/libFDK/src/scale.cpp
+)
+
+set(SYS_SRC
+ fdk-aac/libSYS/src/genericStds.cpp
+ fdk-aac/libSYS/src/syslib_channelMapDescr.cpp
+)
+
+set(ARITHCODING_SRC
+ fdk-aac/libArithCoding/src/ac_arith_coder.cpp
+)
+
+set(MPEGTPDEC_SRC
+ fdk-aac/libMpegTPDec/src/tpdec_adif.cpp
+ fdk-aac/libMpegTPDec/src/tpdec_adts.cpp
+ fdk-aac/libMpegTPDec/src/tpdec_asc.cpp
+ fdk-aac/libMpegTPDec/src/tpdec_drm.cpp
+ fdk-aac/libMpegTPDec/src/tpdec_latm.cpp
+ fdk-aac/libMpegTPDec/src/tpdec_lib.cpp
+)
+
+set(SBRDEC_SRC
+ fdk-aac/libSBRdec/src/HFgen_preFlat.cpp
+ fdk-aac/libSBRdec/src/env_calc.cpp
+ fdk-aac/libSBRdec/src/env_dec.cpp
+ fdk-aac/libSBRdec/src/env_extr.cpp
+ fdk-aac/libSBRdec/src/hbe.cpp
+ fdk-aac/libSBRdec/src/huff_dec.cpp
+ fdk-aac/libSBRdec/src/lpp_tran.cpp
+ fdk-aac/libSBRdec/src/psbitdec.cpp
+ fdk-aac/libSBRdec/src/psdec.cpp
+ fdk-aac/libSBRdec/src/psdec_drm.cpp
+ fdk-aac/libSBRdec/src/psdecrom_drm.cpp
+ fdk-aac/libSBRdec/src/pvc_dec.cpp
+ fdk-aac/libSBRdec/src/sbr_deb.cpp
+ fdk-aac/libSBRdec/src/sbr_dec.cpp
+ fdk-aac/libSBRdec/src/sbr_ram.cpp
+ fdk-aac/libSBRdec/src/sbr_rom.cpp
+ fdk-aac/libSBRdec/src/sbrdec_drc.cpp
+ fdk-aac/libSBRdec/src/sbrdec_freq_sca.cpp
+ fdk-aac/libSBRdec/src/sbrdecoder.cpp
+)
+
+set(PCMUTILS_SRC
+ fdk-aac/libPCMutils/src/limiter.cpp
+ fdk-aac/libPCMutils/src/pcm_utils.cpp
+ fdk-aac/libPCMutils/src/pcmdmx_lib.cpp
+)
+
+set(DRCDEC_SRC
+ fdk-aac/libDRCdec/src/FDK_drcDecLib.cpp
+ fdk-aac/libDRCdec/src/drcDec_gainDecoder.cpp
+ fdk-aac/libDRCdec/src/drcDec_reader.cpp
+ fdk-aac/libDRCdec/src/drcDec_rom.cpp
+ fdk-aac/libDRCdec/src/drcDec_selectionProcess.cpp
+ fdk-aac/libDRCdec/src/drcDec_tools.cpp
+ fdk-aac/libDRCdec/src/drcGainDec_init.cpp
+ fdk-aac/libDRCdec/src/drcGainDec_preprocess.cpp
+ fdk-aac/libDRCdec/src/drcGainDec_process.cpp
+)
+
+set(SACDEC_SRC
+ fdk-aac/libSACdec/src/sac_bitdec.cpp
+ fdk-aac/libSACdec/src/sac_calcM1andM2.cpp
+ fdk-aac/libSACdec/src/sac_dec.cpp
+ fdk-aac/libSACdec/src/sac_dec_conceal.cpp
+ fdk-aac/libSACdec/src/sac_dec_lib.cpp
+ fdk-aac/libSACdec/src/sac_process.cpp
+ fdk-aac/libSACdec/src/sac_qmf.cpp
+ fdk-aac/libSACdec/src/sac_reshapeBBEnv.cpp
+ fdk-aac/libSACdec/src/sac_rom.cpp
+ fdk-aac/libSACdec/src/sac_smoothing.cpp
+ fdk-aac/libSACdec/src/sac_stp.cpp
+ fdk-aac/libSACdec/src/sac_tsd.cpp
+)
+
+add_library(fdk-aac
+ ${AACDEC_SRC}
+ ${FDK_SRC}
+ ${SYS_SRC}
+ ${ARITHCODING_SRC}
+ ${MPEGTPDEC_SRC}
+ ${SBRDEC_SRC}
+ ${PCMUTILS_SRC}
+ ${DRCDEC_SRC}
+ ${SACDEC_SRC}
+)
+
+target_include_directories(fdk-aac
+ PUBLIC
+ fdk-aac/libAACdec/include
+ fdk-aac/libFDK/include
+ fdk-aac/libSYS/include
+ fdk-aac/libArithCoding/include
+ fdk-aac/libMpegTPDec/include
+ fdk-aac/libSBRdec/include
+ fdk-aac/libPCMutils/include
+ fdk-aac/libDRCdec/include
+ fdk-aac/libSACdec/include
+)
diff --git a/externals/aacdec/fdk-aac b/externals/aacdec/fdk-aac
new file mode 160000
index 000000000..ee76460ef
--- /dev/null
+++ b/externals/aacdec/fdk-aac
@@ -0,0 +1 @@
+Subproject commit ee76460efbdb147e26d804c798949c23f174460b
diff --git a/externals/ffmpeg-core b/externals/ffmpeg-core
index b0de1dcca..94dde08c8 160000
--- a/externals/ffmpeg-core
+++ b/externals/ffmpeg-core
@@ -1 +1 @@
-Subproject commit b0de1dcca26c0ebfb8011b8e59dd17fc399db0ff
+Subproject commit 94dde08c8a9e4271a93a2a7e4159e9fb05d30c0a
diff --git a/externals/fmt b/externals/fmt
index 64db979e3..ec73fb724 160000
--- a/externals/fmt
+++ b/externals/fmt
@@ -1 +1 @@
-Subproject commit 64db979e38ec644b1798e41610b28c8d2c8a2739
+Subproject commit ec73fb72477d80926c758894a3ab2cb3994fd051
diff --git a/externals/sdl3 b/externals/sdl3
index e9c2e9bfc..bdb72bb3f 160000
--- a/externals/sdl3
+++ b/externals/sdl3
@@ -1 +1 @@
-Subproject commit e9c2e9bfc3a6e1e70596f743fa9e1fc5fadabef7
+Subproject commit bdb72bb3f051de32c91f5deb439a50bfd51499dc
diff --git a/src/common/config.cpp b/src/common/config.cpp
index 94d8b488c..eac463d0a 100644
--- a/src/common/config.cpp
+++ b/src/common/config.cpp
@@ -198,7 +198,7 @@ static ConfigEntry pipelineCacheArchive(false);
static ConfigEntry isDebugDump(false);
static ConfigEntry isShaderDebug(false);
static ConfigEntry isSeparateLogFilesEnabled(false);
-static ConfigEntry isFpsColor(true);
+static ConfigEntry showFpsCounter(false);
static ConfigEntry logEnabled(true);
// GUI
@@ -222,16 +222,6 @@ static string config_version = Common::g_scm_rev;
// These entries aren't stored in the config
static bool overrideControllerColor = false;
static int controllerCustomColorRGB[3] = {0, 0, 255};
-static bool isGameRunning = false;
-static bool load_auto_patches = true;
-
-bool getGameRunning() {
- return isGameRunning;
-}
-
-void setGameRunning(bool running) {
- isGameRunning = running;
-}
std::filesystem::path getSysModulesPath() {
if (sys_modules_path.empty()) {
@@ -462,8 +452,12 @@ bool isPipelineCacheArchived() {
return pipelineCacheArchive.get();
}
-bool fpsColor() {
- return isFpsColor.get();
+bool getShowFpsCounter() {
+ return showFpsCounter.get();
+}
+
+void setShowFpsCounter(bool enable, bool is_game_specific) {
+ showFpsCounter.set(enable, is_game_specific);
}
bool isLoggingEnabled() {
@@ -846,13 +840,6 @@ void setUsbDeviceBackend(int value, bool is_game_specific) {
usbDeviceBackend.set(value, is_game_specific);
}
-bool getLoadAutoPatches() {
- return load_auto_patches;
-}
-void setLoadAutoPatches(bool enable) {
- load_auto_patches = enable;
-}
-
void load(const std::filesystem::path& path, bool is_game_specific) {
// If the configuration file does not exist, create it and return, unless it is game specific
std::error_code error;
@@ -968,7 +955,7 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
isDebugDump.setFromToml(debug, "DebugDump", is_game_specific);
isSeparateLogFilesEnabled.setFromToml(debug, "isSeparateLogFilesEnabled", is_game_specific);
isShaderDebug.setFromToml(debug, "CollectShader", is_game_specific);
- isFpsColor.setFromToml(debug, "FPSColor", is_game_specific);
+ showFpsCounter.setFromToml(debug, "showFpsCounter", is_game_specific);
logEnabled.setFromToml(debug, "logEnabled", is_game_specific);
current_version = toml::find_or(debug, "ConfigVersion", current_version);
}
@@ -1187,7 +1174,7 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
data["GPU"]["internalScreenWidth"] = internalScreenWidth.base_value;
data["GPU"]["internalScreenHeight"] = internalScreenHeight.base_value;
data["GPU"]["patchShaders"] = shouldPatchShaders.base_value;
- data["Debug"]["FPSColor"] = isFpsColor.base_value;
+ data["Debug"]["showFpsCounter"] = showFpsCounter.base_value;
}
// Sorting of TOML sections
@@ -1295,7 +1282,7 @@ void setDefaultValues(bool is_game_specific) {
internalScreenHeight.base_value = 720;
// Debug
- isFpsColor.base_value = true;
+ showFpsCounter.base_value = false;
}
}
diff --git a/src/common/config.h b/src/common/config.h
index 481ef6444..2a95e6cf0 100644
--- a/src/common/config.h
+++ b/src/common/config.h
@@ -125,7 +125,8 @@ int getSpecialPadClass();
bool getPSNSignedIn();
void setPSNSignedIn(bool sign, bool is_game_specific = false);
bool patchShaders(); // no set
-bool fpsColor(); // no set
+bool getShowFpsCounter();
+void setShowFpsCounter(bool enable, bool is_game_specific = false);
bool isNeoModeConsole();
void setNeoMode(bool enable, bool is_game_specific = false);
bool isDevKitConsole();
@@ -152,8 +153,6 @@ void setConnectedToNetwork(bool enable, bool is_game_specific = false);
void setUserName(const std::string& name, bool is_game_specific = false);
std::filesystem::path getSysModulesPath();
void setSysModulesPath(const std::filesystem::path& path);
-bool getLoadAutoPatches();
-void setLoadAutoPatches(bool enable);
enum UsbBackendType : int { Real, SkylandersPortal, InfinityBase, DimensionsToypad };
int getUsbDeviceBackend();
diff --git a/src/common/key_manager.cpp b/src/common/key_manager.cpp
new file mode 100644
index 000000000..cd0f668bf
--- /dev/null
+++ b/src/common/key_manager.cpp
@@ -0,0 +1,161 @@
+// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include
+#include
+#include
+#include
+#include "common/logging/log.h"
+#include "key_manager.h"
+#include "path_util.h"
+
+std::shared_ptr KeyManager::s_instance = nullptr;
+std::mutex KeyManager::s_mutex;
+
+// ------------------- Constructor & Singleton -------------------
+KeyManager::KeyManager() {
+ SetDefaultKeys();
+}
+KeyManager::~KeyManager() {
+ SaveToFile();
+}
+
+std::shared_ptr KeyManager::GetInstance() {
+ std::lock_guard lock(s_mutex);
+ if (!s_instance)
+ s_instance = std::make_shared();
+ return s_instance;
+}
+
+void KeyManager::SetInstance(std::shared_ptr instance) {
+ std::lock_guard 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(); // deserialize back
+}
+
+// ------------------- Defaults / Checks -------------------
+void KeyManager::SetDefaultKeys() {
+ m_keys = AllKeys{};
+}
+
+bool KeyManager::HasKeys() const {
+ return !m_keys.TrophyKeySet.ReleaseTrophyKey.empty();
+}
+
+// ------------------- Hex conversion -------------------
+std::vector KeyManager::HexStringToBytes(const std::string& hexStr) {
+ std::vector 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& 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;
+}
\ No newline at end of file
diff --git a/src/common/key_manager.h b/src/common/key_manager.h
new file mode 100644
index 000000000..8925ccbd0
--- /dev/null
+++ b/src/common/key_manager.h
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include "common/types.h"
+#include "nlohmann/json.hpp"
+
+using json = nlohmann::json;
+
+class KeyManager {
+public:
+ // ------------------- Nested keysets -------------------
+ struct TrophyKeySet {
+ std::vector ReleaseTrophyKey;
+ };
+
+ struct AllKeys {
+ KeyManager::TrophyKeySet TrophyKeySet;
+ };
+
+ // ------------------- Construction -------------------
+ KeyManager();
+ ~KeyManager();
+
+ // ------------------- Singleton -------------------
+ static std::shared_ptr GetInstance();
+ static void SetInstance(std::shared_ptr 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 HexStringToBytes(const std::string& hexStr);
+ static std::string BytesToHexString(const std::vector& bytes);
+
+private:
+ void KeysToJson(json& j) const;
+ void JsonToKeys(const json& j);
+
+ AllKeys m_keys{};
+
+ static std::shared_ptr 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> {
+ static void to_json(json& j, const std::vector& vec) {
+ j = KeyManager::BytesToHexString(vec);
+ }
+ static void from_json(const json& j, std::vector& vec) {
+ vec = KeyManager::HexStringToBytes(j.get());
+ }
+};
+} // namespace nlohmann
diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp
index 4a85c4cde..d7c816da3 100644
--- a/src/common/logging/backend.cpp
+++ b/src/common/logging/backend.cpp
@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include
@@ -97,6 +98,7 @@ private:
std::size_t bytes_written = 0;
};
+#ifdef _WIN32
/**
* Backend that writes to Visual Studio's output window
*/
@@ -107,15 +109,14 @@ public:
~DebuggerBackend() = default;
void Write(const Entry& entry) {
-#ifdef _WIN32
::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str());
-#endif
}
void Flush() {}
void EnableForStacktrace() {}
};
+#endif
bool initialization_in_progress_suppress_logging = true;
@@ -219,6 +220,7 @@ public:
.line_num = line_num,
.function = function,
.message = std::move(message),
+ .thread = Common::GetCurrentThreadName(),
};
if (Config::getLogType() == "async") {
message_queue.EmplaceWait(entry);
@@ -266,7 +268,9 @@ private:
}
void ForEachBackend(auto lambda) {
- // lambda(debugger_backend);
+#ifdef _WIN32
+ lambda(debugger_backend);
+#endif
lambda(color_console_backend);
lambda(file_backend);
}
@@ -279,7 +283,9 @@ private:
static inline bool should_append{false};
Filter filter;
+#ifdef _WIN32
DebuggerBackend debugger_backend{};
+#endif
ColorConsoleBackend color_console_backend{};
FileBackend file_backend;
diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp
index fd8386aff..9a3fe0aa1 100644
--- a/src/common/logging/filter.cpp
+++ b/src/common/logging/filter.cpp
@@ -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
@@ -106,15 +107,20 @@ 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) \
SUB(Lib, Rtc) \
+ SUB(Lib, Rudp) \
SUB(Lib, DiscMap) \
SUB(Lib, Png) \
SUB(Lib, Jpeg) \
@@ -157,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...
diff --git a/src/common/logging/log_entry.h b/src/common/logging/log_entry.h
index cd4ae9355..6c529f878 100644
--- a/src/common/logging/log_entry.h
+++ b/src/common/logging/log_entry.h
@@ -21,6 +21,7 @@ struct Entry {
u32 line_num = 0;
std::string function;
std::string message;
+ std::string thread;
};
} // namespace Common::Log
diff --git a/src/common/logging/text_formatter.cpp b/src/common/logging/text_formatter.cpp
index b4fa204bc..e8c5f4979 100644
--- a/src/common/logging/text_formatter.cpp
+++ b/src/common/logging/text_formatter.cpp
@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include
@@ -23,8 +24,8 @@ std::string FormatLogMessage(const Entry& entry) {
const char* class_name = GetLogClassName(entry.log_class);
const char* level_name = GetLevelName(entry.log_level);
- return fmt::format("[{}] <{}> {}:{} {}: {}", class_name, level_name, entry.filename,
- entry.line_num, entry.function, entry.message);
+ return fmt::format("[{}] <{}> ({}) {}:{} {}: {}", class_name, level_name, entry.thread,
+ entry.filename, entry.line_num, entry.function, entry.message);
}
void PrintMessage(const Entry& entry) {
diff --git a/src/common/logging/types.h b/src/common/logging/types.h
index 82db477ed..9e176c698 100644
--- a/src/common/logging/types.h
+++ b/src/common/logging/types.h
@@ -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,15 +74,19 @@ 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
Lib_NpSnsFacebookDialog, ///< The LibSceNpSnsFacebookDialog implementation
Lib_Screenshot, ///< The LibSceScreenshot implementation
Lib_LibCInternal, ///< The LibCInternal implementation.
Lib_AppContent, ///< The LibSceAppContent implementation.
Lib_Rtc, ///< The LibSceRtc implementation.
+ Lib_Rudp, ///< The LibSceRudp implementation.
Lib_DiscMap, ///< The LibSceDiscMap implementation.
Lib_Png, ///< The LibScePng implementation.
Lib_Jpeg, ///< The LibSceJpeg implementation.
@@ -107,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.
@@ -125,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
};
diff --git a/src/common/memory_patcher.cpp b/src/common/memory_patcher.cpp
index 045a530cb..a7c020246 100644
--- a/src/common/memory_patcher.cpp
+++ b/src/common/memory_patcher.cpp
@@ -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
@@ -12,6 +12,7 @@
#include "common/elf_info.h"
#include "common/logging/log.h"
#include "common/path_util.h"
+#include "core/emulator_state.h"
#include "core/file_format/psf.h"
#include "memory_patcher.h"
@@ -51,14 +52,14 @@ std::string convertValueToHex(const std::string type, const std::string valueStr
uint32_t i;
} floatUnion;
floatUnion.f = std::stof(valueStr);
- result = toHex(floatUnion.i, sizeof(floatUnion.i));
+ result = toHex(std::byteswap(floatUnion.i), sizeof(floatUnion.i));
} else if (type == "float64") {
union {
double d;
uint64_t i;
} doubleUnion;
doubleUnion.d = std::stod(valueStr);
- result = toHex(doubleUnion.i, sizeof(doubleUnion.i));
+ result = toHex(std::byteswap(doubleUnion.i), sizeof(doubleUnion.i));
} else if (type == "utf8") {
std::vector byteArray =
std::vector(valueStr.begin(), valueStr.end());
@@ -192,7 +193,7 @@ void OnGameLoaded() {
} else {
ApplyPatchesFromXML(file_path);
}
- } else if (Config::getLoadAutoPatches()) {
+ } else if (EmulatorState::GetInstance()->IsAutoPatchesLoadEnabled()) {
for (auto const& repo : std::filesystem::directory_iterator(patch_dir)) {
if (!repo.is_directory()) {
continue;
diff --git a/src/common/shared_first_mutex.h b/src/common/shared_first_mutex.h
index b150c956b..fcf9d0c4f 100644
--- a/src/common/shared_first_mutex.h
+++ b/src/common/shared_first_mutex.h
@@ -17,6 +17,15 @@ public:
writer_active = true;
}
+ bool try_lock() {
+ std::lock_guard lock(mtx);
+ if (writer_active || readers > 0) {
+ return false;
+ }
+ writer_active = true;
+ return true;
+ }
+
void unlock() {
std::lock_guard lock(mtx);
writer_active = false;
diff --git a/src/common/thread.cpp b/src/common/thread.cpp
index 982041ebb..e56953fb6 100644
--- a/src/common/thread.cpp
+++ b/src/common/thread.cpp
@@ -1,11 +1,14 @@
// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include
#include
#include
+#include "core/libraries/kernel/threads/pthread.h"
+
#include "common/error.h"
#include "common/logging/log.h"
#include "common/thread.h"
@@ -171,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());
}
@@ -183,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__)
@@ -209,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
}
@@ -237,4 +249,22 @@ void AccurateTimer::End() {
target_interval - std::chrono::duration_cast(now - start_time);
}
+std::string GetCurrentThreadName() {
+ using namespace Libraries::Kernel;
+ if (g_curthread && !g_curthread->name.empty()) {
+ return g_curthread->name;
+ }
+#ifdef _WIN32
+ PWSTR name;
+ GetThreadDescription(GetCurrentThread(), &name);
+ return Common::UTF16ToUTF8(name);
+#else
+ char name[256];
+ if (pthread_getname_np(pthread_self(), name, sizeof(name)) != 0) {
+ return "";
+ }
+ return std::string{name};
+#endif
+}
+
} // namespace Common
diff --git a/src/common/thread.h b/src/common/thread.h
index 5bd83d35c..a300d10c3 100644
--- a/src/common/thread.h
+++ b/src/common/thread.h
@@ -1,5 +1,6 @@
// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -46,4 +47,6 @@ public:
}
};
+std::string GetCurrentThreadName();
+
} // namespace Common
diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp
index 3f063ea76..194f676f9 100644
--- a/src/core/address_space.cpp
+++ b/src/core/address_space.cpp
@@ -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(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(fd) : backing_handle;
- if (fd && prot == PAGE_READONLY) {
+ HANDLE backing = fd != -1 ? reinterpret_cast(fd) : backing_handle;
+ if (fd != -1 && prot == PAGE_READONLY) {
DWORD resultvar;
ptr = VirtualAlloc2(process, reinterpret_cast(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, ¶m);
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(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(virtual_addr),
MEM_PRESERVE_PLACEHOLDER);
} else {
ret = VirtualFreeEx(process, reinterpret_cast(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 ®ion;
+ // 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(®ion);
+ }
+
+ // 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 ®ion;
+
+ // 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(®ion);
+ }
+
+ 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(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(®ion);
+ }
+
+ // 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 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 m_free_regions;
};
#endif
@@ -675,8 +771,7 @@ AddressSpace::AddressSpace() : impl{std::make_unique()} {
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(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);
diff --git a/src/core/address_space.h b/src/core/address_space.h
index 5c50039bd..b71f66f28 100644
--- a/src/core/address_space.h
+++ b/src/core/address_space.h
@@ -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 GetUsableRegions();
@@ -93,11 +92,11 @@ private:
std::unique_ptr 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
diff --git a/src/core/cpu_patches.cpp b/src/core/cpu_patches.cpp
index 8c0897a48..e303417c3 100644
--- a/src/core/cpu_patches.cpp
+++ b/src/core/cpu_patches.cpp
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -122,6 +123,30 @@ static void GenerateTcbAccess(void* /* address */, const ZydisDecodedOperand* op
#endif
}
+static bool FilterStackCheck(const ZydisDecodedOperand* operands) {
+ const auto& dst_op = operands[0];
+ const auto& src_op = operands[1];
+
+ // Some compilers emit stack checks by starting a function with
+ // 'mov (64-bit register), fs:[0x28]', then checking with `xor (64-bit register), fs:[0x28]`
+ return src_op.type == ZYDIS_OPERAND_TYPE_MEMORY && src_op.mem.segment == ZYDIS_REGISTER_FS &&
+ src_op.mem.base == ZYDIS_REGISTER_NONE && src_op.mem.index == ZYDIS_REGISTER_NONE &&
+ src_op.mem.disp.value == 0x28 && dst_op.reg.value >= ZYDIS_REGISTER_RAX &&
+ dst_op.reg.value <= ZYDIS_REGISTER_R15;
+}
+
+static void GenerateStackCheck(void* /* address */, const ZydisDecodedOperand* operands,
+ Xbyak::CodeGenerator& c) {
+ const auto dst = ZydisToXbyakRegisterOperand(operands[0]);
+ c.xor_(dst, 0);
+}
+
+static void GenerateStackCanary(void* /* address */, const ZydisDecodedOperand* operands,
+ Xbyak::CodeGenerator& c) {
+ const auto dst = ZydisToXbyakRegisterOperand(operands[0]);
+ c.mov(dst, 0);
+}
+
static bool FilterNoSSE4a(const ZydisDecodedOperand*) {
Cpu cpu;
return !cpu.has(Cpu::tSSE4a);
@@ -440,18 +465,26 @@ struct PatchInfo {
bool trampoline;
};
-static const std::unordered_map Patches = {
+static const std::unordered_map> Patches = {
// SSE4a
- {ZYDIS_MNEMONIC_EXTRQ, {FilterNoSSE4a, GenerateEXTRQ, true}},
- {ZYDIS_MNEMONIC_INSERTQ, {FilterNoSSE4a, GenerateINSERTQ, true}},
- {ZYDIS_MNEMONIC_MOVNTSS, {FilterNoSSE4a, ReplaceMOVNTSS, false}},
- {ZYDIS_MNEMONIC_MOVNTSD, {FilterNoSSE4a, ReplaceMOVNTSD, false}},
+ {ZYDIS_MNEMONIC_EXTRQ, {{FilterNoSSE4a, GenerateEXTRQ, true}}},
+ {ZYDIS_MNEMONIC_INSERTQ, {{FilterNoSSE4a, GenerateINSERTQ, true}}},
+ {ZYDIS_MNEMONIC_MOVNTSS, {{FilterNoSSE4a, ReplaceMOVNTSS, false}}},
+ {ZYDIS_MNEMONIC_MOVNTSD, {{FilterNoSSE4a, ReplaceMOVNTSD, false}}},
+#if !defined(__APPLE__)
+ // FS segment patches
+ // These first two patches are for accesses to the stack canary, fs:[0x28]
+ {ZYDIS_MNEMONIC_XOR, {{FilterStackCheck, GenerateStackCheck, false}}},
+ {ZYDIS_MNEMONIC_MOV,
+ {{FilterStackCheck, GenerateStackCanary, false},
#if defined(_WIN32)
- // Windows needs a trampoline.
- {ZYDIS_MNEMONIC_MOV, {FilterTcbAccess, GenerateTcbAccess, true}},
-#elif !defined(__APPLE__)
- {ZYDIS_MNEMONIC_MOV, {FilterTcbAccess, GenerateTcbAccess, false}},
+ // Windows needs a trampoline for Tcb accesses.
+ {FilterTcbAccess, GenerateTcbAccess, true}
+#else
+ {FilterTcbAccess, GenerateTcbAccess, false}
+#endif
+ }},
#endif
};
@@ -503,51 +536,53 @@ static std::pair TryPatch(u8* code, PatchModule* module) {
}
if (Patches.contains(instruction.mnemonic)) {
- const auto& patch_info = Patches.at(instruction.mnemonic);
- bool needs_trampoline = patch_info.trampoline;
- if (patch_info.filter(operands)) {
- auto& patch_gen = module->patch_gen;
+ const auto& patches = Patches.at(instruction.mnemonic);
+ for (const auto& patch_info : patches) {
+ bool needs_trampoline = patch_info.trampoline;
+ if (patch_info.filter(operands)) {
+ auto& patch_gen = module->patch_gen;
- if (needs_trampoline && instruction.length < 5) {
- // Trampoline is needed but instruction is too short to patch.
- // Return false and length to signal to AOT compilation that this instruction
- // should be skipped and handled at runtime.
- return std::make_pair(false, instruction.length);
- }
+ if (needs_trampoline && instruction.length < 5) {
+ // Trampoline is needed but instruction is too short to patch.
+ // Return false and length to signal to AOT compilation that this instruction
+ // should be skipped and handled at runtime.
+ return std::make_pair(false, instruction.length);
+ }
- // Reset state and move to current code position.
- patch_gen.reset();
- patch_gen.setSize(code - patch_gen.getCode());
+ // Reset state and move to current code position.
+ patch_gen.reset();
+ patch_gen.setSize(code - patch_gen.getCode());
- if (needs_trampoline) {
- auto& trampoline_gen = module->trampoline_gen;
- const auto trampoline_ptr = trampoline_gen.getCurr();
+ if (needs_trampoline) {
+ auto& trampoline_gen = module->trampoline_gen;
+ const auto trampoline_ptr = trampoline_gen.getCurr();
- patch_info.generator(code, operands, trampoline_gen);
+ patch_info.generator(code, operands, trampoline_gen);
- // Return to the following instruction at the end of the trampoline.
- trampoline_gen.jmp(code + instruction.length);
+ // Return to the following instruction at the end of the trampoline.
+ trampoline_gen.jmp(code + instruction.length);
- // Replace instruction with near jump to the trampoline.
- patch_gen.jmp(trampoline_ptr, Xbyak::CodeGenerator::LabelType::T_NEAR);
- } else {
- patch_info.generator(code, operands, patch_gen);
- }
+ // Replace instruction with near jump to the trampoline.
+ patch_gen.jmp(trampoline_ptr, Xbyak::CodeGenerator::LabelType::T_NEAR);
+ } else {
+ patch_info.generator(code, operands, patch_gen);
+ }
- const auto patch_size = patch_gen.getCurr() - code;
- if (patch_size > 0) {
- ASSERT_MSG(instruction.length >= patch_size,
- "Instruction {} with length {} is too short to replace at: {}",
- ZydisMnemonicGetString(instruction.mnemonic), instruction.length,
- fmt::ptr(code));
+ const auto patch_size = patch_gen.getCurr() - code;
+ if (patch_size > 0) {
+ ASSERT_MSG(instruction.length >= patch_size,
+ "Instruction {} with length {} is too short to replace at: {}",
+ ZydisMnemonicGetString(instruction.mnemonic), instruction.length,
+ fmt::ptr(code));
- // Fill remaining space with nops.
- patch_gen.nop(instruction.length - patch_size);
+ // Fill remaining space with nops.
+ patch_gen.nop(instruction.length - patch_size);
- module->patched.insert(code);
- LOG_DEBUG(Core, "Patched instruction '{}' at: {}",
- ZydisMnemonicGetString(instruction.mnemonic), fmt::ptr(code));
- return std::make_pair(true, instruction.length);
+ module->patched.insert(code);
+ LOG_DEBUG(Core, "Patched instruction '{}' at: {}",
+ ZydisMnemonicGetString(instruction.mnemonic), fmt::ptr(code));
+ return std::make_pair(true, instruction.length);
+ }
}
}
}
@@ -755,11 +790,12 @@ static bool PatchesIllegalInstructionHandler(void* context) {
Common::Decoder::Instance()->decodeInstruction(instruction, operands, code_address);
if (ZYAN_SUCCESS(status) && instruction.mnemonic == ZydisMnemonic::ZYDIS_MNEMONIC_UD2)
[[unlikely]] {
- UNREACHABLE_MSG("ud2 at code address {:#x}", (u64)code_address);
+ UNREACHABLE_MSG("ud2 at code address {:#x}", reinterpret_cast(code_address));
}
- LOG_ERROR(Core, "Failed to patch address {:x} -- mnemonic: {}", (u64)code_address,
- ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic)
- : "Failed to decode");
+ UNREACHABLE_MSG("Failed to patch address {:x} -- mnemonic: {}",
+ reinterpret_cast(code_address),
+ ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic)
+ : "Failed to decode");
}
}
diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp
index 1fb810030..928040fec 100644
--- a/src/core/devtools/layer.cpp
+++ b/src/core/devtools/layer.cpp
@@ -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 "layer.h"
@@ -11,6 +11,7 @@
#include "common/singleton.h"
#include "common/types.h"
#include "core/debug_state.h"
+#include "core/emulator_state.h"
#include "imgui/imgui_std.h"
#include "imgui_internal.h"
#include "options.h"
@@ -273,14 +274,10 @@ void L::DrawAdvanced() {
void L::DrawSimple() {
const float frameRate = DebugState.Framerate;
- if (Config::fpsColor()) {
- if (frameRate < 10) {
- PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); // Red
- } else if (frameRate >= 10 && frameRate < 20) {
- PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.0f, 1.0f)); // Orange
- } else {
- PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); // White
- }
+ if (frameRate < 10) {
+ PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); // Red
+ } else if (frameRate >= 10 && frameRate < 20) {
+ PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.0f, 1.0f)); // Orange
} else {
PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); // White
}
@@ -311,6 +308,7 @@ static void LoadSettings(const char* line) {
void L::SetupSettings() {
frame_graph.is_open = true;
+ show_simple_fps = Config::getShowFpsCounter();
using SettingLoader = void (*)(const char*);
@@ -475,6 +473,11 @@ void ToggleSimpleFps() {
visibility_toggled = true;
}
+void SetSimpleFps(bool enabled) {
+ show_simple_fps = enabled;
+ visibility_toggled = true;
+}
+
void ToggleQuitWindow() {
show_quit_window = !show_quit_window;
}
diff --git a/src/core/devtools/layer.h b/src/core/devtools/layer.h
index 44afc95bc..96b48a7f0 100644
--- a/src/core/devtools/layer.h
+++ b/src/core/devtools/layer.h
@@ -30,6 +30,7 @@ private:
namespace Overlay {
void ToggleSimpleFps();
+void SetSimpleFps(bool enabled);
void ToggleQuitWindow();
} // namespace Overlay
diff --git a/src/core/devtools/widget/memory_map.cpp b/src/core/devtools/widget/memory_map.cpp
index 278c6595c..d1d1eb410 100644
--- a/src/core/devtools/widget/memory_map.cpp
+++ b/src/core/devtools/widget/memory_map.cpp
@@ -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;
}
diff --git a/src/core/devtools/widget/memory_map.h b/src/core/devtools/widget/memory_map.h
index cc7697c8c..3bbec4643 100644
--- a/src/core/devtools/widget/memory_map.h
+++ b/src/core/devtools/widget/memory_map.h
@@ -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;
diff --git a/src/core/emulator_state.cpp b/src/core/emulator_state.cpp
new file mode 100644
index 000000000..1f02043a3
--- /dev/null
+++ b/src/core/emulator_state.cpp
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "emulator_state.h"
+
+std::shared_ptr EmulatorState::s_instance = nullptr;
+std::mutex EmulatorState::s_mutex;
+
+EmulatorState::EmulatorState() {}
+
+EmulatorState::~EmulatorState() {}
+
+std::shared_ptr EmulatorState::GetInstance() {
+ std::lock_guard lock(s_mutex);
+ if (!s_instance)
+ s_instance = std::make_shared();
+ return s_instance;
+}
+
+void EmulatorState::SetInstance(std::shared_ptr instance) {
+ std::lock_guard lock(s_mutex);
+ s_instance = instance;
+}
+
+bool EmulatorState::IsGameRunning() const {
+ return m_running;
+}
+void EmulatorState::SetGameRunning(bool running) {
+ m_running = running;
+}
+
+bool EmulatorState::IsAutoPatchesLoadEnabled() const {
+ return m_load_patches_auto;
+}
+void EmulatorState::SetAutoPatchesLoadEnabled(bool enable) {
+ m_load_patches_auto = enable;
+}
diff --git a/src/core/emulator_state.h b/src/core/emulator_state.h
new file mode 100644
index 000000000..c12af5401
--- /dev/null
+++ b/src/core/emulator_state.h
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+
+class EmulatorState {
+public:
+ EmulatorState();
+ ~EmulatorState();
+
+ static std::shared_ptr GetInstance();
+ static void SetInstance(std::shared_ptr instance);
+
+ bool IsGameRunning() const;
+ void SetGameRunning(bool running);
+ bool IsAutoPatchesLoadEnabled() const;
+ void SetAutoPatchesLoadEnabled(bool enable);
+
+private:
+ static std::shared_ptr s_instance;
+ static std::mutex s_mutex;
+
+ // state variables
+ bool m_running = false;
+ bool m_load_patches_auto = true;
+};
\ No newline at end of file
diff --git a/src/core/file_format/npbind.cpp b/src/core/file_format/npbind.cpp
new file mode 100644
index 000000000..b2900efa0
--- /dev/null
+++ b/src/core/file_format/npbind.cpp
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include
+#include
+#include
+#include
+#include
+#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 buf(static_cast(sz));
+ if (!f.read(reinterpret_cast(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(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 NPBindFile::GetNpCommIds() const {
+ std::vector 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(body.npcommid.data.data()),
+ body.npcommid.data.size());
+ npcommids.push_back(raw_string);
+ }
+ }
+
+ return npcommids;
+}
\ No newline at end of file
diff --git a/src/core/file_format/npbind.h b/src/core/file_format/npbind.h
new file mode 100644
index 000000000..44d6528bd
--- /dev/null
+++ b/src/core/file_format/npbind.h
@@ -0,0 +1,87 @@
+// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+#include
+#include
+#include
+#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 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 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& Bodies() const {
+ return m_bodies;
+ }
+ const u8* Digest() const {
+ return m_digest;
+ }
+
+ // Get npcommid data
+ std::vector 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));
+ }
+};
\ No newline at end of file
diff --git a/src/core/file_format/trp.cpp b/src/core/file_format/trp.cpp
index 9d37b957e..f0a258c12 100644
--- a/src/core/file_format/trp.cpp
+++ b/src/core/file_format/trp.cpp
@@ -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 trophyKey, std::span NPcommID,
- std::span efsmIv, std::span ciphertext,
+static void DecryptEFSM(std::span trophyKey, std::span NPcommID,
+ std::span efsmIv, std::span ciphertext,
std::span decrypted) {
// Step 1: Encrypt NPcommID
std::array trophyIv{};
std::array 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(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(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& vec) {
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
if (*it == '>') {
@@ -59,95 +46,236 @@ static void hexToBytes(const char* hex, unsigned char* dst) {
bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string titleId) {
std::filesystem::path gameSysDir = trophyPath / "sce_sys/trophy/";
if (!std::filesystem::exists(gameSysDir)) {
- LOG_CRITICAL(Common_Filesystem, "Game sce_sys directory doesn't exist");
+ LOG_WARNING(Common_Filesystem, "Game trophy directory doesn't exist");
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 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(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(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 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 ESFM(entry.entry_len - iv_len);
- std::vector 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(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 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& 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 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 ESFM(entry.entry_len - IV_LEN);
+ std::vector 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 key_span(user_key);
+
+ // Convert npCommId string to span (pad or truncate to 16 bytes)
+ std::array npcommid_array{};
+ size_t copy_len = std::min(npCommId.size(), npcommid_array.size());
+ std::memcpy(npcommid_array.data(), npCommId.data(), copy_len);
+ std::span 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;
+}
\ No newline at end of file
diff --git a/src/core/file_format/trp.h b/src/core/file_format/trp.h
index 01207475b..2b52a4d57 100644
--- a/src/core/file_format/trp.h
+++ b/src/core/file_format/trp.h
@@ -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& user_key, const std::string& npCommId);
+
std::vector NPcommID = std::vector(12);
std::array np_comm_id{};
std::array esfmIv{};
diff --git a/src/core/file_sys/directories/base_directory.cpp b/src/core/file_sys/directories/base_directory.cpp
index c709da6a2..75f67577c 100644
--- a/src/core/file_sys/directories/base_directory.cpp
+++ b/src/core/file_sys/directories/base_directory.cpp
@@ -5,6 +5,7 @@
#include "common/singleton.h"
#include "core/file_sys/directories/base_directory.h"
#include "core/file_sys/fs.h"
+#include "core/libraries/kernel/orbis_error.h"
namespace Core::Directories {
@@ -12,4 +13,35 @@ BaseDirectory::BaseDirectory() = default;
BaseDirectory::~BaseDirectory() = default;
+s64 BaseDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) {
+ s64 bytes_read = 0;
+ for (s32 i = 0; i < iovcnt; i++) {
+ const s64 result = read(iov[i].iov_base, iov[i].iov_len);
+ if (result < 0) {
+ return result;
+ }
+ bytes_read += result;
+ }
+ return bytes_read;
+}
+
+s64 BaseDirectory::preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) {
+ const u64 old_file_pointer = file_offset;
+ file_offset = offset;
+ const s64 bytes_read = readv(iov, iovcnt);
+ file_offset = old_file_pointer;
+ return bytes_read;
+}
+
+s64 BaseDirectory::lseek(s64 offset, s32 whence) {
+
+ s64 file_offset_new = ((0 == whence) * offset) + ((1 == whence) * (file_offset + offset)) +
+ ((2 == whence) * (directory_size + offset));
+ if (file_offset_new < 0)
+ return ORBIS_KERNEL_ERROR_EINVAL;
+
+ file_offset = file_offset_new;
+ return file_offset;
+}
+
} // namespace Core::Directories
\ No newline at end of file
diff --git a/src/core/file_sys/directories/base_directory.h b/src/core/file_sys/directories/base_directory.h
index b412865a2..832b8ac40 100644
--- a/src/core/file_sys/directories/base_directory.h
+++ b/src/core/file_sys/directories/base_directory.h
@@ -19,6 +19,17 @@ struct OrbisKernelDirent;
namespace Core::Directories {
class BaseDirectory {
+protected:
+ static inline u32 fileno_pool{10};
+
+ static u32 next_fileno() {
+ return ++fileno_pool;
+ }
+
+ s64 file_offset = 0;
+ u64 directory_size = 0;
+ std::vector dirent_cache_bin{};
+
public:
explicit BaseDirectory();
@@ -28,13 +39,8 @@ public:
return ORBIS_KERNEL_ERROR_EBADF;
}
- virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) {
- return ORBIS_KERNEL_ERROR_EBADF;
- }
-
- virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) {
- return ORBIS_KERNEL_ERROR_EBADF;
- }
+ virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt);
+ virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset);
virtual s64 write(const void* buf, u64 nbytes) {
return ORBIS_KERNEL_ERROR_EBADF;
@@ -48,9 +54,7 @@ public:
return ORBIS_KERNEL_ERROR_EBADF;
}
- virtual s64 lseek(s64 offset, s32 whence) {
- return ORBIS_KERNEL_ERROR_EBADF;
- }
+ virtual s64 lseek(s64 offset, s32 whence);
virtual s32 fstat(Libraries::Kernel::OrbisKernelStat* stat) {
return ORBIS_KERNEL_ERROR_EBADF;
diff --git a/src/core/file_sys/directories/normal_directory.cpp b/src/core/file_sys/directories/normal_directory.cpp
index a7d76074a..3ed7c9492 100644
--- a/src/core/file_sys/directories/normal_directory.cpp
+++ b/src/core/file_sys/directories/normal_directory.cpp
@@ -15,111 +15,30 @@ std::shared_ptr NormalDirectory::Create(std::string_view guest_di
std::make_shared(guest_directory));
}
-NormalDirectory::NormalDirectory(std::string_view guest_directory) {
- auto* mnt = Common::Singleton::Instance();
-
- static s32 fileno = 0;
- mnt->IterateDirectory(guest_directory, [this](const auto& ent_path, const auto ent_is_file) {
- auto& dirent = dirents.emplace_back();
- dirent.d_fileno = ++fileno;
- dirent.d_type = (ent_is_file ? 8 : 4);
- strncpy(dirent.d_name, ent_path.filename().string().data(), MAX_LENGTH + 1);
- dirent.d_namlen = ent_path.filename().string().size();
-
- // Calculate the appropriate length for this dirent.
- // Account for the null terminator in d_name too.
- dirent.d_reclen = Common::AlignUp(sizeof(dirent.d_fileno) + sizeof(dirent.d_type) +
- sizeof(dirent.d_namlen) + sizeof(dirent.d_reclen) +
- (dirent.d_namlen + 1),
- 4);
-
- directory_size += dirent.d_reclen;
- });
-
- // The last entry of a normal directory should have d_reclen covering the remaining data.
- // Since the dirents of a folder are constant by this point, we can modify the last dirent
- // before creating the emulated file buffer.
- const u64 filler_count = Common::AlignUp(directory_size, DIRECTORY_ALIGNMENT) - directory_size;
- dirents[dirents.size() - 1].d_reclen += filler_count;
-
- // Reading from standard directories seems to be based around file pointer logic.
- // Keep an internal buffer representing the raw contents of this file descriptor,
- // then emulate the various read functions with that.
- directory_size = Common::AlignUp(directory_size, DIRECTORY_ALIGNMENT);
- data_buffer.reserve(directory_size);
- memset(data_buffer.data(), 0, directory_size);
-
- u8* current_dirent = data_buffer.data();
- for (const NormalDirectoryDirent& dirent : dirents) {
- NormalDirectoryDirent* dirent_to_write =
- reinterpret_cast(current_dirent);
- dirent_to_write->d_fileno = dirent.d_fileno;
-
- // Using size d_namlen + 1 to account for null terminator.
- strncpy(dirent_to_write->d_name, dirent.d_name, dirent.d_namlen + 1);
- dirent_to_write->d_namlen = dirent.d_namlen;
- dirent_to_write->d_reclen = dirent.d_reclen;
- dirent_to_write->d_type = dirent.d_type;
-
- current_dirent += dirent.d_reclen;
- }
+NormalDirectory::NormalDirectory(std::string_view guest_directory)
+ : guest_directory(guest_directory) {
+ RebuildDirents();
}
s64 NormalDirectory::read(void* buf, u64 nbytes) {
- // Nothing left to read.
- if (file_offset >= directory_size) {
- return ORBIS_OK;
- }
+ RebuildDirents();
- const s64 remaining_data = directory_size - file_offset;
- const s64 bytes = nbytes > remaining_data ? remaining_data : nbytes;
+ // data is contiguous. read goes like any regular file would: start at offset, read n bytes
+ // output is always aligned up to 512 bytes with 0s
+ // offset - classic. however at the end of read any unused (exceeding dirent buffer size) buffer
+ // space will be left untouched
+ // reclen always sums up to end of current alignment
- std::memcpy(buf, data_buffer.data() + file_offset, bytes);
+ s64 bytes_available = this->dirent_cache_bin.size() - file_offset;
+ if (bytes_available <= 0)
+ return 0;
+ bytes_available = std::min(bytes_available, static_cast(nbytes));
- file_offset += bytes;
- return bytes;
-}
+ // data
+ memcpy(buf, this->dirent_cache_bin.data() + file_offset, bytes_available);
-s64 NormalDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) {
- s64 bytes_read = 0;
- for (s32 i = 0; i < iovcnt; i++) {
- const s64 result = read(iov[i].iov_base, iov[i].iov_len);
- if (result < 0) {
- return result;
- }
- bytes_read += result;
- }
- return bytes_read;
-}
-
-s64 NormalDirectory::preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt,
- s64 offset) {
- const u64 old_file_pointer = file_offset;
- file_offset = offset;
- const s64 bytes_read = readv(iov, iovcnt);
- file_offset = old_file_pointer;
- return bytes_read;
-}
-
-s64 NormalDirectory::lseek(s64 offset, s32 whence) {
- switch (whence) {
- case 0: {
- file_offset = offset;
- break;
- }
- case 1: {
- file_offset += offset;
- break;
- }
- case 2: {
- file_offset = directory_size + offset;
- break;
- }
- default: {
- UNREACHABLE_MSG("lseek with unknown whence {}", whence);
- }
- }
- return file_offset;
+ file_offset += bytes_available;
+ return bytes_available;
}
s32 NormalDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) {
@@ -131,10 +50,110 @@ s32 NormalDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) {
}
s64 NormalDirectory::getdents(void* buf, u64 nbytes, s64* basep) {
- if (basep != nullptr) {
+ RebuildDirents();
+
+ if (basep)
*basep = file_offset;
+
+ // same as others, we just don't need a variable
+ if (file_offset >= directory_size)
+ return 0;
+
+ s64 bytes_written = 0;
+ s64 working_offset = file_offset;
+ s64 dirent_buffer_offset = 0;
+ s64 aligned_count = Common::AlignDown(nbytes, 512);
+
+ const u8* dirent_buffer = this->dirent_cache_bin.data();
+ while (dirent_buffer_offset < this->dirent_cache_bin.size()) {
+ const u8* normal_dirent_ptr = dirent_buffer + dirent_buffer_offset;
+ const NormalDirectoryDirent* normal_dirent =
+ reinterpret_cast(normal_dirent_ptr);
+ auto d_reclen = normal_dirent->d_reclen;
+
+ // bad, incomplete or OOB entry
+ if (normal_dirent->d_namlen == 0)
+ break;
+
+ if (working_offset >= d_reclen) {
+ dirent_buffer_offset += d_reclen;
+ working_offset -= d_reclen;
+ continue;
+ }
+
+ if ((bytes_written + d_reclen) > aligned_count)
+ // dirents are aligned to the last full one
+ break;
+
+ memcpy(static_cast(buf) + bytes_written, normal_dirent_ptr + working_offset,
+ d_reclen - working_offset);
+ bytes_written += d_reclen - working_offset;
+ dirent_buffer_offset += d_reclen;
+ working_offset = 0;
}
- // read behaves identically to getdents for normal directories.
- return read(buf, nbytes);
+
+ file_offset += bytes_written;
+ return bytes_written;
}
+
+void NormalDirectory::RebuildDirents() {
+ // regenerate only when target wants to read contents again
+ // no reason for testing - read is always raw and dirents get processed on the go
+ if (previous_file_offset == file_offset)
+ return;
+ previous_file_offset = file_offset;
+
+ constexpr u32 dirent_meta_size =
+ sizeof(NormalDirectoryDirent::d_fileno) + sizeof(NormalDirectoryDirent::d_type) +
+ sizeof(NormalDirectoryDirent::d_namlen) + sizeof(NormalDirectoryDirent::d_reclen);
+
+ u64 next_ceiling = 0;
+ u64 dirent_offset = 0;
+ u64 last_reclen_offset = 4;
+ dirent_cache_bin.clear();
+ dirent_cache_bin.reserve(512);
+
+ auto* mnt = Common::Singleton::Instance();
+
+ mnt->IterateDirectory(
+ guest_directory, [this, &next_ceiling, &dirent_offset, &last_reclen_offset](
+ const std::filesystem::path& ent_path, const bool ent_is_file) {
+ NormalDirectoryDirent tmp{};
+ std::string leaf(ent_path.filename().string());
+
+ // prepare dirent
+ tmp.d_fileno = BaseDirectory::next_fileno();
+ tmp.d_namlen = leaf.size();
+ strncpy(tmp.d_name, leaf.data(), tmp.d_namlen + 1);
+ tmp.d_type = (ent_is_file ? 0100000 : 0040000) >> 12;
+ tmp.d_reclen = Common::AlignUp(dirent_meta_size + tmp.d_namlen + 1, 4);
+
+ // next element may break 512 byte alignment
+ if (tmp.d_reclen + dirent_offset > next_ceiling) {
+ // align previous dirent's size to the current ceiling
+ *reinterpret_cast(static_cast(dirent_cache_bin.data()) +
+ last_reclen_offset) += next_ceiling - dirent_offset;
+ // set writing pointer to the aligned start position (current ceiling)
+ dirent_offset = next_ceiling;
+ // move the ceiling up and zero-out the buffer
+ next_ceiling += 512;
+ dirent_cache_bin.resize(next_ceiling);
+ std::fill(dirent_cache_bin.begin() + dirent_offset,
+ dirent_cache_bin.begin() + next_ceiling, 0);
+ }
+
+ // current dirent's reclen position
+ last_reclen_offset = dirent_offset + 4;
+ memcpy(dirent_cache_bin.data() + dirent_offset, &tmp, tmp.d_reclen);
+ dirent_offset += tmp.d_reclen;
+ });
+
+ // last reclen, as before
+ *reinterpret_cast(static_cast(dirent_cache_bin.data()) + last_reclen_offset) +=
+ next_ceiling - dirent_offset;
+
+ // i have no idea if this is the case, but lseek returns size aligned to 512
+ directory_size = next_ceiling;
+}
+
} // namespace Core::Directories
\ No newline at end of file
diff --git a/src/core/file_sys/directories/normal_directory.h b/src/core/file_sys/directories/normal_directory.h
index 70e52f581..4fc84cd2a 100644
--- a/src/core/file_sys/directories/normal_directory.h
+++ b/src/core/file_sys/directories/normal_directory.h
@@ -19,27 +19,23 @@ public:
~NormalDirectory() override = default;
virtual s64 read(void* buf, u64 nbytes) override;
- virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) override;
- virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt,
- s64 offset) override;
- virtual s64 lseek(s64 offset, s32 whence) override;
virtual s32 fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
virtual s64 getdents(void* buf, u64 nbytes, s64* basep) override;
private:
- static constexpr s32 MAX_LENGTH = 255;
- static constexpr s64 DIRECTORY_ALIGNMENT = 0x200;
+#pragma pack(push, 1)
struct NormalDirectoryDirent {
u32 d_fileno;
u16 d_reclen;
u8 d_type;
u8 d_namlen;
- char d_name[MAX_LENGTH + 1];
+ char d_name[256];
};
+#pragma pack(pop)
- u64 directory_size = 0;
- s64 file_offset = 0;
- std::vector data_buffer;
- std::vector dirents;
+ std::string_view guest_directory{};
+ s64 previous_file_offset = -1;
+
+ void RebuildDirents(void);
};
} // namespace Core::Directories
diff --git a/src/core/file_sys/directories/pfs_directory.cpp b/src/core/file_sys/directories/pfs_directory.cpp
index fbd97c019..38ceaf345 100644
--- a/src/core/file_sys/directories/pfs_directory.cpp
+++ b/src/core/file_sys/directories/pfs_directory.cpp
@@ -15,77 +15,49 @@ std::shared_ptr PfsDirectory::Create(std::string_view guest_direc
}
PfsDirectory::PfsDirectory(std::string_view guest_directory) {
+ constexpr u32 dirent_meta_size =
+ sizeof(PfsDirectoryDirent::d_fileno) + sizeof(PfsDirectoryDirent::d_type) +
+ sizeof(PfsDirectoryDirent::d_namlen) + sizeof(PfsDirectoryDirent::d_reclen);
+
+ dirent_cache_bin.reserve(512);
+
auto* mnt = Common::Singleton::Instance();
- static s32 fileno = 0;
- mnt->IterateDirectory(guest_directory, [this](const auto& ent_path, const auto ent_is_file) {
- auto& dirent = dirents.emplace_back();
- dirent.d_fileno = ++fileno;
- dirent.d_type = (ent_is_file ? 8 : 4);
- strncpy(dirent.d_name, ent_path.filename().string().data(), MAX_LENGTH + 1);
- dirent.d_namlen = ent_path.filename().string().size();
+ mnt->IterateDirectory(
+ guest_directory, [this](const std::filesystem::path& ent_path, const bool ent_is_file) {
+ PfsDirectoryDirent tmp{};
+ std::string leaf(ent_path.filename().string());
- // Calculate the appropriate length for this dirent.
- // Account for the null terminator in d_name too.
- dirent.d_reclen = Common::AlignUp(sizeof(dirent.d_fileno) + sizeof(dirent.d_type) +
- sizeof(dirent.d_namlen) + sizeof(dirent.d_reclen) +
- (dirent.d_namlen + 1),
- 8);
+ tmp.d_fileno = BaseDirectory::next_fileno();
+ tmp.d_namlen = leaf.size();
+ strncpy(tmp.d_name, leaf.data(), tmp.d_namlen + 1);
+ tmp.d_type = ent_is_file ? 2 : 4;
+ tmp.d_reclen = Common::AlignUp(dirent_meta_size + tmp.d_namlen + 1, 8);
+ auto dirent_ptr = reinterpret_cast(&tmp);
- // To handle some obscure dirents_index behavior,
- // keep track of the "actual" length of this directory.
- directory_content_size += dirent.d_reclen;
- });
+ dirent_cache_bin.insert(dirent_cache_bin.end(), dirent_ptr, dirent_ptr + tmp.d_reclen);
+ });
- directory_size = Common::AlignUp(directory_content_size, DIRECTORY_ALIGNMENT);
+ directory_size = Common::AlignUp(dirent_cache_bin.size(), 0x10000);
}
s64 PfsDirectory::read(void* buf, u64 nbytes) {
- if (dirents_index >= dirents.size()) {
- if (dirents_index < directory_content_size) {
- // We need to find the appropriate dirents_index to start from.
- s64 data_to_skip = dirents_index;
- u64 corrected_index = 0;
- while (data_to_skip > 0) {
- const auto dirent = dirents[corrected_index++];
- data_to_skip -= dirent.d_reclen;
- }
- dirents_index = corrected_index;
- } else {
- // Nothing left to read.
- return ORBIS_OK;
- }
+ s64 bytes_available = this->dirent_cache_bin.size() - file_offset;
+ if (bytes_available <= 0)
+ return 0;
+
+ bytes_available = std::min(bytes_available, static_cast(nbytes));
+ memcpy(buf, this->dirent_cache_bin.data() + file_offset, bytes_available);
+
+ s64 to_fill =
+ (std::min(directory_size, static_cast(nbytes))) - bytes_available - file_offset;
+ if (to_fill < 0) {
+ LOG_ERROR(Kernel_Fs, "Dirent may have leaked {} bytes", -to_fill);
+ return -to_fill + bytes_available;
}
-
- s64 bytes_remaining = nbytes > directory_size ? directory_size : nbytes;
- // read on PfsDirectories will always return the maximum possible value.
- const u64 bytes_written = bytes_remaining;
- memset(buf, 0, bytes_remaining);
-
- char* current_dirent = static_cast(buf);
- PfsDirectoryDirent dirent = dirents[dirents_index];
- while (bytes_remaining > dirent.d_reclen) {
- PfsDirectoryDirent* dirent_to_write = reinterpret_cast(current_dirent);
- dirent_to_write->d_fileno = dirent.d_fileno;
-
- // Using size d_namlen + 1 to account for null terminator.
- strncpy(dirent_to_write->d_name, dirent.d_name, dirent.d_namlen + 1);
- dirent_to_write->d_namlen = dirent.d_namlen;
- dirent_to_write->d_reclen = dirent.d_reclen;
- dirent_to_write->d_type = dirent.d_type;
-
- current_dirent += dirent.d_reclen;
- bytes_remaining -= dirent.d_reclen;
-
- if (dirents_index == dirents.size() - 1) {
- // Currently at the last dirent, so break out of the loop.
- dirents_index++;
- break;
- }
- dirent = dirents[++dirents_index];
- }
-
- return bytes_written;
+ memset(static_cast(buf) + bytes_available, 0, to_fill);
+ file_offset += to_fill + bytes_available;
+ return to_fill + bytes_available;
}
s64 PfsDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) {
@@ -101,62 +73,13 @@ s64 PfsDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovc
}
s64 PfsDirectory::preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) {
- const u64 old_dirent_index = dirents_index;
- dirents_index = 0;
- s64 data_to_skip = offset;
- // If offset is part-way through one dirent, that dirent is skipped.
- while (data_to_skip > 0) {
- const auto dirent = dirents[dirents_index++];
- data_to_skip -= dirent.d_reclen;
- if (dirents_index == dirents.size()) {
- // We've reached the end of the dirents, nothing more can be skipped.
- break;
- }
- }
-
+ const u64 old_file_pointer = file_offset;
+ file_offset = offset;
const s64 bytes_read = readv(iov, iovcnt);
- dirents_index = old_dirent_index;
+ file_offset = old_file_pointer;
return bytes_read;
}
-s64 PfsDirectory::lseek(s64 offset, s32 whence) {
- switch (whence) {
- // Seek start
- case 0: {
- dirents_index = 0;
- }
- case 1: {
- // There aren't any dirents left to pass through.
- if (dirents_index >= dirents.size()) {
- dirents_index = dirents_index + offset;
- break;
- }
- s64 data_to_skip = offset;
- while (data_to_skip > 0) {
- const auto dirent = dirents[dirents_index++];
- data_to_skip -= dirent.d_reclen;
- if (dirents_index == dirents.size()) {
- // We've passed through all file dirents.
- // Set dirents_index to directory_size + remaining_offset instead.
- dirents_index = directory_content_size + data_to_skip;
- break;
- }
- }
- break;
- }
- case 2: {
- // Seems like real hardware gives up on tracking dirents_index if you go this route.
- dirents_index = directory_size + offset;
- break;
- }
- default: {
- UNREACHABLE_MSG("lseek with unknown whence {}", whence);
- }
- }
-
- return dirents_index;
-}
-
s32 PfsDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) {
stat->st_mode = 0000777u | 0040000u;
stat->st_size = directory_size;
@@ -166,55 +89,58 @@ s32 PfsDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) {
}
s64 PfsDirectory::getdents(void* buf, u64 nbytes, s64* basep) {
- // basep is set at the start of the function.
- if (basep != nullptr) {
- *basep = dirents_index;
- }
+ if (basep)
+ *basep = file_offset;
- if (dirents_index >= dirents.size()) {
- if (dirents_index < directory_content_size) {
- // We need to find the appropriate dirents_index to start from.
- s64 data_to_skip = dirents_index;
- u64 corrected_index = 0;
- while (data_to_skip > 0) {
- const auto dirent = dirents[corrected_index++];
- data_to_skip -= dirent.d_reclen;
- }
- dirents_index = corrected_index;
- } else {
- // Nothing left to read.
- return ORBIS_OK;
- }
- }
-
- s64 bytes_remaining = nbytes > directory_size ? directory_size : nbytes;
- memset(buf, 0, bytes_remaining);
+ // same as others, we just don't need a variable
+ if (file_offset >= directory_size)
+ return 0;
u64 bytes_written = 0;
- char* current_dirent = static_cast(buf);
- // getdents has to convert pfs dirents to normal dirents
- PfsDirectoryDirent dirent = dirents[dirents_index];
- while (bytes_remaining > dirent.d_reclen) {
- NormalDirectoryDirent* dirent_to_write =
- reinterpret_cast(current_dirent);
- dirent_to_write->d_fileno = dirent.d_fileno;
- strncpy(dirent_to_write->d_name, dirent.d_name, dirent.d_namlen + 1);
- dirent_to_write->d_namlen = dirent.d_namlen;
- dirent_to_write->d_reclen = dirent.d_reclen;
- dirent_to_write->d_type = dirent.d_type;
+ u64 starting_offset = 0;
+ u64 buffer_position = 0;
+ while (buffer_position < this->dirent_cache_bin.size()) {
+ const PfsDirectoryDirent* pfs_dirent =
+ reinterpret_cast(this->dirent_cache_bin.data() + buffer_position);
- current_dirent += dirent.d_reclen;
- bytes_remaining -= dirent.d_reclen;
- bytes_written += dirent.d_reclen;
-
- if (dirents_index == dirents.size() - 1) {
- // Currently at the last dirent, so set dirents_index appropriately and break.
- dirents_index = directory_size;
+ // bad, incomplete or OOB entry
+ if (pfs_dirent->d_namlen == 0)
break;
+
+ if (starting_offset < file_offset) {
+ // reading starts from the nearest full dirent
+ starting_offset += pfs_dirent->d_reclen;
+ buffer_position = bytes_written + starting_offset;
+ continue;
}
- dirent = dirents[++dirents_index];
+
+ if ((bytes_written + pfs_dirent->d_reclen) > nbytes)
+ // dirents are aligned to the last full one
+ break;
+
+ // if this dirent breaks alignment, skip
+ // dirents are count-aligned here, excess data is simply not written
+ // if (Common::AlignUp(buffer_position, count) !=
+ // Common::AlignUp(buffer_position + pfs_dirent->d_reclen, count))
+ // break;
+
+ // reclen for both is the same despite difference in var sizes, extra 0s are padded after
+ // the name
+ NormalDirectoryDirent normal_dirent{};
+ normal_dirent.d_fileno = pfs_dirent->d_fileno;
+ normal_dirent.d_reclen = pfs_dirent->d_reclen;
+ normal_dirent.d_type = (pfs_dirent->d_type == 2) ? 8 : 4;
+ normal_dirent.d_namlen = pfs_dirent->d_namlen;
+ memcpy(normal_dirent.d_name, pfs_dirent->d_name, pfs_dirent->d_namlen);
+
+ memcpy(static_cast(buf) + bytes_written, &normal_dirent, normal_dirent.d_reclen);
+ bytes_written += normal_dirent.d_reclen;
+ buffer_position = bytes_written + starting_offset;
}
+ file_offset = (buffer_position >= this->dirent_cache_bin.size())
+ ? directory_size
+ : (file_offset + bytes_written);
return bytes_written;
}
} // namespace Core::Directories
\ No newline at end of file
diff --git a/src/core/file_sys/directories/pfs_directory.h b/src/core/file_sys/directories/pfs_directory.h
index 8f3e8d1f5..23b7e1eb0 100644
--- a/src/core/file_sys/directories/pfs_directory.h
+++ b/src/core/file_sys/directories/pfs_directory.h
@@ -22,32 +22,28 @@ public:
virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) override;
virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt,
s64 offset) override;
- virtual s64 lseek(s64 offset, s32 whence) override;
virtual s32 fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
virtual s64 getdents(void* buf, u64 nbytes, s64* basep) override;
private:
- static constexpr s32 MAX_LENGTH = 255;
- static constexpr s32 DIRECTORY_ALIGNMENT = 0x10000;
+#pragma pack(push, 1)
struct PfsDirectoryDirent {
u32 d_fileno;
u32 d_type;
u32 d_namlen;
u32 d_reclen;
- char d_name[MAX_LENGTH + 1];
+ char d_name[256];
};
+#pragma pack(pop)
+#pragma pack(push, 1)
struct NormalDirectoryDirent {
u32 d_fileno;
u16 d_reclen;
u8 d_type;
u8 d_namlen;
- char d_name[MAX_LENGTH + 1];
+ char d_name[256];
};
-
- u64 directory_size = 0;
- u64 directory_content_size = 0;
- s64 dirents_index = 0;
- std::vector dirents;
+#pragma pack(pop)
};
} // namespace Core::Directories
diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp
index 2a0fa43dd..cba95fe37 100644
--- a/src/core/file_sys/fs.cpp
+++ b/src/core/file_sys/fs.cpp
@@ -52,6 +52,9 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea
pos = corrected_path.find("//", pos + 1);
}
+ if (path.length() > 255)
+ return "";
+
const MntPair* mount = GetMount(corrected_path);
if (!mount) {
return "";
@@ -229,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;
}
diff --git a/src/core/ipc/ipc.cpp b/src/core/ipc/ipc.cpp
index 08cf3bbb2..ea7cd38b4 100644
--- a/src/core/ipc/ipc.cpp
+++ b/src/core/ipc/ipc.cpp
@@ -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 "ipc.h"
@@ -14,6 +14,7 @@
#include "common/types.h"
#include "core/debug_state.h"
#include "core/debugger.h"
+#include "core/emulator_state.h"
#include "core/libraries/audio/audioout.h"
#include "input/input_handler.h"
#include "sdl_window.h"
@@ -71,7 +72,7 @@ void IPC::Init() {
return;
}
- Config::setLoadAutoPatches(false);
+ EmulatorState::GetInstance()->SetAutoPatchesLoadEnabled(false);
input_thread = std::jthread([this] {
Common::SetCurrentThreadName("IPC Read thread");
diff --git a/src/core/libraries/ajm/ajm.cpp b/src/core/libraries/ajm/ajm.cpp
index 83620250b..2bec1bf0f 100644
--- a/src/core/libraries/ajm/ajm.cpp
+++ b/src/core/libraries/ajm/ajm.cpp
@@ -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);
}
}
@@ -144,9 +144,8 @@ int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* p_context_id) {
return ORBIS_OK;
}
-int PS4_SYSV_ABI sceAjmInstanceCodecType() {
- LOG_ERROR(Lib_Ajm, "(STUBBED) called");
- return ORBIS_OK;
+AjmCodecType PS4_SYSV_ABI sceAjmInstanceCodecType(u32 instance_id) {
+ return static_cast((instance_id >> 14) & 0x1F);
}
int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context_id, AjmCodecType codec_type,
diff --git a/src/core/libraries/ajm/ajm.h b/src/core/libraries/ajm/ajm.h
index 2c529cd4b..1bfd88351 100644
--- a/src/core/libraries/ajm/ajm.h
+++ b/src/core/libraries/ajm/ajm.h
@@ -82,8 +82,6 @@ enum class AjmStatisticsFlags : u64 {
DECLARE_ENUM_FLAG_OPERATORS(AjmStatisticsFlags)
union AjmStatisticsJobFlags {
- AjmStatisticsJobFlags(AjmJobFlags job_flags) : raw(job_flags.raw) {}
-
u64 raw;
struct {
u64 version : 3;
@@ -133,7 +131,7 @@ struct AjmSidebandGaplessDecode {
struct AjmSidebandResampleParameters {
float ratio;
- uint32_t flags;
+ u32 flags;
};
struct AjmDecAt9InitializeParameters {
@@ -217,7 +215,7 @@ int PS4_SYSV_ABI sceAjmDecMp3ParseFrame(const u8* stream, u32 stream_size, int p
AjmDecMp3ParseFrame* frame);
int PS4_SYSV_ABI sceAjmFinalize();
int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* out_context);
-int PS4_SYSV_ABI sceAjmInstanceCodecType();
+AjmCodecType PS4_SYSV_ABI sceAjmInstanceCodecType(u32 instance_id);
int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context, AjmCodecType codec_type, AjmInstanceFlags flags,
u32* instance);
int PS4_SYSV_ABI sceAjmInstanceDestroy(u32 context, u32 instance);
diff --git a/src/core/libraries/ajm/ajm_aac.cpp b/src/core/libraries/ajm/ajm_aac.cpp
new file mode 100644
index 000000000..061b77890
--- /dev/null
+++ b/src/core/libraries/ajm/ajm_aac.cpp
@@ -0,0 +1,218 @@
+// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "ajm.h"
+#include "ajm_aac.h"
+#include "ajm_result.h"
+
+#include
+// using this internal header to manually configure the decoder in RAW mode
+#include "externals/aacdec/fdk-aac/libAACdec/src/aacdecoder.h"
+
+#include // std::transform
+#include // std::back_inserter
+#include
+
+namespace Libraries::Ajm {
+
+std::span 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(pcm_data.size(), max_pcm));
+}
+
+template <>
+size_t AjmAacDecoder::WriteOutputSamples(SparseOutputBuffer& out, std::span pcm) {
+ if (pcm.empty()) {
+ return 0;
+ }
+
+ m_resample_buffer.clear();
+ constexpr float inv_scale = 1.0f / std::numeric_limits::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(1024 * 8),
+ m_skip_frames(True(flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2) {
+ m_resample_buffer.reserve(m_pcm_buffer.size());
+}
+
+AjmAacDecoder::~AjmAacDecoder() {
+ if (m_decoder) {
+ aacDecoder_Close(m_decoder);
+ }
+}
+
+TRANSPORT_TYPE TransportTypeFromConfigType(ConfigType config_type) {
+ switch (config_type) {
+ case ConfigType::ADTS:
+ return TT_MP4_ADTS;
+ case ConfigType::RAW:
+ return TT_MP4_RAW;
+ default:
+ UNREACHABLE();
+ }
+}
+
+static UINT g_freq[] = {
+ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000,
+};
+
+void AjmAacDecoder::Reset() {
+ if (m_decoder) {
+ aacDecoder_Close(m_decoder);
+ }
+
+ m_decoder = aacDecoder_Open(TransportTypeFromConfigType(m_init_params.config_type), 1);
+ if (m_init_params.config_type == ConfigType::RAW) {
+ // Manually configure the decoder
+ // Things may be incorrect due to limited documentation
+ CSAudioSpecificConfig asc{};
+ asc.m_aot = AOT_AAC_LC;
+ asc.m_samplingFrequency = g_freq[m_init_params.sampling_freq_type];
+ asc.m_samplingFrequencyIndex = m_init_params.sampling_freq_type;
+ asc.m_samplesPerFrame = 1024;
+ asc.m_epConfig = -1;
+ switch (m_channels) {
+ case 0:
+ asc.m_channelConfiguration = 2;
+ break;
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ asc.m_channelConfiguration = m_channels;
+ break;
+ case 7:
+ asc.m_channelConfiguration = 11;
+ break;
+ case 8:
+ asc.m_channelConfiguration = 12; // 7, 12 or 14 ?
+ break;
+ default:
+ UNREACHABLE();
+ }
+
+ UCHAR changed = 1;
+ CAacDecoder_Init(m_decoder, &asc, AC_CM_ALLOC_MEM, &changed);
+ }
+ m_skip_frames = True(m_flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2;
+}
+
+void AjmAacDecoder::Initialize(const void* buffer, u32 buffer_size) {
+ ASSERT(buffer_size == 8);
+ m_init_params = *reinterpret_cast(buffer);
+ Reset();
+}
+
+void AjmAacDecoder::GetInfo(void* out_info) const {
+ auto* codec_info = reinterpret_cast(out_info);
+ *codec_info = {
+ .heaac = True(m_flags & AjmAacCodecFlags::EnableSbrDecode),
+ };
+}
+
+AjmSidebandFormat AjmAacDecoder::GetFormat() const {
+ const auto* const info = aacDecoder_GetStreamInfo(m_decoder);
+ return {
+ .num_channels = static_cast(info->numChannels),
+ .channel_mask = GetChannelMask(info->numChannels),
+ .sampl_freq = static_cast(info->sampleRate),
+ .sample_encoding = m_format,
+ .bitrate = static_cast(info->bitRate),
+ };
+}
+
+u32 AjmAacDecoder::GetMinimumInputSize() const {
+ return 0;
+}
+
+u32 AjmAacDecoder::GetNextFrameSize(const AjmInstanceGapless& gapless) const {
+ const auto* const info = aacDecoder_GetStreamInfo(m_decoder);
+ if (info->aacSamplesPerFrame <= 0) {
+ return 0;
+ }
+ const auto skip_samples = std::min(gapless.current.skip_samples, info->frameSize);
+ const auto samples =
+ gapless.init.total_samples != 0
+ ? std::min(gapless.current.total_samples, info->frameSize - skip_samples)
+ : info->frameSize - skip_samples;
+ return samples * info->numChannels * GetPCMSize(m_format);
+}
+
+DecoderResult AjmAacDecoder::ProcessData(std::span& input, SparseOutputBuffer& output,
+ AjmInstanceGapless& gapless) {
+ DecoderResult result{};
+
+ // Discard the previous contents of the internal buffer and replace them with new ones
+ aacDecoder_SetParam(m_decoder, AAC_TPDEC_CLEAR_BUFFER, 1);
+ UCHAR* buffers[] = {input.data()};
+ const UINT sizes[] = {static_cast(input.size())};
+ UINT valid = sizes[0];
+ aacDecoder_Fill(m_decoder, buffers, sizes, &valid);
+ auto ret = aacDecoder_DecodeFrame(m_decoder, m_pcm_buffer.data(), m_pcm_buffer.size(), 0);
+
+ switch (ret) {
+ case AAC_DEC_OK:
+ break;
+ case AAC_DEC_NOT_ENOUGH_BITS:
+ result.result = ORBIS_AJM_RESULT_PARTIAL_INPUT;
+ return result;
+ default:
+ LOG_ERROR(Lib_Ajm, "aacDecoder_DecodeFrame failed ret = {:#x}", static_cast(ret));
+ result.result = ORBIS_AJM_RESULT_CODEC_ERROR | ORBIS_AJM_RESULT_FATAL;
+ result.internal_result = ret;
+ return result;
+ }
+
+ const auto* const info = aacDecoder_GetStreamInfo(m_decoder);
+ auto bytes_used = info->numTotalBytes;
+
+ result.frames_decoded += 1;
+ input = input.subspan(bytes_used);
+
+ if (m_skip_frames > 0) {
+ --m_skip_frames;
+ return result;
+ }
+
+ u32 skip_samples = 0;
+ if (gapless.current.skip_samples > 0) {
+ skip_samples = std::min(info->frameSize, gapless.current.skip_samples);
+ gapless.current.skip_samples -= skip_samples;
+ }
+
+ const auto max_samples =
+ 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 = output.Write(pcm);
+ break;
+ case AjmFormatEncoding::S32:
+ UNREACHABLE_MSG("NOT IMPLEMENTED");
+ break;
+ case AjmFormatEncoding::Float:
+ pcm_written = WriteOutputSamples(output, pcm);
+ break;
+ default:
+ UNREACHABLE();
+ }
+
+ result.samples_written = pcm_written / info->numChannels;
+ gapless.current.skipped_samples += info->frameSize - result.samples_written;
+ if (gapless.init.total_samples != 0) {
+ gapless.current.total_samples -= result.samples_written;
+ }
+
+ return result;
+}
+
+} // namespace Libraries::Ajm
\ No newline at end of file
diff --git a/src/core/libraries/ajm/ajm_aac.h b/src/core/libraries/ajm/ajm_aac.h
new file mode 100644
index 000000000..4ff55d843
--- /dev/null
+++ b/src/core/libraries/ajm/ajm_aac.h
@@ -0,0 +1,69 @@
+// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "common/enum.h"
+#include "common/types.h"
+#include "core/libraries/ajm/ajm_instance.h"
+
+#include
+#include
+
+struct AAC_DECODER_INSTANCE;
+
+namespace Libraries::Ajm {
+
+enum ConfigType : u32 {
+ ADTS = 1,
+ RAW = 2,
+};
+
+enum AjmAacCodecFlags : u32 {
+ EnableSbrDecode = 1 << 0,
+ EnableNondelayOutput = 1 << 1,
+ SurroundChannelInterleaveOrderExtlExtrLsRs = 1 << 2,
+ SurroundChannelInterleaveOrderLsRsExtlExtr = 1 << 3,
+};
+DECLARE_ENUM_FLAG_OPERATORS(AjmAacCodecFlags)
+
+struct AjmSidebandDecM4aacCodecInfo {
+ u32 heaac;
+ u32 reserved;
+};
+
+struct AjmAacDecoder final : AjmCodec {
+ explicit AjmAacDecoder(AjmFormatEncoding format, AjmAacCodecFlags flags, u32 channels);
+ ~AjmAacDecoder() override;
+
+ void Reset() override;
+ void Initialize(const void* buffer, u32 buffer_size) override;
+ void GetInfo(void* out_info) const override;
+ AjmSidebandFormat GetFormat() const override;
+ u32 GetMinimumInputSize() const override;
+ u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const override;
+ DecoderResult ProcessData(std::span& input, SparseOutputBuffer& output,
+ AjmInstanceGapless& gapless) override;
+
+private:
+ struct InitializeParameters {
+ ConfigType config_type;
+ u32 sampling_freq_type;
+ };
+
+ template
+ size_t WriteOutputSamples(SparseOutputBuffer& output, std::span pcm);
+ std::span GetOuputPcm(u32 skipped_pcm, u32 max_pcm) const;
+
+ const AjmFormatEncoding m_format;
+ const AjmAacCodecFlags m_flags;
+ const u32 m_channels;
+ std::vector m_pcm_buffer;
+ std::vector m_resample_buffer;
+
+ u32 m_skip_frames = 0;
+ InitializeParameters m_init_params = {};
+ AAC_DECODER_INSTANCE* m_decoder = nullptr;
+};
+
+} // namespace Libraries::Ajm
\ No newline at end of file
diff --git a/src/core/libraries/ajm/ajm_at9.cpp b/src/core/libraries/ajm/ajm_at9.cpp
index 014d1a4e5..4452d032d 100644
--- a/src/core/libraries/ajm/ajm_at9.cpp
+++ b/src/core/libraries/ajm/ajm_at9.cpp
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include "ajm_result.h"
#include "common/assert.h"
#include "core/libraries/ajm/ajm_at9.h"
#include "error_codes.h"
@@ -53,7 +54,7 @@ struct RIFFHeader {
};
static_assert(sizeof(RIFFHeader) == 12);
-AjmAt9Decoder::AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags)
+AjmAt9Decoder::AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags, u32)
: m_format(format), m_flags(flags), m_handle(Atrac9GetHandle()) {}
AjmAt9Decoder::~AjmAt9Decoder() {
@@ -85,8 +86,8 @@ void AjmAt9Decoder::GetInfo(void* out_info) const {
auto* info = reinterpret_cast(out_info);
info->super_frame_size = m_codec_info.superframeSize;
info->frames_in_super_frame = m_codec_info.framesInSuperframe;
+ info->next_frame_size = m_superframe_bytes_remain;
info->frame_samples = m_codec_info.frameSamples;
- info->next_frame_size = static_cast(m_handle)->Config.FrameBytes;
}
u8 g_at9_guid[] = {0xD2, 0x42, 0xE1, 0x47, 0xBA, 0x36, 0x8D, 0x4D,
@@ -133,18 +134,22 @@ void AjmAt9Decoder::ParseRIFFHeader(std::span& in_buf, AjmInstanceGapless& g
}
}
-std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf,
- SparseOutputBuffer& output,
- AjmInstanceGapless& gapless) {
- bool is_reset = false;
+u32 AjmAt9Decoder::GetMinimumInputSize() const {
+ return m_superframe_bytes_remain;
+}
+
+DecoderResult AjmAt9Decoder::ProcessData(std::span& in_buf, SparseOutputBuffer& output,
+ AjmInstanceGapless& gapless) {
+ DecoderResult result{};
if (True(m_flags & AjmAt9CodecFlags::ParseRiffHeader) &&
*reinterpret_cast(in_buf.data()) == 'FFIR') {
ParseRIFFHeader(in_buf, gapless);
- is_reset = true;
+ result.is_reset = true;
}
if (!m_is_initialized) {
- return {0, 0, is_reset};
+ result.result = ORBIS_AJM_RESULT_NOT_INITIALIZED;
+ return result;
}
int ret = 0;
@@ -166,7 +171,14 @@ std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf,
default:
UNREACHABLE();
}
- ASSERT_MSG(ret == At9Status::ERR_SUCCESS, "Atrac9Decode failed ret = {:#x}", ret);
+ if (ret != At9Status::ERR_SUCCESS) {
+ LOG_ERROR(Lib_Ajm, "Atrac9Decode failed ret = {:#x}", ret);
+ result.result = ORBIS_AJM_RESULT_CODEC_ERROR | ORBIS_AJM_RESULT_FATAL;
+ result.internal_result = ret;
+ return result;
+ }
+
+ result.frames_decoded += 1;
in_buf = in_buf.subspan(bytes_used);
m_superframe_bytes_remain -= bytes_used;
@@ -196,10 +208,10 @@ std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf,
UNREACHABLE();
}
- const auto samples_written = pcm_written / m_codec_info.channels;
- gapless.current.skipped_samples += m_codec_info.frameSamples - samples_written;
+ result.samples_written = pcm_written / m_codec_info.channels;
+ gapless.current.skipped_samples += m_codec_info.frameSamples - result.samples_written;
if (gapless.init.total_samples != 0) {
- gapless.current.total_samples -= samples_written;
+ gapless.current.total_samples -= result.samples_written;
}
m_num_frames += 1;
@@ -209,9 +221,23 @@ std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf,
}
m_superframe_bytes_remain = m_codec_info.superframeSize;
m_num_frames = 0;
+ } else if (gapless.IsEnd()) {
+ // Drain the remaining superframe
+ std::vector buf(m_codec_info.frameSamples * m_codec_info.channels, 0);
+ while ((m_num_frames % m_codec_info.framesInSuperframe) != 0) {
+ ret = Atrac9Decode(m_handle, in_buf.data(), buf.data(), &bytes_used,
+ True(m_flags & AjmAt9CodecFlags::NonInterleavedOutput));
+ in_buf = in_buf.subspan(bytes_used);
+ m_superframe_bytes_remain -= bytes_used;
+ result.frames_decoded += 1;
+ m_num_frames += 1;
+ }
+ in_buf = in_buf.subspan(m_superframe_bytes_remain);
+ m_superframe_bytes_remain = m_codec_info.superframeSize;
+ m_num_frames = 0;
}
- return {1, m_codec_info.frameSamples, is_reset};
+ return result;
}
AjmSidebandFormat AjmAt9Decoder::GetFormat() const {
@@ -232,7 +258,7 @@ u32 AjmAt9Decoder::GetNextFrameSize(const AjmInstanceGapless& gapless) const {
const auto samples =
gapless.init.total_samples != 0
? std::min(gapless.current.total_samples, m_codec_info.frameSamples - skip_samples)
- : m_codec_info.frameSamples;
+ : m_codec_info.frameSamples - skip_samples;
return samples * m_codec_info.channels * GetPCMSize(m_format);
}
diff --git a/src/core/libraries/ajm/ajm_at9.h b/src/core/libraries/ajm/ajm_at9.h
index 3262f1aa0..8eb6166e2 100644
--- a/src/core/libraries/ajm/ajm_at9.h
+++ b/src/core/libraries/ajm/ajm_at9.h
@@ -3,6 +3,7 @@
#pragma once
+#include "common/enum.h"
#include "common/types.h"
#include "core/libraries/ajm/ajm_instance.h"
@@ -13,8 +14,6 @@
namespace Libraries::Ajm {
-constexpr s32 ORBIS_AJM_DEC_AT9_MAX_CHANNELS = 8;
-
enum AjmAt9CodecFlags : u32 {
ParseRiffHeader = 1 << 0,
NonInterleavedOutput = 1 << 8,
@@ -29,16 +28,17 @@ struct AjmSidebandDecAt9CodecInfo {
};
struct AjmAt9Decoder final : AjmCodec {
- explicit AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags);
+ explicit AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags, u32 channels);
~AjmAt9Decoder() override;
void Reset() override;
void Initialize(const void* buffer, u32 buffer_size) override;
void GetInfo(void* out_info) const override;
AjmSidebandFormat GetFormat() const override;
+ u32 GetMinimumInputSize() const override;
u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const override;
- std::tuple ProcessData(std::span& input, SparseOutputBuffer& output,
- AjmInstanceGapless& gapless) override;
+ DecoderResult ProcessData(std::span& input, SparseOutputBuffer& output,
+ AjmInstanceGapless& gapless) override;
private:
template
diff --git a/src/core/libraries/ajm/ajm_batch.cpp b/src/core/libraries/ajm/ajm_batch.cpp
index 30e1deb71..3ab2ed4ab 100644
--- a/src/core/libraries/ajm/ajm_batch.cpp
+++ b/src/core/libraries/ajm/ajm_batch.cpp
@@ -165,7 +165,7 @@ AjmJob AjmStatisticsJobFromBatchBuffer(u32 instance_id, AjmBatchBuffer batch_buf
ASSERT(job_flags.has_value());
job.flags = job_flags.value();
- AjmStatisticsJobFlags flags(job.flags);
+ AjmStatisticsJobFlags flags{.raw = job.flags.raw};
if (input_control_buffer.has_value()) {
AjmBatchBuffer input_batch(input_control_buffer.value());
if (True(flags.statistics_flags & AjmStatisticsFlags::Engine)) {
@@ -280,9 +280,7 @@ AjmJob AjmJobFromBatchBuffer(u32 instance_id, AjmBatchBuffer batch_buffer) {
job.input.resample_parameters = input_batch.Consume();
}
if (True(control_flags & AjmJobControlFlags::Initialize)) {
- job.input.init_params = AjmDecAt9InitializeParameters{};
- std::memcpy(&job.input.init_params.value(), input_batch.GetCurrent(),
- input_batch.BytesRemaining());
+ job.input.init_params = input_batch.Consume();
}
}
diff --git a/src/core/libraries/ajm/ajm_batch.h b/src/core/libraries/ajm/ajm_batch.h
index 09daa630d..c18e9efbf 100644
--- a/src/core/libraries/ajm/ajm_batch.h
+++ b/src/core/libraries/ajm/ajm_batch.h
@@ -21,7 +21,7 @@ namespace Libraries::Ajm {
struct AjmJob {
struct Input {
- std::optional init_params;
+ std::optional init_params;
std::optional resample_parameters;
std::optional statistics_engine_parameters;
std::optional format;
@@ -52,6 +52,7 @@ struct AjmBatch {
u32 id{};
std::atomic_bool waiting{};
std::atomic_bool canceled{};
+ std::atomic_bool processed{};
std::binary_semaphore finished{0};
boost::container::small_vector jobs;
diff --git a/src/core/libraries/ajm/ajm_context.cpp b/src/core/libraries/ajm/ajm_context.cpp
index 0e2915f32..8ce8f3434 100644
--- a/src/core/libraries/ajm/ajm_context.cpp
+++ b/src/core/libraries/ajm/ajm_context.cpp
@@ -18,7 +18,8 @@
namespace Libraries::Ajm {
-static constexpr u32 ORBIS_AJM_WAIT_INFINITE = -1;
+constexpr u32 ORBIS_AJM_WAIT_INFINITE = -1;
+constexpr int INSTANCE_ID_MASK = 0x3FFF;
AjmContext::AjmContext() {
worker_thread = std::jthread([this](std::stop_token stop) { this->WorkerThread(stop); });
@@ -39,7 +40,12 @@ s32 AjmContext::BatchCancel(const u32 batch_id) {
batch = *p_batch;
}
- batch->canceled = true;
+ if (batch->processed) {
+ return ORBIS_OK;
+ }
+
+ bool expected = false;
+ batch->canceled.compare_exchange_strong(expected, true);
return ORBIS_OK;
}
@@ -58,7 +64,9 @@ void AjmContext::WorkerThread(std::stop_token stop) {
Common::SetCurrentThreadName("shadPS4:AjmWorker");
while (!stop.stop_requested()) {
auto batch = batch_queue.PopWait(stop);
- if (batch != nullptr) {
+ if (batch != nullptr && !batch->canceled) {
+ bool expected = false;
+ batch->processed.compare_exchange_strong(expected, true);
ProcessBatch(batch->id, batch->jobs);
batch->finished.release();
}
@@ -77,7 +85,7 @@ void AjmContext::ProcessBatch(u32 id, std::span jobs) {
std::shared_ptr instance;
{
std::shared_lock lock(instances_mutex);
- auto* p_instance = instances.Get(job.instance_id);
+ auto* p_instance = instances.Get(job.instance_id & INSTANCE_ID_MASK);
ASSERT_MSG(p_instance != nullptr, "Attempting to execute job on null instance");
instance = *p_instance;
}
@@ -169,15 +177,15 @@ s32 AjmContext::InstanceCreate(AjmCodecType codec_type, AjmInstanceFlags flags,
if (!opt_index.has_value()) {
return ORBIS_AJM_ERROR_OUT_OF_RESOURCES;
}
- *out_instance = opt_index.value();
+ *out_instance = opt_index.value() | (static_cast(codec_type) << 14);
LOG_INFO(Lib_Ajm, "instance = {}", *out_instance);
return ORBIS_OK;
}
-s32 AjmContext::InstanceDestroy(u32 instance) {
+s32 AjmContext::InstanceDestroy(u32 instance_id) {
std::unique_lock lock(instances_mutex);
- if (!instances.Destroy(instance)) {
+ if (!instances.Destroy(instance_id & INSTANCE_ID_MASK)) {
return ORBIS_AJM_ERROR_INVALID_INSTANCE;
}
return ORBIS_OK;
diff --git a/src/core/libraries/ajm/ajm_instance.cpp b/src/core/libraries/ajm/ajm_instance.cpp
index c4ea395b9..d25517c81 100644
--- a/src/core/libraries/ajm/ajm_instance.cpp
+++ b/src/core/libraries/ajm/ajm_instance.cpp
@@ -1,27 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
-#include "core/libraries/ajm/ajm_at9.h"
-#include "core/libraries/ajm/ajm_instance.h"
-#include "core/libraries/ajm/ajm_mp3.h"
+#include "ajm_aac.h"
+#include "ajm_at9.h"
+#include "ajm_instance.h"
+#include "ajm_mp3.h"
+#include "ajm_result.h"
#include
namespace Libraries::Ajm {
-constexpr int ORBIS_AJM_RESULT_NOT_INITIALIZED = 0x00000001;
-constexpr int ORBIS_AJM_RESULT_INVALID_DATA = 0x00000002;
-constexpr int ORBIS_AJM_RESULT_INVALID_PARAMETER = 0x00000004;
-constexpr int ORBIS_AJM_RESULT_PARTIAL_INPUT = 0x00000008;
-constexpr int ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010;
-constexpr int ORBIS_AJM_RESULT_STREAM_CHANGE = 0x00000020;
-constexpr int ORBIS_AJM_RESULT_TOO_MANY_CHANNELS = 0x00000040;
-constexpr int ORBIS_AJM_RESULT_UNSUPPORTED_FLAG = 0x00000080;
-constexpr int ORBIS_AJM_RESULT_SIDEBAND_TRUNCATED = 0x00000100;
-constexpr int ORBIS_AJM_RESULT_PRIORITY_PASSED = 0x00000200;
-constexpr int ORBIS_AJM_RESULT_CODEC_ERROR = 0x40000000;
-constexpr int ORBIS_AJM_RESULT_FATAL = 0x80000000;
-
u8 GetPCMSize(AjmFormatEncoding format) {
switch (format) {
case AjmFormatEncoding::S16:
@@ -38,13 +27,18 @@ u8 GetPCMSize(AjmFormatEncoding format) {
AjmInstance::AjmInstance(AjmCodecType codec_type, AjmInstanceFlags flags) : m_flags(flags) {
switch (codec_type) {
case AjmCodecType::At9Dec: {
- m_codec = std::make_unique(AjmFormatEncoding(flags.format),
- AjmAt9CodecFlags(flags.codec));
+ m_codec = std::make_unique(
+ AjmFormatEncoding(flags.format), AjmAt9CodecFlags(flags.codec), u32(flags.channels));
break;
}
case AjmCodecType::Mp3Dec: {
- m_codec = std::make_unique(AjmFormatEncoding(flags.format),
- AjmMp3CodecFlags(flags.codec));
+ m_codec = std::make_unique(
+ AjmFormatEncoding(flags.format), AjmMp3CodecFlags(flags.codec), u32(flags.channels));
+ break;
+ }
+ case AjmCodecType::M4aacDec: {
+ m_codec = std::make_unique(
+ AjmFormatEncoding(flags.format), AjmAacCodecFlags(flags.codec), u32(flags.channels));
break;
}
default:
@@ -60,6 +54,7 @@ void AjmInstance::Reset() {
void AjmInstance::ExecuteJob(AjmJob& job) {
const auto control_flags = job.flags.control_flags;
+ job.output.p_result->result = 0;
if (True(control_flags & AjmJobControlFlags::Reset)) {
LOG_TRACE(Lib_Ajm, "Resetting instance {}", job.instance_id);
Reset();
@@ -91,8 +86,7 @@ void AjmInstance::ExecuteJob(AjmJob& job) {
m_gapless.current.total_samples -= sample_difference;
} else {
LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_INVALID_PARAMETER");
- job.output.p_result->result = ORBIS_AJM_RESULT_INVALID_PARAMETER;
- return;
+ job.output.p_result->result |= ORBIS_AJM_RESULT_INVALID_PARAMETER;
}
}
@@ -106,61 +100,59 @@ void AjmInstance::ExecuteJob(AjmJob& job) {
m_gapless.current.skip_samples -= sample_difference;
} else {
LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_INVALID_PARAMETER");
- job.output.p_result->result = ORBIS_AJM_RESULT_INVALID_PARAMETER;
- return;
+ job.output.p_result->result |= ORBIS_AJM_RESULT_INVALID_PARAMETER;
}
}
}
- if (!job.input.buffer.empty() && !job.output.buffers.empty()) {
- std::span in_buf(job.input.buffer);
- SparseOutputBuffer out_buf(job.output.buffers);
+ std::span in_buf(job.input.buffer);
+ SparseOutputBuffer out_buf(job.output.buffers);
+ auto in_size = in_buf.size();
+ auto out_size = out_buf.Size();
+ u32 frames_decoded = 0;
- u32 frames_decoded = 0;
- auto in_size = in_buf.size();
- auto out_size = out_buf.Size();
- while (!in_buf.empty() && !out_buf.IsEmpty() && !m_gapless.IsEnd()) {
- if (!HasEnoughSpace(out_buf)) {
- if (job.output.p_mframe == nullptr || frames_decoded == 0) {
- LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM ({} < {})",
- out_buf.Size(), m_codec->GetNextFrameSize(m_gapless));
- job.output.p_result->result = ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM;
- break;
- }
- }
-
- const auto [nframes, nsamples, reset] =
- m_codec->ProcessData(in_buf, out_buf, m_gapless);
- if (reset) {
+ if (!job.input.buffer.empty()) {
+ for (;;) {
+ if (m_flags.gapless_loop && m_gapless.IsEnd()) {
+ m_gapless.Reset();
m_total_samples = 0;
}
- if (!nframes) {
- LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_NOT_INITIALIZED");
- job.output.p_result->result = ORBIS_AJM_RESULT_NOT_INITIALIZED;
+ if (!HasEnoughSpace(out_buf)) {
+ LOG_TRACE(Lib_Ajm, "ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM ({} < {})", out_buf.Size(),
+ m_codec->GetNextFrameSize(m_gapless));
+ job.output.p_result->result |= ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM;
+ }
+ if (in_buf.size() < m_codec->GetMinimumInputSize()) {
+ job.output.p_result->result |= ORBIS_AJM_RESULT_PARTIAL_INPUT;
+ }
+ if (job.output.p_result->result != 0) {
+ break;
+ }
+ const auto result = m_codec->ProcessData(in_buf, out_buf, m_gapless);
+ if (result.is_reset) {
+ m_total_samples = 0;
+ } else {
+ m_total_samples += result.samples_written;
+ }
+ frames_decoded += result.frames_decoded;
+ if (result.result != 0) {
+ job.output.p_result->result |= result.result;
+ job.output.p_result->internal_result = result.internal_result;
break;
}
- frames_decoded += nframes;
- m_total_samples += nsamples;
-
if (False(job.flags.run_flags & AjmJobRunFlags::MultipleFrames)) {
break;
}
}
+ }
- const auto total_decoded_samples = m_total_samples;
- if (m_flags.gapless_loop && m_gapless.IsEnd()) {
- in_buf = in_buf.subspan(in_buf.size());
- m_gapless.Reset();
- m_codec->Reset();
- }
- if (job.output.p_mframe) {
- job.output.p_mframe->num_frames = frames_decoded;
- }
- if (job.output.p_stream) {
- job.output.p_stream->input_consumed = in_size - in_buf.size();
- job.output.p_stream->output_written = out_size - out_buf.Size();
- job.output.p_stream->total_decoded_samples = total_decoded_samples;
- }
+ if (job.output.p_mframe) {
+ job.output.p_mframe->num_frames = frames_decoded;
+ }
+ if (job.output.p_stream) {
+ job.output.p_stream->input_consumed = in_size - in_buf.size();
+ job.output.p_stream->output_written = out_size - out_buf.Size();
+ job.output.p_stream->total_decoded_samples = m_total_samples;
}
if (job.output.p_format != nullptr) {
@@ -175,6 +167,9 @@ void AjmInstance::ExecuteJob(AjmJob& job) {
}
bool AjmInstance::HasEnoughSpace(const SparseOutputBuffer& output) const {
+ if (m_gapless.IsEnd()) {
+ return true;
+ }
return output.Size() >= m_codec->GetNextFrameSize(m_gapless);
}
diff --git a/src/core/libraries/ajm/ajm_instance.h b/src/core/libraries/ajm/ajm_instance.h
index ad0a82f29..db53add4d 100644
--- a/src/core/libraries/ajm/ajm_instance.h
+++ b/src/core/libraries/ajm/ajm_instance.h
@@ -73,6 +73,14 @@ struct AjmInstanceGapless {
}
};
+struct DecoderResult {
+ s32 result = 0;
+ s32 internal_result = 0;
+ u32 frames_decoded = 0;
+ u32 samples_written = 0;
+ bool is_reset = false;
+};
+
class AjmCodec {
public:
virtual ~AjmCodec() = default;
@@ -81,9 +89,10 @@ public:
virtual void Reset() = 0;
virtual void GetInfo(void* out_info) const = 0;
virtual AjmSidebandFormat GetFormat() const = 0;
+ virtual u32 GetMinimumInputSize() const = 0;
virtual u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const = 0;
- virtual std::tuple ProcessData(std::span& input, SparseOutputBuffer& output,
- AjmInstanceGapless& gapless) = 0;
+ virtual DecoderResult ProcessData(std::span& input, SparseOutputBuffer& output,
+ AjmInstanceGapless& gapless) = 0;
};
class AjmInstance {
@@ -94,7 +103,6 @@ public:
private:
bool HasEnoughSpace(const SparseOutputBuffer& output) const;
- std::optional GetNumRemainingSamples() const;
void Reset();
AjmInstanceFlags m_flags{};
diff --git a/src/core/libraries/ajm/ajm_instance_statistics.cpp b/src/core/libraries/ajm/ajm_instance_statistics.cpp
index c0c1af8bb..2e4a65914 100644
--- a/src/core/libraries/ajm/ajm_instance_statistics.cpp
+++ b/src/core/libraries/ajm/ajm_instance_statistics.cpp
@@ -8,7 +8,7 @@ namespace Libraries::Ajm {
void AjmInstanceStatistics::ExecuteJob(AjmJob& job) {
if (job.output.p_engine) {
- job.output.p_engine->usage_batch = 0.01;
+ job.output.p_engine->usage_batch = 0.05;
const auto ic = job.input.statistics_engine_parameters->interval_count;
for (u32 idx = 0; idx < ic; ++idx) {
job.output.p_engine->usage_interval[idx] = 0.01;
@@ -25,10 +25,12 @@ void AjmInstanceStatistics::ExecuteJob(AjmJob& job) {
job.output.p_memory->batch_size = 0x4200;
job.output.p_memory->input_size = 0x2000;
job.output.p_memory->output_size = 0x2000;
- job.output.p_memory->small_size = 0x200;
+ job.output.p_memory->small_size = 0x400;
}
}
+void AjmInstanceStatistics::Reset() {}
+
AjmInstanceStatistics& AjmInstanceStatistics::Getinstance() {
static AjmInstanceStatistics instance;
return instance;
diff --git a/src/core/libraries/ajm/ajm_instance_statistics.h b/src/core/libraries/ajm/ajm_instance_statistics.h
index ea70c9d56..0ec79aeac 100644
--- a/src/core/libraries/ajm/ajm_instance_statistics.h
+++ b/src/core/libraries/ajm/ajm_instance_statistics.h
@@ -10,6 +10,7 @@ namespace Libraries::Ajm {
class AjmInstanceStatistics {
public:
void ExecuteJob(AjmJob& job);
+ void Reset();
static AjmInstanceStatistics& Getinstance();
};
diff --git a/src/core/libraries/ajm/ajm_mp3.cpp b/src/core/libraries/ajm/ajm_mp3.cpp
index f17f53d51..d1c9374cc 100644
--- a/src/core/libraries/ajm/ajm_mp3.cpp
+++ b/src/core/libraries/ajm/ajm_mp3.cpp
@@ -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" {
@@ -105,7 +107,7 @@ AVFrame* AjmMp3Decoder::ConvertAudioFrame(AVFrame* frame) {
return new_frame;
}
-AjmMp3Decoder::AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags)
+AjmMp3Decoder::AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags, u32)
: m_format(format), m_flags(flags), m_codec(avcodec_find_decoder(AV_CODEC_ID_MP3)),
m_codec_context(avcodec_alloc_context3(m_codec)), m_parser(av_parser_init(m_codec->id)) {
int ret = avcodec_open2(m_codec_context, m_codec, nullptr);
@@ -138,16 +140,31 @@ void AjmMp3Decoder::GetInfo(void* out_info) const {
}
}
-std::tuple AjmMp3Decoder::ProcessData(std::span& in_buf,
- SparseOutputBuffer& output,
- AjmInstanceGapless& gapless) {
+u32 AjmMp3Decoder::GetMinimumInputSize() const {
+ // 4 bytes is for mp3 header that contains frame_size
+ return 4;
+}
+
+DecoderResult AjmMp3Decoder::ProcessData(std::span& in_buf, SparseOutputBuffer& output,
+ AjmInstanceGapless& gapless) {
+ 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(in_buf.data()));
- AjmDecMp3ParseFrame info{};
- ParseMp3Header(in_buf.data(), in_buf.size(), false, &info);
- m_frame_samples = info.samples_per_channel;
+ m_header = std::byteswap(*reinterpret_cast(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(info.encoder_delay),
+ .skipped_samples = 0,
+ };
+ 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(),
@@ -155,9 +172,6 @@ std::tuple AjmMp3Decoder::ProcessData(std::span