diff --git a/.github/ISSUE_TEMPLATE/rfc.yaml b/.github/ISSUE_TEMPLATE/rfc.yaml new file mode 100644 index 000000000..7ad453101 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/rfc.yaml @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later +# Docs - https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema +name: Request For Comments (RFC) +description: Ask for feedback on major architectural changes or design choices +title: "[RFC]: " +labels: ["RFC"] + +body: + - type: markdown + attributes: + value: | + ## Important: Read First + + RFCs are for major architectural changes, design direction, or changes that benefit from structured discussion before merge. + + Please make an effort to search for an existing RFC or issue before opening a new one. + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have searched for a similar RFC or issue in this repository and did not find one. + required: true + + - type: textarea + id: motivation + attributes: + label: Motivation + description: | + Explain the problem this RFC is trying to solve. + + Describe why the current design is insufficient and why this change is worth discussing now. + validations: + required: true + + - type: textarea + id: proposed_change + attributes: + label: Proposed Change + description: | + Describe the proposed change in enough detail for maintainers and contributors to evaluate it. + + Include the high-level design, affected areas, and any important constraints. + validations: + required: true + + - type: textarea + id: feedback_period + attributes: + label: Feedback Period + description: | + State the intended review window for this RFC. + + Example: one week, two weeks, or until specific maintainers have reviewed it. + placeholder: "Example: 1 week" + validations: + required: false + + - type: textarea + id: cc_list + attributes: + label: CC List + description: | + List any maintainers or contributors you want to explicitly notify for feedback. + validations: + required: false + + - type: textarea + id: additional_context + attributes: + label: Any Other Things + description: | + Add any other relevant context, tradeoffs, diagrams, migration notes, or links to related work. + validations: + required: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ffe7c22fb..96f5e33d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,8 @@ on: - "documents/**" - "**/*.md" + workflow_dispatch: + concurrency: group: ci-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'push' }} @@ -26,14 +28,14 @@ jobs: runs-on: ubuntu-24.04 continue-on-error: true steps: - - uses: actions/checkout@v5 - - uses: fsfe/reuse-action@v5 + - uses: actions/checkout@v6 + - uses: fsfe/reuse-action@v6 clang-format: runs-on: ubuntu-24.04 continue-on-error: true steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install @@ -46,7 +48,7 @@ jobs: env: COMMIT_RANGE: ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} run: ./.ci/clang-format.sh - + get-info: runs-on: ubuntu-24.04 outputs: @@ -54,7 +56,7 @@ jobs: shorthash: ${{ steps.vars.outputs.shorthash }} fullhash: ${{ steps.vars.outputs.fullhash }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Get date and git hash id: vars run: | @@ -65,27 +67,102 @@ jobs: echo "shorthash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "fullhash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + test: + name: Run C++ Tests on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + include: + - os: windows-latest + compiler_cxx: clang-cl + compiler_c: clang-cl + - os: ubuntu-latest + compiler_cxx: clang++ + compiler_c: clang + - os: macos-latest + compiler_cxx: clang++ + compiler_c: clang + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup CMake + uses: lukka/get-cmake@latest + + - name: Setup Visual Studio shell (Windows only) + if: runner.os == 'Windows' + uses: egor-tensin/vs-shell@v2 + with: + arch: x64 + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y ninja-build libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev libxrandr-dev libxfixes-dev libudev-dev uuid-dev uuid-dev + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew install ninja + + - name: Configure CMake + run: | + cmake -B build -G Ninja \ + -DCMAKE_CXX_COMPILER="${{ matrix.compiler_cxx }}" \ + -DCMAKE_C_COMPILER="${{ matrix.compiler_c }}" \ + -DCMAKE_BUILD_TYPE=Debug \ + -DENABLE_TESTS=ON \ + ${{ runner.os == 'macOS' && '-DCMAKE_OSX_ARCHITECTURES=x86_64' || '' }} + shell: bash + + - name: Create shadPS4 user data directory (Linux) + if: runner.os == 'Linux' + run: mkdir -p ~/.local/share/shadPS4 + + - name: Create shadPS4 user data directory (macOS) + if: runner.os == 'macOS' + run: mkdir -p ~/Library/Application\ Support/shadPS4 + + - name: Create shadPS4 user data directory (Windows) + if: runner.os == 'Windows' + run: mkdir -p "$APPDATA/shadPS4" + shell: bash + + - name: Build all tests + run: cmake --build build + shell: bash + + - name: Run tests with CTest + run: ctest --test-dir build --output-on-failure --progress + shell: bash + windows-sdl: runs-on: windows-2025 needs: get-info steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: recursive - name: Cache CMake Configuration - uses: actions/cache@v4 + uses: actions/cache@v5 env: cache-name: ${{ runner.os }}-sdl-ninja-cache-cmake-configuration with: - path: | + path: | ${{github.workspace}}/build key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} restore-keys: | ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.21 env: cache-name: ${{ runner.os }}-sdl-cache-cmake-build with: @@ -99,7 +176,7 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $env:NUMBER_OF_PROCESSORS - name: Upload Windows SDL artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: shadps4-win64-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: ${{github.workspace}}/build/shadPS4.exe @@ -108,7 +185,7 @@ jobs: runs-on: macos-15 needs: get-info steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: recursive @@ -118,18 +195,18 @@ jobs: xcode-version: latest - name: Cache CMake Configuration - uses: actions/cache@v4 - env: + uses: actions/cache@v5 + env: cache-name: ${{ runner.os }}-sdl-cache-cmake-configuration - with: - path: | - ${{github.workspace}}/build - key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - restore-keys: | - ${{ env.cache-name }}- + with: + path: | + ${{github.workspace}}/build + key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} + restore-keys: | + ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.21 env: cache-name: ${{runner.os}}-sdl-cache-cmake-build with: @@ -150,7 +227,7 @@ jobs: mv ${{github.workspace}}/build/shadps4 upload mv ${{github.workspace}}/build/MoltenVK_icd.json upload mv ${{github.workspace}}/build/libMoltenVK.dylib upload - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: shadps4-macos-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: upload/ @@ -159,7 +236,7 @@ jobs: runs-on: ubuntu-24.04 needs: get-info steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: recursive @@ -172,18 +249,18 @@ jobs: run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 clang-19 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev - name: Cache CMake Configuration - uses: actions/cache@v4 - env: + uses: actions/cache@v5 + env: cache-name: ${{ runner.os }}-sdl-cache-cmake-configuration - with: - path: | - ${{github.workspace}}/build - key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - restore-keys: | - ${{ env.cache-name }}- + with: + path: | + ${{github.workspace}}/build + key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} + restore-keys: | + ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.21 env: cache-name: ${{ runner.os }}-sdl-cache-cmake-build with: @@ -195,23 +272,23 @@ jobs: - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc) - - - name: Package and Upload Linux(ubuntu64) SDL artifact + + - name: Package and Upload Linux(ubuntu64) SDL artifact run: | ls -la ${{ github.workspace }}/build/shadps4 - - - uses: actions/upload-artifact@v4 + + - uses: actions/upload-artifact@v7 with: name: shadps4-ubuntu64-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: ${{ github.workspace }}/build/shadps4 - name: Run AppImage packaging script run: ./.github/linux-appimage-sdl.sh - + - name: Package and Upload Linux SDL artifact run: | tar cf shadps4-linux-sdl.tar.gz -C ${{github.workspace}}/build shadps4 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: shadps4-linux-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: Shadps4-sdl.AppImage @@ -220,7 +297,7 @@ jobs: runs-on: ubuntu-24.04 needs: get-info steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: recursive @@ -228,18 +305,18 @@ jobs: run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 gcc-14 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev - name: Cache CMake Configuration - uses: actions/cache@v4 - env: + uses: actions/cache@v5 + env: cache-name: ${{ runner.os }}-sdl-gcc-cache-cmake-configuration - with: - path: | - ${{github.workspace}}/build - key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - restore-keys: | - ${{ env.cache-name }}- + with: + path: | + ${{github.workspace}}/build + key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} + restore-keys: | + ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.19 + uses: hendrikmuhs/ccache-action@v1.2.21 env: cache-name: ${{ runner.os }}-sdl-gcc-cache-cmake-build with: @@ -258,7 +335,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download all artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v8 with: path: ./artifacts @@ -266,7 +343,7 @@ jobs: run: | chmod -R a+x ./artifacts/shadps4-linux-sdl-* chmod -R a+x ./artifacts/shadps4-macos-sdl-* - + - name: Compress individual directories (without parent directory) run: | cd ./artifacts @@ -277,7 +354,7 @@ jobs: (cd "$dir_name" && zip -r "../${dir_name}.zip" .) fi done - + - name: Get latest release information id: get_latest_release env: @@ -351,52 +428,52 @@ jobs: upload_url="https://uploads.github.com/repos/$REPO/releases/$release_id/assets?name=$filename" curl -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/octet-stream" --data-binary @"$file" "$upload_url" done - + - name: Get current pre-release information env: GITHUB_TOKEN: ${{ secrets.SHADPS4_TOKEN_REPO }} run: | api_url="https://api.github.com/repos/${{ github.repository }}/releases" - + # Get all releases (sorted by date) releases=$(curl -H "Authorization: token $GITHUB_TOKEN" "$api_url") - + # Capture the most recent pre-release (assuming the first one is the latest) current_release=$(echo "$releases" | jq -c '.[] | select(.prerelease == true) | .published_at' | sort -r | head -n 1) - + # Remove extra quotes from captured date current_release=$(echo $current_release | tr -d '"') - + # Export the current published_at to be available for the next step echo "CURRENT_PUBLISHED_AT=$current_release" >> $GITHUB_ENV - + - name: Delete old pre-releases and tags env: GITHUB_TOKEN: ${{ secrets.SHADPS4_TOKEN_REPO }} run: | api_url="https://api.github.com/repos/${{ github.repository }}/releases" - + # Get current pre-releases releases=$(curl -H "Authorization: token $GITHUB_TOKEN" "$api_url") - + # Remove extra quotes from captured date CURRENT_PUBLISHED_AT=$(echo $CURRENT_PUBLISHED_AT | tr -d '"') - + # Convert CURRENT_PUBLISHED_AT para timestamp Unix current_published_ts=$(date -d "$CURRENT_PUBLISHED_AT" +%s) - + # Identify pre-releases echo "$releases" | jq -c '.[] | select(.prerelease == true)' | while read -r release; do release_date=$(echo "$release" | jq -r '.published_at') release_id=$(echo "$release" | jq -r '.id') release_tag=$(echo "$release" | jq -r '.tag_name') - + # Remove extra quotes from captured date release_date=$(echo $release_date | tr -d '"') - + # Convert release_date para timestamp Unix release_date_ts=$(date -d "$release_date" +%s) - + # Compare timestamps and delete old pre-releases if [[ "$release_date_ts" -lt "$current_published_ts" ]]; then echo "Deleting old pre-release: $release_id from $release_date with tag: $release_tag" diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d5225602..6bb22db33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ endif() option(ENABLE_DISCORD_RPC "Enable the Discord RPC integration" ON) option(ENABLE_UPDATER "Enables the options to updater" ON) +option(ENABLE_TESTS "Build unit tests (requires GTest)" OFF) # First, determine whether to use CMAKE_OSX_ARCHITECTURES or CMAKE_SYSTEM_PROCESSOR. if (APPLE AND CMAKE_OSX_ARCHITECTURES) @@ -202,7 +203,7 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") -set(EMULATOR_VERSION_MINOR "14") +set(EMULATOR_VERSION_MINOR "15") set(EMULATOR_VERSION_PATCH "1") set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}") @@ -295,8 +296,15 @@ set(AUDIO_LIB src/core/libraries/audio/audioin.cpp src/core/libraries/audio/audioout_backend.h src/core/libraries/audio/audioout_error.h src/core/libraries/audio/sdl_audio_out.cpp + src/core/libraries/audio/openal_audio_out.cpp + src/core/libraries/audio/openal_manager.h src/core/libraries/ngs2/ngs2.cpp src/core/libraries/ngs2/ngs2.h + src/core/libraries/audio3d/audio3d.cpp + src/core/libraries/audio3d/audio3d_openal.cpp + src/core/libraries/audio3d/audio3d_openal.h + src/core/libraries/audio3d/audio3d.h + src/core/libraries/audio3d/audio3d_error.h ) set(GNM_LIB src/core/libraries/gnmdriver/gnmdriver.cpp @@ -414,9 +422,12 @@ set(SYSTEM_LIBS src/core/libraries/system/commondialog.cpp src/core/libraries/save_data/dialog/savedatadialog.h src/core/libraries/save_data/dialog/savedatadialog_ui.cpp src/core/libraries/save_data/dialog/savedatadialog_ui.h - src/core/libraries/system/sysmodule.cpp - src/core/libraries/system/sysmodule.h - src/core/libraries/system/system_error.h + src/core/libraries/sysmodule/sysmodule.cpp + src/core/libraries/sysmodule/sysmodule.h + src/core/libraries/sysmodule/sysmodule_internal.cpp + src/core/libraries/sysmodule/sysmodule_internal.h + src/core/libraries/sysmodule/sysmodule_error.h + src/core/libraries/sysmodule/sysmodule_table.h src/core/libraries/system/systemservice.cpp src/core/libraries/system/systemservice.h src/core/libraries/system/systemservice_error.h @@ -868,6 +879,12 @@ set(CORE src/core/aerolib/stubs.cpp src/core/tls.h src/core/emulator_state.cpp src/core/emulator_state.h + src/core/emulator_settings.cpp + src/core/emulator_settings.h + src/core/user_manager.cpp + src/core/user_manager.h + src/core/user_settings.cpp + src/core/user_settings.h ) if (ARCHITECTURE STREQUAL "x86_64") @@ -1099,6 +1116,8 @@ set(EMULATOR src/emulator.cpp src/sdl_window.cpp ) +if(NOT ENABLE_TESTS) + add_executable(shadps4 ${AUDIO_CORE} ${IMGUI} @@ -1252,3 +1271,8 @@ endif() # Install rules install(TARGETS shadps4 BUNDLE DESTINATION .) + +else() + enable_testing() + add_subdirectory(tests) +endif() \ No newline at end of file diff --git a/dist/net.shadps4.shadPS4.metainfo.xml b/dist/net.shadps4.shadPS4.metainfo.xml index 210ca1c5e..8a7fa852b 100644 --- a/dist/net.shadps4.shadPS4.metainfo.xml +++ b/dist/net.shadps4.shadPS4.metainfo.xml @@ -38,7 +38,10 @@ Game - + + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.15.0 + + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.14.0 diff --git a/src/common/elf_info.h b/src/common/elf_info.h index 0f2311cb0..8f79c9e69 100644 --- a/src/common/elf_info.h +++ b/src/common/elf_info.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "assert.h" #include "bit_field.h" @@ -73,6 +74,7 @@ class ElfInfo { std::filesystem::path splash_path{}; std::filesystem::path game_folder{}; + std::vector npCommIds{}; public: static constexpr u32 FW_10 = 0x1000000; @@ -88,7 +90,10 @@ public: static constexpr u32 FW_50 = 0x5000000; static constexpr u32 FW_55 = 0x5500000; static constexpr u32 FW_60 = 0x6000000; + static constexpr u32 FW_70 = 0x7000000; + static constexpr u32 FW_75 = 0x7500000; static constexpr u32 FW_80 = 0x8000000; + static constexpr u32 FW_115 = 0x11500000; static ElfInfo& Instance() { return *Singleton::Instance(); @@ -136,6 +141,10 @@ public: [[nodiscard]] const std::filesystem::path& GetGameFolder() const { return game_folder; } + + [[nodiscard]] const std::vector GetNpCommIds() const { + return npCommIds; + } }; } // namespace Common diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 930b1ac30..5de4f64a0 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -14,7 +14,6 @@ #endif #include "common/bounded_threadsafe_queue.h" -#include "common/config.h" #include "common/debug.h" #include "common/io_file.h" #include "common/logging/backend.h" @@ -24,6 +23,7 @@ #include "common/path_util.h" #include "common/string_util.h" #include "common/thread.h" +#include "core/emulator_settings.h" namespace Common::Log { @@ -141,7 +141,7 @@ public: const auto& log_dir = GetUserPath(PathType::LogDir); std::filesystem::create_directory(log_dir); Filter filter; - filter.ParseFilterString(Config::getLogFilter()); + filter.ParseFilterString(EmulatorSettings.GetLogFilter()); const auto& log_file_path = log_file.empty() ? LOG_FILE : log_file; instance = std::unique_ptr( new Impl(log_dir / log_file_path, filter), Deleter); @@ -185,7 +185,7 @@ public: void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num, const char* function, const char* format, const fmt::format_args& args) { - if (!filter.CheckMessage(log_class, log_level) || !Config::getLoggingEnabled()) { + if (!filter.CheckMessage(log_class, log_level) || !EmulatorSettings.IsLogEnabled()) { return; } @@ -213,7 +213,7 @@ public: using std::chrono::microseconds; using std::chrono::steady_clock; - if (Config::groupIdenticalLogs()) { + if (EmulatorSettings.IsIdenticalLogGrouped()) { std::unique_lock entry_loc(_mutex); if (_last_entry.message == message) { @@ -226,7 +226,7 @@ public: } if (_last_entry.counter >= 1) { - if (Config::getLogType() == "async") { + if (EmulatorSettings.GetLogType() == "async") { message_queue.EmplaceWait(_last_entry); } else { ForEachBackend([this](auto& backend) { backend.Write(this->_last_entry); }); @@ -258,7 +258,7 @@ public: .counter = 1, }; - if (Config::getLogType() == "async") { + if (EmulatorSettings.GetLogType() == "async") { message_queue.EmplaceWait(entry); } else { ForEachBackend([&entry](auto& backend) { backend.Write(entry); }); @@ -296,14 +296,14 @@ private: } void StopBackendThread() { - if (Config::groupIdenticalLogs()) { + if (EmulatorSettings.IsIdenticalLogGrouped()) { // log last message if (_last_entry.counter >= 2) { _last_entry.message += " x" + std::to_string(_last_entry.counter); } if (_last_entry.counter >= 1) { - if (Config::getLogType() == "async") { + if (EmulatorSettings.GetLogType() == "async") { message_queue.EmplaceWait(_last_entry); } else { ForEachBackend([this](auto& backend) { backend.Write(this->_last_entry); }); diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 9a3fe0aa1..fd48faf72 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -68,6 +68,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(Common) \ SUB(Common, Filesystem) \ SUB(Common, Memory) \ + CLS(KeyManager) \ CLS(Core) \ SUB(Core, Linker) \ SUB(Core, Devices) \ @@ -80,7 +81,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Kernel, Event) \ SUB(Kernel, Sce) \ CLS(Lib) \ - SUB(Lib, LibC) \ SUB(Lib, LibcInternal) \ SUB(Lib, Kernel) \ SUB(Lib, Pad) \ @@ -117,7 +117,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, NpSnsFacebookDialog) \ SUB(Lib, NpPartner) \ SUB(Lib, Screenshot) \ - SUB(Lib, LibCInternal) \ SUB(Lib, AppContent) \ SUB(Lib, Rtc) \ SUB(Lib, Rudp) \ @@ -163,7 +162,6 @@ 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/types.h b/src/common/logging/types.h index 9e176c698..2c6edef3b 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -34,6 +34,7 @@ enum class Class : u8 { Common, ///< Library routines Common_Filesystem, ///< Filesystem interface library Common_Memory, ///< Memory mapping and management functions + KeyManager, ///< Key management system Core, ///< LLE emulation core Core_Linker, ///< The module linker Core_Devices, ///< Devices emulation @@ -44,10 +45,9 @@ enum class Class : u8 { Kernel_Fs, ///< The filesystem implementation of the kernel. Kernel_Vmm, ///< The virtual memory implementation of the kernel. Kernel_Event, ///< The event management implementation of the kernel. - Kernel_Sce, ///< The sony specific interfaces provided by the kernel. + Kernel_Sce, ///< The Sony-specific interfaces provided by the kernel. Lib, ///< HLE implementation of system library. Each major library ///< should have its own subclass. - Lib_LibC, ///< The LibC implementation. Lib_LibcInternal, ///< The LibcInternal implementation. Lib_Kernel, ///< The LibKernel implementation. Lib_Pad, ///< The LibScePad implementation. @@ -83,7 +83,6 @@ enum class Class : u8 { 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. @@ -131,7 +130,6 @@ 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 a7c020246..2517e3f22 100644 --- a/src/common/memory_patcher.cpp +++ b/src/common/memory_patcher.cpp @@ -8,7 +8,6 @@ #include #include #include -#include "common/config.h" #include "common/elf_info.h" #include "common/logging/log.h" #include "common/path_util.h" diff --git a/src/common/path_util.cpp b/src/common/path_util.cpp index 5d37990ff..103f17d29 100644 --- a/src/common/path_util.cpp +++ b/src/common/path_util.cpp @@ -129,6 +129,7 @@ static auto UserPaths = [] { create_path(PathType::CustomConfigs, user_dir / CUSTOM_CONFIGS); create_path(PathType::CacheDir, user_dir / CACHE_DIR); create_path(PathType::FontsDir, user_dir / FONTS_DIR); + create_path(PathType::HomeDir, user_dir / HOME_DIR); std::ofstream notice_file(user_dir / CUSTOM_TROPHY / "Notice.txt"); if (notice_file.is_open()) { diff --git a/src/common/path_util.h b/src/common/path_util.h index 434f77b0d..485c72270 100644 --- a/src/common/path_util.h +++ b/src/common/path_util.h @@ -26,6 +26,7 @@ enum class PathType { CustomConfigs, // Where custom files for different games are stored. CacheDir, // Where pipeline and shader cache is stored. FontsDir, // Where dumped system fonts are stored. + HomeDir, // PS4 home directory }; constexpr auto PORTABLE_DIR = "user"; @@ -46,6 +47,7 @@ constexpr auto CUSTOM_TROPHY = "custom_trophy"; constexpr auto CUSTOM_CONFIGS = "custom_configs"; constexpr auto CACHE_DIR = "cache"; constexpr auto FONTS_DIR = "fonts"; +constexpr auto HOME_DIR = "home"; // Filenames constexpr auto LOG_FILE = "shad_log.txt"; diff --git a/src/common/serdes.h b/src/common/serdes.h index a36fed4d3..f91a0ace8 100644 --- a/src/common/serdes.h +++ b/src/common/serdes.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -7,6 +7,7 @@ #include "common/types.h" #include +#include namespace Serialization { diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index 758c7240c..ca3d52042 100644 --- a/src/core/address_space.cpp +++ b/src/core/address_space.cpp @@ -1,14 +1,14 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/alignment.h" #include "common/arch.h" #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/error.h" #include "core/address_space.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/memory.h" #include "core/memory.h" #include "libraries/error_codes.h" @@ -187,7 +187,7 @@ struct AddressSpace::Impl { user_size = supported_user_max - USER_MIN - 1; // Increase BackingSize to account for config options. - BackingSize += Config::getExtraDmemInMbytes() * 1_MB; + BackingSize += EmulatorSettings.GetExtraDmemInMBytes() * 1_MB; // Allocate backing file that represents the total physical memory. backing_handle = CreateFileMapping2(INVALID_HANDLE_VALUE, nullptr, FILE_MAP_ALL_ACCESS, @@ -606,7 +606,7 @@ enum PosixPageProtection { struct AddressSpace::Impl { Impl() { - BackingSize += Config::getExtraDmemInMbytes() * 1_MB; + BackingSize += EmulatorSettings.GetExtraDmemInMBytes() * 1_MB; // Allocate virtual address placeholder for our address space. system_managed_size = SystemManagedSize; system_reserved_size = SystemReservedSize; diff --git a/src/core/aerolib/stubs.cpp b/src/core/aerolib/stubs.cpp index 2634fc46a..7023144d7 100644 --- a/src/core/aerolib/stubs.cpp +++ b/src/core/aerolib/stubs.cpp @@ -19,7 +19,7 @@ namespace Core::AeroLib { // and to longer compile / CI times // // Must match STUBS_LIST define -constexpr u32 MAX_STUBS = 1024; +constexpr u32 MAX_STUBS = 2048; u64 UnresolvedStub() { LOG_ERROR(Core, "Returning zero to {}", __builtin_return_address(0)); @@ -61,8 +61,9 @@ static u32 UsedStubEntries; #define XREP_256(x) XREP_128(x) XREP_128(x + 128) #define XREP_512(x) XREP_256(x) XREP_256(x + 256) #define XREP_1024(x) XREP_512(x) XREP_512(x + 512) +#define XREP_2048(x) XREP_1024(x) XREP_1024(x + 1024) -#define STUBS_LIST XREP_1024(0) +#define STUBS_LIST XREP_2048(0) static u64 (*stub_handlers[MAX_STUBS])() = {STUBS_LIST}; diff --git a/src/core/cpu_patches.cpp b/src/core/cpu_patches.cpp index e303417c3..f6a4f7620 100644 --- a/src/core/cpu_patches.cpp +++ b/src/core/cpu_patches.cpp @@ -788,14 +788,11 @@ static bool PatchesIllegalInstructionHandler(void* context) { ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; const auto status = 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}", reinterpret_cast(code_address)); - } - UNREACHABLE_MSG("Failed to patch address {:x} -- mnemonic: {}", - reinterpret_cast(code_address), - ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic) - : "Failed to decode"); + LOG_ERROR(Core, "Failed to patch address {:x} -- mnemonic: {}", + reinterpret_cast(code_address), + ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic) + : "Failed to decode"); + return false; } } diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index 4be107713..10e5f911c 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -7,10 +7,10 @@ #include #include "SDL3/SDL_log.h" -#include "common/config.h" #include "common/singleton.h" #include "common/types.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/emulator_state.h" #include "imgui/imgui_std.h" #include "imgui_internal.h" @@ -110,11 +110,11 @@ void L::DrawMenuBar() { EndDisabled(); if (Button("Save")) { - Config::setFsrEnabled(fsr.enable); - Config::setRcasEnabled(fsr.use_rcas); - Config::setRcasAttenuation(static_cast(fsr.rcas_attenuation * 1000)); - Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / - "config.toml"); + EmulatorSettings.SetFsrEnabled(fsr.enable); + EmulatorSettings.SetRcasEnabled(fsr.use_rcas); + EmulatorSettings.SetRcasAttenuation( + static_cast(fsr.rcas_attenuation * 1000)); + EmulatorSettings.Save(); CloseCurrentPopup(); } @@ -311,7 +311,7 @@ static void LoadSettings(const char* line) { void L::SetupSettings() { frame_graph.is_open = true; - show_simple_fps = Config::getShowFpsCounter(); + show_simple_fps = EmulatorSettings.IsShowFpsCounter(); using SettingLoader = void (*)(const char*); @@ -472,7 +472,7 @@ void L::Draw() { if (ImGui::Begin("Volume Window", &show_volume, ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) { - Text("Volume: %d", Config::getVolumeSlider()); + Text("Volume: %d", EmulatorSettings.GetVolumeSlider()); } End(); } diff --git a/src/core/devtools/widget/frame_graph.cpp b/src/core/devtools/widget/frame_graph.cpp index 6b63d4978..6d4452074 100644 --- a/src/core/devtools/widget/frame_graph.cpp +++ b/src/core/devtools/widget/frame_graph.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "frame_graph.h" -#include "common/config.h" #include "common/singleton.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "imgui.h" #include "imgui_internal.h" @@ -29,7 +29,7 @@ void FrameGraph::DrawFrameGraph() { return; } - float target_dt = 1.0f / (float)Config::vblankFreq(); + float target_dt = 1.0f / (float)EmulatorSettings.GetVblankFrequency(); float cur_pos_x = pos.x + full_width; pos.y += FRAME_GRAPH_PADDING_Y; const float final_pos_y = pos.y + FRAME_GRAPH_HEIGHT; diff --git a/src/core/devtools/widget/module_list.h b/src/core/devtools/widget/module_list.h index 0702ac4db..4eed5444d 100644 --- a/src/core/devtools/widget/module_list.h +++ b/src/core/devtools/widget/module_list.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -8,9 +8,9 @@ #include #include #include -#include "common/config.h" #include "common/elf_info.h" #include "common/path_util.h" +#include "core/emulator_settings.h" namespace Core::Devtools::Widget { @@ -23,7 +23,7 @@ public: bool open = false; static bool IsSystemModule(const std::filesystem::path& path) { - const auto sys_modules_path = Config::getSysModulesPath(); + const auto sys_modules_path = EmulatorSettings.GetSysModulesDir(); const auto abs_path = std::filesystem::absolute(path).lexically_normal(); const auto abs_sys_path = std::filesystem::absolute(sys_modules_path).lexically_normal(); diff --git a/src/core/devtools/widget/shader_list.cpp b/src/core/devtools/widget/shader_list.cpp index 0285db5a5..243e2355f 100644 --- a/src/core/devtools/widget/shader_list.cpp +++ b/src/core/devtools/widget/shader_list.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 @@ -8,11 +8,11 @@ #include #include "common.h" -#include "common/config.h" #include "common/path_util.h" #include "common/string_util.h" #include "core/debug_state.h" #include "core/devtools/options.h" +#include "core/emulator_settings.h" #include "imgui/imgui_std.h" #include "sdl_window.h" #include "video_core/renderer_vulkan/vk_presenter.h" @@ -244,8 +244,8 @@ void ShaderList::Draw() { return; } - if (!Config::collectShadersForDebug()) { - DrawCenteredText("Enable 'CollectShader' in config to see shaders"); + if (!EmulatorSettings.IsShaderCollect()) { + DrawCenteredText("Enable 'shader_collect' in config to see shaders"); End(); return; } diff --git a/src/core/emulator_settings.cpp b/src/core/emulator_settings.cpp new file mode 100644 index 000000000..066c23af8 --- /dev/null +++ b/src/core/emulator_settings.cpp @@ -0,0 +1,688 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "emulator_settings.h" +#include "emulator_state.h" + +#include + +using json = nlohmann::json; + +// ── Singleton storage ───────────────────────────────────────────────── +std::shared_ptr EmulatorSettingsImpl::s_instance = nullptr; +std::mutex EmulatorSettingsImpl::s_mutex; + +// ── nlohmann helpers for std::filesystem::path ─────────────────────── +namespace nlohmann { +template <> +struct adl_serializer { + static void to_json(json& j, const std::filesystem::path& p) { + const auto u8 = p.u8string(); + j = std::string(reinterpret_cast(u8.data()), u8.size()); + } + static void from_json(const json& j, std::filesystem::path& p) { + const std::string s = j.get(); + p = std::filesystem::path( + std::u8string_view(reinterpret_cast(s.data()), s.size())); + } +}; +} // namespace nlohmann + +namespace toml { +// why is it so hard to avoid exceptions with this library +template +std::optional get_optional(const toml::value& v, const std::string& key) { + if (!v.is_table()) + return std::nullopt; + const auto& tbl = v.as_table(); + auto it = tbl.find(key); + if (it == tbl.end()) + return std::nullopt; + + if constexpr (std::is_same_v) { + if (it->second.is_integer()) { + return static_cast(toml::get(it->second)); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_integer()) { + return static_cast(toml::get(it->second)); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_floating()) { + return toml::get(it->second); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_string()) { + return toml::get(it->second); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_string()) { + return toml::get(it->second); + } + } else if constexpr (std::is_same_v) { + if (it->second.is_boolean()) { + return toml::get(it->second); + } + } else { + static_assert([] { return false; }(), "Unsupported type in get_optional"); + } + + return std::nullopt; +} + +} // namespace toml + +// ── Helpers ─────────────────────────────────────────────────────────── + +void EmulatorSettingsImpl::PrintChangedSummary(const std::vector& changed) { + if (changed.empty()) { + LOG_DEBUG(Config, "No game-specific overrides applied"); + return; + } + LOG_DEBUG(Config, "Game-specific overrides applied:"); + for (const auto& k : changed) + LOG_DEBUG(Config, " * {}", k); +} + +// ── Singleton ──────────────────────────────────────────────────────── +EmulatorSettingsImpl::EmulatorSettingsImpl() = default; + +EmulatorSettingsImpl::~EmulatorSettingsImpl() { + if (m_loaded) + Save(); +} + +std::shared_ptr EmulatorSettingsImpl::GetInstance() { + std::lock_guard lock(s_mutex); + if (!s_instance) + s_instance = std::make_shared(); + return s_instance; +} + +void EmulatorSettingsImpl::SetInstance(std::shared_ptr instance) { + std::lock_guard lock(s_mutex); + s_instance = std::move(instance); +} + +// -------------------- +// General helpers +// -------------------- +bool EmulatorSettingsImpl::AddGameInstallDir(const std::filesystem::path& dir, bool enabled) { + for (const auto& d : m_general.install_dirs.value) + if (d.path == dir) + return false; + m_general.install_dirs.value.push_back({dir, enabled}); + return true; +} + +std::vector EmulatorSettingsImpl::GetGameInstallDirs() const { + std::vector out; + for (const auto& d : m_general.install_dirs.value) + if (d.enabled) + out.push_back(d.path); + return out; +} + +const std::vector& EmulatorSettingsImpl::GetAllGameInstallDirs() const { + return m_general.install_dirs.value; +} + +void EmulatorSettingsImpl::SetAllGameInstallDirs(const std::vector& dirs) { + m_general.install_dirs.value = dirs; +} + +void EmulatorSettingsImpl::RemoveGameInstallDir(const std::filesystem::path& dir) { + auto iterator = + std::find_if(m_general.install_dirs.value.begin(), m_general.install_dirs.value.end(), + [&dir](const GameInstallDir& install_dir) { return install_dir.path == dir; }); + if (iterator != m_general.install_dirs.value.end()) { + m_general.install_dirs.value.erase(iterator); + } +} + +void EmulatorSettingsImpl::SetGameInstallDirEnabled(const std::filesystem::path& dir, + bool enabled) { + auto iterator = + std::find_if(m_general.install_dirs.value.begin(), m_general.install_dirs.value.end(), + [&dir](const GameInstallDir& install_dir) { return install_dir.path == dir; }); + if (iterator != m_general.install_dirs.value.end()) { + iterator->enabled = enabled; + } +} + +void EmulatorSettingsImpl::SetGameInstallDirs( + const std::vector& dirs_config) { + m_general.install_dirs.value.clear(); + for (const auto& dir : dirs_config) { + m_general.install_dirs.value.push_back({dir, true}); + } +} + +const std::vector EmulatorSettingsImpl::GetGameInstallDirsEnabled() { + std::vector enabled_dirs; + for (const auto& dir : m_general.install_dirs.value) { + enabled_dirs.push_back(dir.enabled); + } + return enabled_dirs; +} + +std::filesystem::path EmulatorSettingsImpl::GetHomeDir() { + if (m_general.home_dir.value.empty()) { + return Common::FS::GetUserPath(Common::FS::PathType::HomeDir); + } + return m_general.home_dir.value; +} + +void EmulatorSettingsImpl::SetHomeDir(const std::filesystem::path& dir) { + m_general.home_dir.value = dir; +} + +std::filesystem::path EmulatorSettingsImpl::GetSysModulesDir() { + if (m_general.sys_modules_dir.value.empty()) { + return Common::FS::GetUserPath(Common::FS::PathType::SysModuleDir); + } + return m_general.sys_modules_dir.value; +} + +void EmulatorSettingsImpl::SetSysModulesDir(const std::filesystem::path& dir) { + m_general.sys_modules_dir.value = dir; +} + +std::filesystem::path EmulatorSettingsImpl::GetFontsDir() { + if (m_general.font_dir.value.empty()) { + return Common::FS::GetUserPath(Common::FS::PathType::FontsDir); + } + return m_general.font_dir.value; +} + +void EmulatorSettingsImpl::SetFontsDir(const std::filesystem::path& dir) { + m_general.font_dir.value = dir; +} + +// ── Game-specific override management ──────────────────────────────── +void EmulatorSettingsImpl::ClearGameSpecificOverrides() { + ClearGroupOverrides(m_general); + ClearGroupOverrides(m_debug); + ClearGroupOverrides(m_input); + ClearGroupOverrides(m_audio); + ClearGroupOverrides(m_gpu); + ClearGroupOverrides(m_vulkan); + LOG_DEBUG(Config, "All game-specific overrides cleared"); +} + +void EmulatorSettingsImpl::ResetGameSpecificValue(const std::string& key) { + // Walk every overrideable group until we find the matching key. + auto tryGroup = [&key](auto& group) { + for (auto& item : group.GetOverrideableFields()) { + if (key == item.key) { + item.reset_game_specific(&group); + return true; + } + } + return false; + }; + if (tryGroup(m_general)) + return; + if (tryGroup(m_debug)) + return; + if (tryGroup(m_input)) + return; + if (tryGroup(m_audio)) + return; + if (tryGroup(m_gpu)) + return; + if (tryGroup(m_vulkan)) + return; + LOG_WARNING(Config, "ResetGameSpecificValue: key '{}' not found", key); +} + +bool EmulatorSettingsImpl::Save(const std::string& serial) { + try { + if (!serial.empty()) { + const auto cfgDir = Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs); + std::filesystem::create_directories(cfgDir); + const auto path = cfgDir / (serial + ".json"); + + json j = json::object(); + + json generalObj = json::object(); + SaveGroupGameSpecific(m_general, generalObj); + j["General"] = generalObj; + + json debugObj = json::object(); + SaveGroupGameSpecific(m_debug, debugObj); + j["Debug"] = debugObj; + + json inputObj = json::object(); + SaveGroupGameSpecific(m_input, inputObj); + j["Input"] = inputObj; + + json audioObj = json::object(); + SaveGroupGameSpecific(m_audio, audioObj); + j["Audio"] = audioObj; + + json gpuObj = json::object(); + SaveGroupGameSpecific(m_gpu, gpuObj); + j["GPU"] = gpuObj; + + json vulkanObj = json::object(); + SaveGroupGameSpecific(m_vulkan, vulkanObj); + j["Vulkan"] = vulkanObj; + + std::ofstream out(path); + if (!out) { + LOG_ERROR(Config, "Failed to open game config for writing: {}", path.string()); + return false; + } + out << std::setw(2) << j; + return !out.fail(); + + } else { + // ── Global config.json ───────────────────────────────────── + const auto path = + Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.json"; + + SetConfigVersion(Common::g_scm_rev); + + json j; + j["General"] = m_general; + j["Debug"] = m_debug; + j["Input"] = m_input; + j["Audio"] = m_audio; + j["GPU"] = m_gpu; + j["Vulkan"] = m_vulkan; + + // Read the existing file so we can preserve keys unknown to this build + json existing = json::object(); + if (std::ifstream existingIn{path}; existingIn.good()) { + try { + existingIn >> existing; + } catch (...) { + existing = json::object(); + } + } + + // Merge: update each section's known keys, but leave unknown keys intact + for (auto& [section, val] : j.items()) { + if (existing.contains(section) && existing[section].is_object() && val.is_object()) + existing[section].update(val); // overwrites known keys, keeps unknown ones + else + existing[section] = val; + } + + std::ofstream out(path); + if (!out) { + LOG_ERROR(Config, "Failed to open config for writing: {}", path.string()); + return false; + } + out << std::setw(2) << existing; + return !out.fail(); + } + } catch (const std::exception& e) { + LOG_ERROR(Config, "Error saving settings: {}", e.what()); + return false; + } +} + +// ── Load ────────────────────────────────────────────────────────────── + +bool EmulatorSettingsImpl::Load(const std::string& serial) { + try { + if (serial.empty()) { + // ── Global config ────────────────────────────────────────── + const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + const auto configPath = userDir / "config.json"; + LOG_DEBUG(Config, "Loading global config from: {}", configPath.string()); + + if (std::ifstream in{configPath}; in.good()) { + json gj; + in >> gj; + + auto mergeGroup = [&gj](auto& group, const char* section) { + if (!gj.contains(section)) + return; + json current = group; + current.update(gj.at(section)); + group = current.get>(); + }; + + mergeGroup(m_general, "General"); + mergeGroup(m_debug, "Debug"); + mergeGroup(m_input, "Input"); + mergeGroup(m_audio, "Audio"); + mergeGroup(m_gpu, "GPU"); + mergeGroup(m_vulkan, "Vulkan"); + + LOG_DEBUG(Config, "Global config loaded successfully"); + } else { + if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / + "config.toml")) { + SDL_MessageBoxButtonData btns[2]{ + {0, 0, "Defaults"}, + {0, 1, "Update"}, + }; + SDL_MessageBoxData msg_box{ + 0, + nullptr, + "Config Migration", + "The shadPS4 config backend has been updated, and you only have " + "the old version of the config. Do you wish to update it " + "automatically, or continue with the default config?", + 2, + btns, + nullptr, + }; + int result = 1; + SDL_ShowMessageBox(&msg_box, &result); + if (result == 1) { + if (TransferSettings()) { + m_loaded = true; + Save(); + return true; + } else { + SDL_ShowSimpleMessageBox(0, "Config Migration", + "Error transferring settings, exiting.", + nullptr); + std::quick_exit(1); + } + } + } + LOG_DEBUG(Config, "Global config not found - using defaults"); + SetDefaultValues(); + Save(); + } + if (GetConfigVersion() != Common::g_scm_rev) { + Save(); + } + m_loaded = true; + return true; + } else { + // ── Per-game override file ───────────────────────────────── + // Never reloads global settings. Only applies + // game_specific_value overrides on top of the already-loaded + // base configuration. + const auto gamePath = + Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (serial + ".json"); + LOG_DEBUG(Config, "Applying game config: {}", gamePath.string()); + + if (!std::filesystem::exists(gamePath)) { + LOG_DEBUG(Config, "No game-specific config found for {}", serial); + return false; + } + + std::ifstream in(gamePath); + if (!in) { + LOG_ERROR(Config, "Failed to open game config: {}", gamePath.string()); + return false; + } + + json gj; + in >> gj; + + std::vector changed; + + // ApplyGroupOverrides now correctly stores values as + // game_specific_value (see make_override in the header). + // ConfigMode::Default will then resolve them at getter call + // time without ever touching the base values. + if (gj.contains("General")) + ApplyGroupOverrides(m_general, gj.at("General"), changed); + if (gj.contains("Debug")) + ApplyGroupOverrides(m_debug, gj.at("Debug"), changed); + if (gj.contains("Input")) + ApplyGroupOverrides(m_input, gj.at("Input"), changed); + if (gj.contains("Audio")) + ApplyGroupOverrides(m_audio, gj.at("Audio"), changed); + if (gj.contains("GPU")) + ApplyGroupOverrides(m_gpu, gj.at("GPU"), changed); + if (gj.contains("Vulkan")) + ApplyGroupOverrides(m_vulkan, gj.at("Vulkan"), changed); + + PrintChangedSummary(changed); + EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(true); + return true; + } + } catch (const std::exception& e) { + LOG_ERROR(Config, "Error loading settings: {}", e.what()); + return false; + } +} + +void EmulatorSettingsImpl::SetDefaultValues() { + m_general = GeneralSettings{}; + m_debug = DebugSettings{}; + m_input = InputSettings{}; + m_audio = AudioSettings{}; + m_gpu = GPUSettings{}; + m_vulkan = VulkanSettings{}; +} + +bool EmulatorSettingsImpl::TransferSettings() { + toml::value og_data; + json new_data = json::object(); + try { + auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml"; + std::ifstream ifs; + ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit); + ifs.open(path, std::ios_base::binary); + og_data = toml::parse(ifs, std::string{fmt::UTF(path.filename().u8string()).data}); + } catch (std::exception& ex) { + fmt::print("Got exception trying to load config file. Exception: {}\n", ex.what()); + return false; + } + auto setFromToml = [&](Setting& n, toml::value const& t, std::string k) { + n = toml::get_optional(t, k).value_or(n.default_value); + }; + if (og_data.contains("General")) { + const toml::value& general = og_data.at("General"); + auto& s = m_general; + + setFromToml(s.volume_slider, general, "volumeSlider"); + setFromToml(s.neo_mode, general, "isPS4Pro"); + setFromToml(s.dev_kit_mode, general, "isDevKit"); + setFromToml(s.psn_signed_in, general, "isPSNSignedIn"); + setFromToml(s.trophy_popup_disabled, general, "isTrophyPopupDisabled"); + setFromToml(s.trophy_notification_duration, general, "trophyNotificationDuration"); + setFromToml(s.discord_rpc_enabled, general, "enableDiscordRPC"); + setFromToml(s.log_filter, general, "logFilter"); + setFromToml(s.log_type, general, "logType"); + setFromToml(s.identical_log_grouped, general, "isIdenticalLogGrouped"); + setFromToml(s.show_splash, general, "showSplash"); + setFromToml(s.trophy_notification_side, general, "sideTrophy"); + setFromToml(s.connected_to_network, general, "isConnectedToNetwork"); + setFromToml(s.sys_modules_dir, general, "sysModulesPath"); + setFromToml(s.font_dir, general, "fontsPath"); + // setFromToml(, general, "userName"); + // setFromToml(s.defaultControllerID, general, "defaultControllerID"); + } + + if (og_data.contains("Input")) { + const toml::value& input = og_data.at("Input"); + auto& s = m_input; + + setFromToml(s.cursor_state, input, "cursorState"); + setFromToml(s.cursor_hide_timeout, input, "cursorHideTimeout"); + setFromToml(s.use_special_pad, input, "useSpecialPad"); + setFromToml(s.special_pad_class, input, "specialPadClass"); + setFromToml(s.motion_controls_enabled, input, "isMotionControlsEnabled"); + setFromToml(s.use_unified_input_config, input, "useUnifiedInputConfig"); + setFromToml(s.background_controller_input, input, "backgroundControllerInput"); + setFromToml(s.usb_device_backend, input, "usbDeviceBackend"); + } + + if (og_data.contains("Audio")) { + const toml::value& audio = og_data.at("Audio"); + auto& s = m_audio; + + setFromToml(s.sdl_mic_device, audio, "micDevice"); + setFromToml(s.sdl_main_output_device, audio, "mainOutputDevice"); + setFromToml(s.sdl_padSpk_output_device, audio, "padSpkOutputDevice"); + } + + if (og_data.contains("GPU")) { + const toml::value& gpu = og_data.at("GPU"); + auto& s = m_gpu; + + setFromToml(s.window_width, gpu, "screenWidth"); + setFromToml(s.window_height, gpu, "screenHeight"); + setFromToml(s.internal_screen_width, gpu, "internalScreenWidth"); + setFromToml(s.internal_screen_height, gpu, "internalScreenHeight"); + setFromToml(s.null_gpu, gpu, "nullGpu"); + setFromToml(s.copy_gpu_buffers, gpu, "copyGPUBuffers"); + setFromToml(s.readbacks_mode, gpu, "readbacksMode"); + setFromToml(s.readback_linear_images_enabled, gpu, "readbackLinearImages"); + setFromToml(s.direct_memory_access_enabled, gpu, "directMemoryAccess"); + setFromToml(s.dump_shaders, gpu, "dumpShaders"); + setFromToml(s.patch_shaders, gpu, "patchShaders"); + setFromToml(s.vblank_frequency, gpu, "vblankFrequency"); + setFromToml(s.full_screen, gpu, "Fullscreen"); + setFromToml(s.full_screen_mode, gpu, "FullscreenMode"); + setFromToml(s.present_mode, gpu, "presentMode"); + setFromToml(s.hdr_allowed, gpu, "allowHDR"); + setFromToml(s.fsr_enabled, gpu, "fsrEnabled"); + setFromToml(s.rcas_enabled, gpu, "rcasEnabled"); + setFromToml(s.rcas_attenuation, gpu, "rcasAttenuation"); + } + + if (og_data.contains("Vulkan")) { + const toml::value& vk = og_data.at("Vulkan"); + auto& s = m_vulkan; + + setFromToml(s.gpu_id, vk, "gpuId"); + setFromToml(s.vkvalidation_enabled, vk, "validation"); + setFromToml(s.vkvalidation_core_enabled, vk, "validation_core"); + setFromToml(s.vkvalidation_sync_enabled, vk, "validation_sync"); + setFromToml(s.vkvalidation_gpu_enabled, vk, "validation_gpu"); + setFromToml(s.vkcrash_diagnostic_enabled, vk, "crashDiagnostic"); + setFromToml(s.vkhost_markers, vk, "hostMarkers"); + setFromToml(s.vkguest_markers, vk, "guestMarkers"); + setFromToml(s.renderdoc_enabled, vk, "rdocEnable"); + setFromToml(s.pipeline_cache_enabled, vk, "pipelineCacheEnable"); + setFromToml(s.pipeline_cache_archived, vk, "pipelineCacheArchive"); + } + + if (og_data.contains("Debug")) { + const toml::value& debug = og_data.at("Debug"); + auto& s = m_debug; + + setFromToml(s.debug_dump, debug, "DebugDump"); + setFromToml(s.separate_logging_enabled, debug, "isSeparateLogFilesEnabled"); + setFromToml(s.shader_collect, debug, "CollectShader"); + setFromToml(s.log_enabled, debug, "logEnabled"); + setFromToml(m_general.show_fps_counter, debug, "showFpsCounter"); + } + + if (og_data.contains("Settings")) { + const toml::value& settings = og_data.at("Settings"); + auto& s = m_general; + setFromToml(s.console_language, settings, "consoleLanguage"); + } + + if (og_data.contains("GUI")) { + const toml::value& gui = og_data.at("GUI"); + auto& s = m_general; + + // Transfer install directories + try { + const auto install_dir_array = + toml::find_or>(gui, "installDirs", {}); + std::vector install_dirs_enabled; + + try { + install_dirs_enabled = toml::find>(gui, "installDirsEnabled"); + } catch (...) { + // If it does not exist, assume that all are enabled. + install_dirs_enabled.resize(install_dir_array.size(), true); + } + + if (install_dirs_enabled.size() < install_dir_array.size()) { + install_dirs_enabled.resize(install_dir_array.size(), true); + } + + std::vector settings_install_dirs; + for (size_t i = 0; i < install_dir_array.size(); i++) { + settings_install_dirs.push_back( + {std::filesystem::path{install_dir_array[i]}, install_dirs_enabled[i]}); + } + s.install_dirs.value = settings_install_dirs; + } catch (const std::exception& e) { + LOG_WARNING(Config, "Failed to transfer install directories: {}", e.what()); + } + + // Transfer addon install directory + try { + std::string addon_install_dir_str; + if (gui.contains("addonInstallDir")) { + const auto& addon_value = gui.at("addonInstallDir"); + if (addon_value.is_string()) { + addon_install_dir_str = toml::get(addon_value); + if (!addon_install_dir_str.empty()) { + s.addon_install_dir.value = std::filesystem::path{addon_install_dir_str}; + } + } + } + } catch (const std::exception& e) { + LOG_WARNING(Config, "Failed to transfer addon install directory: {}", e.what()); + } + } + if (og_data.contains("General")) { + const toml::value& general = og_data.at("General"); + auto& s = m_general; + // Transfer sysmodules install directory + try { + std::string sysmodules_install_dir_str; + if (general.contains("sysModulesPath")) { + const auto& sysmodule_value = general.at("sysModulesPath"); + if (sysmodule_value.is_string()) { + sysmodules_install_dir_str = toml::get(sysmodule_value); + if (!sysmodules_install_dir_str.empty()) { + s.sys_modules_dir.value = std::filesystem::path{sysmodules_install_dir_str}; + } + } + } + } catch (const std::exception& e) { + LOG_WARNING(Config, "Failed to transfer sysmodules install directory: {}", e.what()); + } + + // Transfer font install directory + try { + std::string font_install_dir_str; + if (general.contains("fontsPath")) { + const auto& font_value = general.at("fontsPath"); + if (font_value.is_string()) { + font_install_dir_str = toml::get(font_value); + if (!font_install_dir_str.empty()) { + s.font_dir.value = std::filesystem::path{font_install_dir_str}; + } + } + } + } catch (const std::exception& e) { + LOG_WARNING(Config, "Failed to transfer font install directory: {}", e.what()); + } + } + + return true; +} + +std::vector EmulatorSettingsImpl::GetAllOverrideableKeys() const { + std::vector keys; + auto addGroup = [&keys](const auto& fields) { + for (const auto& item : fields) + keys.push_back(item.key); + }; + addGroup(m_general.GetOverrideableFields()); + addGroup(m_debug.GetOverrideableFields()); + addGroup(m_input.GetOverrideableFields()); + addGroup(m_audio.GetOverrideableFields()); + addGroup(m_gpu.GetOverrideableFields()); + addGroup(m_vulkan.GetOverrideableFields()); + return keys; +} diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h new file mode 100644 index 000000000..370fb0ab0 --- /dev/null +++ b/src/core/emulator_settings.h @@ -0,0 +1,639 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "common/types.h" + +#define EmulatorSettings (*EmulatorSettingsImpl::GetInstance()) + +enum HideCursorState : int { + Never, + Idle, + Always, +}; + +enum UsbBackendType : int { + Real, + SkylandersPortal, + InfinityBase, + DimensionsToypad, +}; + +enum GpuReadbacksMode : int { + Disabled, + Relaxed, + Precise, +}; + +enum class ConfigMode { + Default, + Global, + Clean, +}; + +enum AudioBackend : int { + SDL, + OpenAL, + // Add more backends as needed +}; + +template +struct Setting { + T default_value{}; + T value{}; + std::optional game_specific_value{}; + + Setting() = default; + // Single-argument ctor: initialises both default_value and value so + // that CleanMode can always recover the intended factory default. + /*implicit*/ Setting(T init) : default_value(std::move(init)), value(default_value) {} + + /// Return the active value under the given mode. + T get(ConfigMode mode = ConfigMode::Default) const { + switch (mode) { + case ConfigMode::Default: + return game_specific_value.value_or(value); + case ConfigMode::Global: + return value; + case ConfigMode::Clean: + return default_value; + } + return value; + } + + /// Write v to the base layer. + /// Game-specific overrides are applied exclusively via Load(serial) + void set(const T& v) { + value = v; + } + + /// Discard the game-specific override; subsequent get(Default) will + /// fall back to the base value. + void reset_game_specific() { + game_specific_value = std::nullopt; + } +}; + +template +void to_json(nlohmann::json& j, const Setting& s) { + j = s.value; +} + +template +void from_json(const nlohmann::json& j, Setting& s) { + s.value = j.get(); +} + +struct OverrideItem { + const char* key; + std::function& changed)> + apply; + /// Return the value that should be written to the per-game config file. + /// Falls back to base value if no game-specific override is set. + std::function get_for_save; + + /// Clear game_specific_value for this field. + std::function reset_game_specific; +}; + +template +inline OverrideItem make_override(const char* key, Setting Struct::* member) { + return OverrideItem{ + key, + [member, key](void* base, const nlohmann::json& entry, std::vector& changed) { + LOG_DEBUG(Config, "[make_override] Processing key: {}", key); + LOG_DEBUG(Config, "[make_override] Entry JSON: {}", entry.dump()); + Struct* obj = reinterpret_cast(base); + Setting& dst = obj->*member; + try { + T newValue = entry.get(); + LOG_DEBUG(Config, "[make_override] Parsed value: {}", newValue); + LOG_DEBUG(Config, "[make_override] Current value: {}", dst.value); + if (dst.value != newValue) { + std::ostringstream oss; + oss << key << " ( " << dst.value << " → " << newValue << " )"; + changed.push_back(oss.str()); + LOG_DEBUG(Config, "[make_override] Recorded change: {}", oss.str()); + } + dst.game_specific_value = newValue; + LOG_DEBUG(Config, "[make_override] Successfully updated {}", key); + } catch (const std::exception& e) { + LOG_ERROR(Config, "[make_override] ERROR parsing {}: {}", key, e.what()); + LOG_ERROR(Config, "[make_override] Entry was: {}", entry.dump()); + LOG_ERROR(Config, "[make_override] Type name: {}", entry.type_name()); + } + }, + + // --- get_for_save ------------------------------------------- + // Returns game_specific_value when present, otherwise base value. + // This means a freshly-opened game-specific dialog still shows + // useful (current-global) values rather than empty entries. + [member](const void* base) -> nlohmann::json { + const Struct* obj = reinterpret_cast(base); + const Setting& src = obj->*member; + return nlohmann::json(src.game_specific_value.value_or(src.value)); + }, + + // --- reset_game_specific ------------------------------------ + [member](void* base) { + Struct* obj = reinterpret_cast(base); + (obj->*member).reset_game_specific(); + }}; +} + +// ------------------------------- +// Support types +// ------------------------------- +struct GameInstallDir { + std::filesystem::path path; + bool enabled; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GameInstallDir, path, enabled) + +// ------------------------------- +// General settings +// ------------------------------- +struct GeneralSettings { + Setting> install_dirs; + Setting addon_install_dir; + Setting home_dir; + Setting sys_modules_dir; + Setting font_dir; + + Setting volume_slider{100}; + Setting neo_mode{false}; + Setting dev_kit_mode{false}; + Setting extra_dmem_in_mbytes{0}; + Setting psn_signed_in{false}; + Setting trophy_popup_disabled{false}; + Setting trophy_notification_duration{6.0}; + Setting trophy_notification_side{"right"}; + Setting log_filter{""}; + Setting log_type{"sync"}; + Setting show_splash{false}; + Setting identical_log_grouped{true}; + Setting connected_to_network{false}; + Setting discord_rpc_enabled{false}; + Setting show_fps_counter{false}; + Setting console_language{1}; + + // return a vector of override descriptors (runtime, but tiny) + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("volume_slider", &GeneralSettings::volume_slider), + make_override("neo_mode", &GeneralSettings::neo_mode), + make_override("dev_kit_mode", &GeneralSettings::dev_kit_mode), + make_override("extra_dmem_in_mbytes", + &GeneralSettings::extra_dmem_in_mbytes), + make_override("psn_signed_in", &GeneralSettings::psn_signed_in), + make_override("trophy_popup_disabled", + &GeneralSettings::trophy_popup_disabled), + make_override("trophy_notification_duration", + &GeneralSettings::trophy_notification_duration), + make_override("log_filter", &GeneralSettings::log_filter), + make_override("log_type", &GeneralSettings::log_type), + make_override("identical_log_grouped", + &GeneralSettings::identical_log_grouped), + make_override("show_splash", &GeneralSettings::show_splash), + make_override("trophy_notification_side", + &GeneralSettings::trophy_notification_side), + make_override("connected_to_network", + &GeneralSettings::connected_to_network)}; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, install_dirs, addon_install_dir, home_dir, + sys_modules_dir, font_dir, volume_slider, neo_mode, dev_kit_mode, + extra_dmem_in_mbytes, psn_signed_in, trophy_popup_disabled, + trophy_notification_duration, log_filter, log_type, show_splash, + identical_log_grouped, trophy_notification_side, + connected_to_network, discord_rpc_enabled, show_fps_counter, + console_language) + +// ------------------------------- +// Debug settings +// ------------------------------- +struct DebugSettings { + Setting separate_logging_enabled{false}; // specific + Setting debug_dump{false}; // specific + Setting shader_collect{false}; // specific + Setting log_enabled{true}; // specific + Setting config_version{""}; // specific + + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("debug_dump", &DebugSettings::debug_dump), + make_override("shader_collect", &DebugSettings::shader_collect), + make_override("separate_logging_enabled", + &DebugSettings::separate_logging_enabled), + make_override("log_enabled", &DebugSettings::log_enabled)}; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DebugSettings, separate_logging_enabled, debug_dump, + shader_collect, log_enabled, config_version) + +// ------------------------------- +// Input settings +// ------------------------------- + +struct InputSettings { + Setting cursor_state{HideCursorState::Idle}; // specific + Setting cursor_hide_timeout{5}; // specific + Setting usb_device_backend{UsbBackendType::Real}; // specific + Setting use_special_pad{false}; + Setting special_pad_class{1}; + Setting motion_controls_enabled{true}; // specific + Setting use_unified_input_config{true}; + Setting default_controller_id{""}; + Setting background_controller_input{false}; // specific + Setting camera_id{-1}; + + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("cursor_state", &InputSettings::cursor_state), + make_override("cursor_hide_timeout", + &InputSettings::cursor_hide_timeout), + make_override("usb_device_backend", &InputSettings::usb_device_backend), + make_override("motion_controls_enabled", + &InputSettings::motion_controls_enabled), + make_override("background_controller_input", + &InputSettings::background_controller_input), + make_override("camera_id", &InputSettings::camera_id)}; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(InputSettings, cursor_state, cursor_hide_timeout, + usb_device_backend, use_special_pad, special_pad_class, + motion_controls_enabled, use_unified_input_config, + default_controller_id, background_controller_input, camera_id) +// ------------------------------- +// Audio settings +// ------------------------------- +struct AudioSettings { + Setting audio_backend{AudioBackend::SDL}; + Setting sdl_mic_device{"Default Device"}; + Setting sdl_main_output_device{"Default Device"}; + Setting sdl_padSpk_output_device{"Default Device"}; + Setting openal_mic_device{"Default Device"}; + Setting openal_main_output_device{"Default Device"}; + Setting openal_padSpk_output_device{"Default Device"}; + + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("audio_backend", &AudioSettings::audio_backend), + make_override("sdl_mic_device", &AudioSettings::sdl_mic_device), + make_override("sdl_main_output_device", + &AudioSettings::sdl_main_output_device), + make_override("sdl_padSpk_output_device", + &AudioSettings::sdl_padSpk_output_device), + make_override("openal_mic_device", &AudioSettings::openal_mic_device), + make_override("openal_main_output_device", + &AudioSettings::openal_main_output_device), + make_override("openal_padSpk_output_device", + &AudioSettings::openal_padSpk_output_device)}; + } +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AudioSettings, audio_backend, sdl_mic_device, + sdl_main_output_device, sdl_padSpk_output_device, + openal_mic_device, openal_main_output_device, + openal_padSpk_output_device) + +// ------------------------------- +// GPU settings +// ------------------------------- +struct GPUSettings { + Setting window_width{1280}; + Setting window_height{720}; + Setting internal_screen_width{1280}; + Setting internal_screen_height{720}; + Setting null_gpu{false}; + Setting copy_gpu_buffers{false}; + Setting readbacks_mode{GpuReadbacksMode::Disabled}; + Setting readback_linear_images_enabled{false}; + Setting direct_memory_access_enabled{false}; + Setting dump_shaders{false}; + Setting patch_shaders{false}; + Setting vblank_frequency{60}; + Setting full_screen{false}; + Setting full_screen_mode{"Windowed"}; + Setting present_mode{"Mailbox"}; + Setting hdr_allowed{false}; + Setting fsr_enabled{false}; + Setting rcas_enabled{true}; + Setting rcas_attenuation{250}; + // TODO add overrides + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("null_gpu", &GPUSettings::null_gpu), + make_override("copy_gpu_buffers", &GPUSettings::copy_gpu_buffers), + make_override("full_screen", &GPUSettings::full_screen), + make_override("full_screen_mode", &GPUSettings::full_screen_mode), + make_override("present_mode", &GPUSettings::present_mode), + make_override("window_height", &GPUSettings::window_height), + make_override("window_width", &GPUSettings::window_width), + make_override("hdr_allowed", &GPUSettings::hdr_allowed), + make_override("fsr_enabled", &GPUSettings::fsr_enabled), + make_override("rcas_enabled", &GPUSettings::rcas_enabled), + make_override("rcas_attenuation", &GPUSettings::rcas_attenuation), + make_override("dump_shaders", &GPUSettings::dump_shaders), + make_override("patch_shaders", &GPUSettings::patch_shaders), + make_override("readbacks_mode", &GPUSettings::readbacks_mode), + make_override("readback_linear_images_enabled", + &GPUSettings::readback_linear_images_enabled), + make_override("direct_memory_access_enabled", + &GPUSettings::direct_memory_access_enabled), + make_override("vblank_frequency", &GPUSettings::vblank_frequency), + }; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GPUSettings, window_width, window_height, internal_screen_width, + internal_screen_height, null_gpu, copy_gpu_buffers, + readbacks_mode, readback_linear_images_enabled, + direct_memory_access_enabled, dump_shaders, patch_shaders, + vblank_frequency, full_screen, full_screen_mode, present_mode, + hdr_allowed, fsr_enabled, rcas_enabled, rcas_attenuation) +// ------------------------------- +// Vulkan settings +// ------------------------------- +struct VulkanSettings { + Setting gpu_id{-1}; + Setting renderdoc_enabled{false}; + Setting vkvalidation_enabled{false}; + Setting vkvalidation_core_enabled{true}; + Setting vkvalidation_sync_enabled{false}; + Setting vkvalidation_gpu_enabled{false}; + Setting vkcrash_diagnostic_enabled{false}; + Setting vkhost_markers{false}; + Setting vkguest_markers{false}; + Setting pipeline_cache_enabled{false}; + Setting pipeline_cache_archived{false}; + std::vector GetOverrideableFields() const { + return std::vector{ + make_override("gpu_id", &VulkanSettings::gpu_id), + make_override("renderdoc_enabled", &VulkanSettings::renderdoc_enabled), + make_override("vkvalidation_enabled", + &VulkanSettings::vkvalidation_enabled), + make_override("vkvalidation_core_enabled", + &VulkanSettings::vkvalidation_core_enabled), + make_override("vkvalidation_sync_enabled", + &VulkanSettings::vkvalidation_sync_enabled), + make_override("vkvalidation_gpu_enabled", + &VulkanSettings::vkvalidation_gpu_enabled), + make_override("vkcrash_diagnostic_enabled", + &VulkanSettings::vkcrash_diagnostic_enabled), + make_override("vkhost_markers", &VulkanSettings::vkhost_markers), + make_override("vkguest_markers", &VulkanSettings::vkguest_markers), + make_override("pipeline_cache_enabled", + &VulkanSettings::pipeline_cache_enabled), + make_override("pipeline_cache_archived", + &VulkanSettings::pipeline_cache_archived), + }; + } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(VulkanSettings, gpu_id, renderdoc_enabled, vkvalidation_enabled, + vkvalidation_core_enabled, vkvalidation_sync_enabled, + vkvalidation_gpu_enabled, vkcrash_diagnostic_enabled, + vkhost_markers, vkguest_markers, pipeline_cache_enabled, + pipeline_cache_archived) + +// ------------------------------- +// Main manager +// ------------------------------- +class EmulatorSettingsImpl { +public: + EmulatorSettingsImpl(); + ~EmulatorSettingsImpl(); + + static std::shared_ptr GetInstance(); + static void SetInstance(std::shared_ptr instance); + + bool Save(const std::string& serial = ""); + bool Load(const std::string& serial = ""); + void SetDefaultValues(); + bool TransferSettings(); + + // Config mode + ConfigMode GetConfigMode() const { + return m_configMode; + } + void SetConfigMode(ConfigMode mode) { + m_configMode = mode; + } + + // + // Game-specific override management + /// Clears all per-game overrides. Call this when a game exits so + /// the emulator reverts to global settings. + void ClearGameSpecificOverrides(); + + /// Reset a single field's game-specific override by its JSON ke + void ResetGameSpecificValue(const std::string& key); + + // general accessors + bool AddGameInstallDir(const std::filesystem::path& dir, bool enabled = true); + std::vector GetGameInstallDirs() const; + void SetAllGameInstallDirs(const std::vector& dirs); + void RemoveGameInstallDir(const std::filesystem::path& dir); + void SetGameInstallDirEnabled(const std::filesystem::path& dir, bool enabled); + void SetGameInstallDirs(const std::vector& dirs_config); + const std::vector GetGameInstallDirsEnabled(); + const std::vector& GetAllGameInstallDirs() const; + + std::filesystem::path GetHomeDir(); + void SetHomeDir(const std::filesystem::path& dir); + std::filesystem::path GetSysModulesDir(); + void SetSysModulesDir(const std::filesystem::path& dir); + std::filesystem::path GetFontsDir(); + void SetFontsDir(const std::filesystem::path& dir); + +private: + GeneralSettings m_general{}; + DebugSettings m_debug{}; + InputSettings m_input{}; + AudioSettings m_audio{}; + GPUSettings m_gpu{}; + VulkanSettings m_vulkan{}; + ConfigMode m_configMode{ConfigMode::Default}; + + bool m_loaded{false}; + + static std::shared_ptr s_instance; + static std::mutex s_mutex; + + /// Apply overrideable fields from groupJson into group.game_specific_value. + template + void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson, + std::vector& changed) { + for (auto& item : group.GetOverrideableFields()) { + if (!groupJson.contains(item.key)) + continue; + item.apply(&group, groupJson.at(item.key), changed); + } + } + + // Write all overrideable fields from group into out (for game-specific save). + template + static void SaveGroupGameSpecific(const Group& group, nlohmann::json& out) { + for (auto& item : group.GetOverrideableFields()) + out[item.key] = item.get_for_save(&group); + } + + // Discard every game-specific override in group. + template + static void ClearGroupOverrides(Group& group) { + for (auto& item : group.GetOverrideableFields()) + item.reset_game_specific(&group); + } + + static void PrintChangedSummary(const std::vector& changed); + +public: + // Add these getters to access overrideable fields + std::vector GetGeneralOverrideableFields() const { + return m_general.GetOverrideableFields(); + } + std::vector GetDebugOverrideableFields() const { + return m_debug.GetOverrideableFields(); + } + std::vector GetInputOverrideableFields() const { + return m_input.GetOverrideableFields(); + } + std::vector GetAudioOverrideableFields() const { + return m_audio.GetOverrideableFields(); + } + std::vector GetGPUOverrideableFields() const { + return m_gpu.GetOverrideableFields(); + } + std::vector GetVulkanOverrideableFields() const { + return m_vulkan.GetOverrideableFields(); + } + std::vector GetAllOverrideableKeys() const; + +#define SETTING_FORWARD(group, Name, field) \ + auto Get##Name() const { \ + return (group).field.get(m_configMode); \ + } \ + void Set##Name(const decltype((group).field.value)& v) { \ + (group).field.value = v; \ + } +#define SETTING_FORWARD_BOOL(group, Name, field) \ + bool Is##Name() const { \ + return (group).field.get(m_configMode); \ + } \ + void Set##Name(bool v) { \ + (group).field.value = v; \ + } +#define SETTING_FORWARD_BOOL_READONLY(group, Name, field) \ + bool Is##Name() const { \ + return (group).field.get(m_configMode); \ + } + + // General settings + SETTING_FORWARD(m_general, VolumeSlider, volume_slider) + SETTING_FORWARD_BOOL(m_general, Neo, neo_mode) + SETTING_FORWARD_BOOL(m_general, DevKit, dev_kit_mode) + SETTING_FORWARD(m_general, ExtraDmemInMBytes, extra_dmem_in_mbytes) + SETTING_FORWARD_BOOL(m_general, PSNSignedIn, psn_signed_in) + SETTING_FORWARD_BOOL(m_general, TrophyPopupDisabled, trophy_popup_disabled) + SETTING_FORWARD(m_general, TrophyNotificationDuration, trophy_notification_duration) + SETTING_FORWARD(m_general, TrophyNotificationSide, trophy_notification_side) + SETTING_FORWARD_BOOL(m_general, ShowSplash, show_splash) + SETTING_FORWARD_BOOL(m_general, IdenticalLogGrouped, identical_log_grouped) + SETTING_FORWARD(m_general, AddonInstallDir, addon_install_dir) + SETTING_FORWARD(m_general, LogFilter, log_filter) + SETTING_FORWARD(m_general, LogType, log_type) + SETTING_FORWARD_BOOL(m_general, ConnectedToNetwork, connected_to_network) + SETTING_FORWARD_BOOL(m_general, DiscordRPCEnabled, discord_rpc_enabled) + SETTING_FORWARD_BOOL(m_general, ShowFpsCounter, show_fps_counter) + SETTING_FORWARD(m_general, ConsoleLanguage, console_language) + + // Audio settings + SETTING_FORWARD(m_audio, AudioBackend, audio_backend) + SETTING_FORWARD(m_audio, SDLMicDevice, sdl_mic_device) + SETTING_FORWARD(m_audio, SDLMainOutputDevice, sdl_main_output_device) + SETTING_FORWARD(m_audio, SDLPadSpkOutputDevice, sdl_padSpk_output_device) + SETTING_FORWARD(m_audio, OpenALMicDevice, openal_mic_device) + SETTING_FORWARD(m_audio, OpenALMainOutputDevice, openal_main_output_device) + SETTING_FORWARD(m_audio, OpenALPadSpkOutputDevice, openal_padSpk_output_device) + + // Debug settings + SETTING_FORWARD_BOOL(m_debug, SeparateLoggingEnabled, separate_logging_enabled) + SETTING_FORWARD_BOOL(m_debug, DebugDump, debug_dump) + SETTING_FORWARD_BOOL(m_debug, ShaderCollect, shader_collect) + SETTING_FORWARD_BOOL(m_debug, LogEnabled, log_enabled) + SETTING_FORWARD(m_debug, ConfigVersion, config_version) + + // GPU Settings + SETTING_FORWARD_BOOL(m_gpu, NullGPU, null_gpu) + SETTING_FORWARD_BOOL(m_gpu, DumpShaders, dump_shaders) + SETTING_FORWARD_BOOL(m_gpu, CopyGpuBuffers, copy_gpu_buffers) + SETTING_FORWARD_BOOL(m_gpu, FullScreen, full_screen) + SETTING_FORWARD(m_gpu, FullScreenMode, full_screen_mode) + SETTING_FORWARD(m_gpu, PresentMode, present_mode) + SETTING_FORWARD(m_gpu, WindowHeight, window_height) + SETTING_FORWARD(m_gpu, WindowWidth, window_width) + SETTING_FORWARD(m_gpu, InternalScreenHeight, internal_screen_height) + SETTING_FORWARD(m_gpu, InternalScreenWidth, internal_screen_width) + SETTING_FORWARD_BOOL(m_gpu, HdrAllowed, hdr_allowed) + SETTING_FORWARD_BOOL(m_gpu, FsrEnabled, fsr_enabled) + SETTING_FORWARD_BOOL(m_gpu, RcasEnabled, rcas_enabled) + SETTING_FORWARD(m_gpu, RcasAttenuation, rcas_attenuation) + SETTING_FORWARD(m_gpu, ReadbacksMode, readbacks_mode) + SETTING_FORWARD_BOOL(m_gpu, ReadbackLinearImagesEnabled, readback_linear_images_enabled) + SETTING_FORWARD_BOOL(m_gpu, DirectMemoryAccessEnabled, direct_memory_access_enabled) + SETTING_FORWARD_BOOL_READONLY(m_gpu, PatchShaders, patch_shaders) + + u32 GetVblankFrequency() { + if (m_gpu.vblank_frequency.value < 30) { + return 30; + } + return m_gpu.vblank_frequency.get(); + } + void SetVblankFrequency(const u32& v, bool is_specific = false) { + u32 val = v < 30 ? 30 : v; + if (is_specific) { + m_gpu.vblank_frequency.game_specific_value = val; + } else { + m_gpu.vblank_frequency.value = val; + } + } + + // Input Settings + SETTING_FORWARD(m_input, CursorState, cursor_state) + SETTING_FORWARD(m_input, CursorHideTimeout, cursor_hide_timeout) + SETTING_FORWARD(m_input, UsbDeviceBackend, usb_device_backend) + SETTING_FORWARD_BOOL(m_input, MotionControlsEnabled, motion_controls_enabled) + SETTING_FORWARD_BOOL(m_input, BackgroundControllerInput, background_controller_input) + SETTING_FORWARD(m_input, DefaultControllerId, default_controller_id) + SETTING_FORWARD_BOOL(m_input, UsingSpecialPad, use_special_pad) + SETTING_FORWARD(m_input, SpecialPadClass, special_pad_class) + SETTING_FORWARD_BOOL(m_input, UseUnifiedInputConfig, use_unified_input_config) + SETTING_FORWARD(m_input, CameraId, camera_id) + + // Vulkan settings + SETTING_FORWARD(m_vulkan, GpuId, gpu_id) + SETTING_FORWARD_BOOL(m_vulkan, RenderdocEnabled, renderdoc_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkValidationEnabled, vkvalidation_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkValidationCoreEnabled, vkvalidation_core_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkValidationSyncEnabled, vkvalidation_sync_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkValidationGpuEnabled, vkvalidation_gpu_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkCrashDiagnosticEnabled, vkcrash_diagnostic_enabled) + SETTING_FORWARD_BOOL(m_vulkan, VkHostMarkersEnabled, vkhost_markers) + SETTING_FORWARD_BOOL(m_vulkan, VkGuestMarkersEnabled, vkguest_markers) + SETTING_FORWARD_BOOL(m_vulkan, PipelineCacheEnabled, pipeline_cache_enabled) + SETTING_FORWARD_BOOL(m_vulkan, PipelineCacheArchived, pipeline_cache_archived) + +#undef SETTING_FORWARD +#undef SETTING_FORWARD_BOOL +#undef SETTING_FORWARD_BOOL_READONLY +}; diff --git a/src/core/file_format/psf.cpp b/src/core/file_format/psf.cpp index e647059f0..c5be7410a 100644 --- a/src/core/file_format/psf.cpp +++ b/src/core/file_format/psf.cpp @@ -113,6 +113,7 @@ bool PSF::Encode(const std::filesystem::path& filepath) const { LOG_ERROR(Core, "Failed to write PSF file. Written {} Expected {}", written, psf_buffer.size()); } + file.Close(); return written == psf_buffer.size(); } diff --git a/src/core/file_format/trp.cpp b/src/core/file_format/trp.cpp index f0a258c12..6269fc6c7 100644 --- a/src/core/file_format/trp.cpp +++ b/src/core/file_format/trp.cpp @@ -5,7 +5,6 @@ #include "common/key_manager.h" #include "common/logging/log.h" #include "common/path_util.h" -#include "core/file_format/npbind.h" #include "core/file_format/trp.h" static void DecryptEFSM(std::span trophyKey, std::span NPcommID, @@ -43,8 +42,10 @@ static void hexToBytes(const char* hex, unsigned char* dst) { } } -bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string titleId) { - std::filesystem::path gameSysDir = trophyPath / "sce_sys/trophy/"; +bool TRP::Extract(const std::filesystem::path& trophyPath, int index, std::string npCommId, + const std::filesystem::path& outputPath) { + std::filesystem::path gameSysDir = + trophyPath / "sce_sys/trophy/" / std::format("trophy{:02d}.trp", index); if (!std::filesystem::exists(gameSysDir)) { LOG_WARNING(Common_Filesystem, "Game trophy directory doesn't exist"); return false; @@ -61,117 +62,82 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit std::array user_key{}; std::copy(user_key_vec.begin(), user_key_vec.end(), user_key.begin()); - // Load npbind.dat using the new class - std::filesystem::path npbindPath = trophyPath / "sce_sys/npbind.dat"; - NPBindFile npbind; - if (!npbind.Load(npbindPath.string())) { - LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file"); - } - - auto npCommIds = npbind.GetNpCommIds(); - if (npCommIds.empty()) { - LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat"); - } - bool success = true; int trpFileIndex = 0; try { - // Process each TRP file in the trophy directory - for (const auto& it : std::filesystem::directory_iterator(gameSysDir)) { - if (!it.is_regular_file() || it.path().extension() != ".trp") { - continue; // Skip non-TRP files - } + const auto& it = gameSysDir; + if (it.extension() != ".trp") { + return false; + } + Common::FS::IOFile file(it, Common::FS::FileAccessMode::Read); + if (!file.IsOpen()) { + LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.string()); + return false; + } - // Get NPCommID for this TRP file (if available) - std::string npCommId; - if (trpFileIndex < static_cast(npCommIds.size())) { - npCommId = npCommIds[trpFileIndex]; - LOG_DEBUG(Common_Filesystem, "Using NPCommID: {} for {}", npCommId, - it.path().filename().string()); - } else { - LOG_WARNING(Common_Filesystem, "No NPCommID found for TRP file index {}", - trpFileIndex); - } + TrpHeader header; + if (!file.Read(header)) { + LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}", it.string()); + return false; + } - Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read); - if (!file.IsOpen()) { - LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.path().string()); + if (header.magic != TRP_MAGIC) { + LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.string()); + return false; + } + + s64 seekPos = sizeof(TrpHeader); + // Create output directories + if (!std::filesystem::create_directories(outputPath / "Icons") || + !std::filesystem::create_directories(outputPath / "Xml")) { + LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", npCommId); + return false; + } + + // Process each entry in the TRP file + for (int i = 0; i < header.entry_num; i++) { + if (!file.Seek(seekPos)) { + LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset"); success = false; - continue; + break; } + seekPos += static_cast(header.entry_size); - TrpHeader header; - if (!file.Read(header)) { - LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}", - it.path().string()); + TrpEntry entry; + if (!file.Read(entry)) { + LOG_ERROR(Common_Filesystem, "Failed to read TRP entry"); success = false; - continue; + break; } - if (header.magic != TRP_MAGIC) { - LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.path().string()); - success = false; - continue; - } + std::string_view name(entry.entry_name); - s64 seekPos = sizeof(TrpHeader); - std::filesystem::path trpFilesPath( - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId / - "TrophyFiles" / it.path().stem()); - - // Create output directories - if (!std::filesystem::create_directories(trpFilesPath / "Icons") || - !std::filesystem::create_directories(trpFilesPath / "Xml")) { - LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", titleId); - success = false; - continue; - } - - // Process each entry in the TRP file - for (int i = 0; i < header.entry_num; i++) { - if (!file.Seek(seekPos)) { - LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset"); + if (entry.flag == ENTRY_FLAG_PNG) { + if (!ProcessPngEntry(file, entry, outputPath, name)) { success = false; - break; + // Continue with next entry } - seekPos += static_cast(header.entry_size); - - TrpEntry entry; - if (!file.Read(entry)) { - LOG_ERROR(Common_Filesystem, "Failed to read TRP entry"); - success = false; - break; - } - - std::string_view name(entry.entry_name); - - if (entry.flag == ENTRY_FLAG_PNG) { - if (!ProcessPngEntry(file, entry, trpFilesPath, name)) { + } else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) { + // Check if we have a valid NPCommID for decryption + if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') { + if (!ProcessEncryptedXmlEntry(file, entry, outputPath, name, user_key, + npCommId)) { success = false; // Continue with next entry } - } else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) { - // Check if we have a valid NPCommID for decryption - if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') { - if (!ProcessEncryptedXmlEntry(file, entry, trpFilesPath, name, user_key, - npCommId)) { - success = false; - // Continue with next entry - } - } else { - LOG_WARNING(Common_Filesystem, - "Skipping encrypted XML entry - invalid NPCommID"); - // Skip this entry but continue - } } else { - LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}", - static_cast(entry.flag), name); + LOG_WARNING(Common_Filesystem, + "Skipping encrypted XML entry - invalid NPCommID"); + // Skip this entry but continue } + } else { + LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}", + static_cast(entry.flag), name); } - trpFileIndex++; } + } catch (const std::filesystem::filesystem_error& e) { LOG_CRITICAL(Common_Filesystem, "Filesystem error during trophy extraction: {}", e.what()); return false; @@ -182,7 +148,7 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit if (success) { LOG_INFO(Common_Filesystem, "Successfully extracted {} trophy files for {}", trpFileIndex, - titleId); + npCommId); } return success; diff --git a/src/core/file_format/trp.h b/src/core/file_format/trp.h index 2b52a4d57..df0ea6eaf 100644 --- a/src/core/file_format/trp.h +++ b/src/core/file_format/trp.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -36,7 +36,8 @@ class TRP { public: TRP(); ~TRP(); - bool Extract(const std::filesystem::path& trophyPath, const std::string titleId); + bool Extract(const std::filesystem::path& trophyPath, int index, std::string npCommId, + const std::filesystem::path& outputPath); private: bool ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry, @@ -45,9 +46,6 @@ private: const std::filesystem::path& outputPath, std::string_view name, const std::array& user_key, const std::string& npCommId); - std::vector NPcommID = std::vector(12); - std::array np_comm_id{}; std::array esfmIv{}; - std::filesystem::path trpFilesPath; static constexpr int iv_len = 16; }; diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index 96a04ee5e..2fdf8c10b 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/string_util.h" #include "core/file_sys/devices/logger.h" #include "core/file_sys/devices/nop_device.h" diff --git a/src/core/ipc/ipc.cpp b/src/core/ipc/ipc.cpp index aab3e7de5..70180d3bf 100644 --- a/src/core/ipc/ipc.cpp +++ b/src/core/ipc/ipc.cpp @@ -8,12 +8,12 @@ #include -#include "common/config.h" #include "common/memory_patcher.h" #include "common/thread.h" #include "common/types.h" #include "core/debug_state.h" #include "core/debugger.h" +#include "core/emulator_settings.h" #include "core/emulator_state.h" #include "core/libraries/audio/audioout.h" #include "input/input_handler.h" @@ -153,7 +153,7 @@ void IPC::InputLoop() { } else if (cmd == "ADJUST_VOLUME") { int value = static_cast(next_u64()); bool is_game_specific = next_u64() != 0; - Config::setVolumeSlider(value, is_game_specific); + EmulatorSettings.SetVolumeSlider(value); Libraries::AudioOut::AdjustVol(); } else if (cmd == "SET_FSR") { bool use_fsr = next_u64() != 0; @@ -211,13 +211,6 @@ void IPC::InputLoop() { } else if (cmd == "RELOAD_INPUTS") { std::string config = next_str(); Input::ParseInputConfig(config); - } else if (cmd == "SET_ACTIVE_CONTROLLER") { - std::string active_controller = next_str(); - GamepadSelect::SetSelectedGamepad(active_controller); - SDL_Event checkGamepad; - SDL_memset(&checkGamepad, 0, sizeof(checkGamepad)); - checkGamepad.type = SDL_EVENT_CHANGE_CONTROLLER; - SDL_PushEvent(&checkGamepad); } else { std::cerr << ";UNKNOWN CMD: " << cmd << std::endl; } diff --git a/src/core/libraries/app_content/app_content.cpp b/src/core/libraries/app_content/app_content.cpp index a5952c7ea..bf2b72b07 100644 --- a/src/core/libraries/app_content/app_content.cpp +++ b/src/core/libraries/app_content/app_content.cpp @@ -5,9 +5,9 @@ #include "app_content.h" #include "common/assert.h" -#include "common/config.h" #include "common/logging/log.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/file_format/psf.h" #include "core/file_sys/fs.h" #include "core/libraries/app_content/app_content_error.h" @@ -57,7 +57,7 @@ int PS4_SYSV_ABI sceAppContentAddcontMount(u32 service_label, OrbisAppContentMountPoint* mount_point) { LOG_INFO(Lib_AppContent, "called"); - const auto& addon_path = Config::getAddonInstallDir() / title_id; + const auto& addon_path = EmulatorSettings.GetAddonInstallDir() / title_id; auto* mnt = Common::Singleton::Instance(); // Determine which loaded additional content this entitlement label is for. @@ -282,7 +282,7 @@ int PS4_SYSV_ABI sceAppContentInitialize(const OrbisAppContentInitParam* initPar LOG_ERROR(Lib_AppContent, "(DUMMY) called"); auto* param_sfo = Common::Singleton::Instance(); - const auto addons_dir = Config::getAddonInstallDir(); + const auto addons_dir = EmulatorSettings.GetAddonInstallDir(); if (const auto value = param_sfo->GetString("TITLE_ID"); value.has_value()) { title_id = *value; } else { diff --git a/src/core/libraries/audio/audioin.h b/src/core/libraries/audio/audioin.h index 0eda2013e..be43315e0 100644 --- a/src/core/libraries/audio/audioin.h +++ b/src/core/libraries/audio/audioin.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include "common/types.h" diff --git a/src/core/libraries/audio/audioout.cpp b/src/core/libraries/audio/audioout.cpp index 100ddd51c..e009f1f39 100644 --- a/src/core/libraries/audio/audioout.cpp +++ b/src/core/libraries/audio/audioout.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 @@ -6,10 +6,9 @@ #include #include #include +#include #include - #include "common/assert.h" -#include "common/config.h" #include "common/logging/log.h" #include "common/thread.h" #include "core/libraries/audio/audioout.h" @@ -206,7 +205,11 @@ s32 PS4_SYSV_ABI sceAudioOutInit() { return ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT; } - audio = std::make_unique(); + if (EmulatorSettings.GetAudioBackend() == AudioBackend::OpenAL) { + audio = std::make_unique(); + } else { + audio = std::make_unique(); + } LOG_INFO(Lib_AudioOut, "Audio system initialized"); return ORBIS_OK; diff --git a/src/core/libraries/audio/audioout_backend.h b/src/core/libraries/audio/audioout_backend.h index 0f36f19c8..e71abfefb 100644 --- a/src/core/libraries/audio/audioout_backend.h +++ b/src/core/libraries/audio/audioout_backend.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -31,4 +31,9 @@ public: std::unique_ptr Open(PortOut& port) override; }; +class OpenALAudioOut final : public AudioOutBackend { +public: + std::unique_ptr Open(PortOut& port) override; +}; + } // namespace Libraries::AudioOut diff --git a/src/core/libraries/audio/openal_audio_out.cpp b/src/core/libraries/audio/openal_audio_out.cpp new file mode 100644 index 000000000..d40f01588 --- /dev/null +++ b/src/core/libraries/audio/openal_audio_out.cpp @@ -0,0 +1,832 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "core/emulator_settings.h" +#include "core/libraries/audio/audioout.h" +#include "core/libraries/audio/audioout_backend.h" +#include "core/libraries/audio/openal_manager.h" +#include "core/libraries/kernel/threads.h" + +// SIMD support detection +#if defined(__x86_64__) || defined(_M_X64) +#include +#define HAS_SSE2 +#endif + +namespace Libraries::AudioOut { + +// Volume constants +constexpr float VOLUME_0DB = 32768.0f; // 1 << 15 +constexpr float INV_VOLUME_0DB = 1.0f / VOLUME_0DB; +constexpr float VOLUME_EPSILON = 0.001f; +// Timing constants +constexpr u64 VOLUME_CHECK_INTERVAL_US = 50000; // Check every 50ms +constexpr u64 MIN_SLEEP_THRESHOLD_US = 10; +constexpr u64 TIMING_RESYNC_THRESHOLD_US = 100000; // Resync if >100ms behind + +// OpenAL constants +constexpr ALsizei NUM_BUFFERS = 6; +constexpr ALsizei BUFFER_QUEUE_THRESHOLD = 2; // Queue more buffers when below this + +// Channel positions +enum ChannelPos : u8 { + FL = 0, + FR = 1, + FC = 2, + LF = 3, + SL = 4, + SR = 5, + BL = 6, + BR = 7, + STD_SL = 6, + STD_SR = 7, + STD_BL = 4, + STD_BR = 5 +}; + +class OpenALPortBackend : public PortBackend { +public: + explicit OpenALPortBackend(const PortOut& port) + : frame_size(port.format_info.FrameSize()), guest_buffer_size(port.BufferSize()), + buffer_frames(port.buffer_frames), sample_rate(port.sample_rate), + num_channels(port.format_info.num_channels), is_float(port.format_info.is_float), + is_std(port.format_info.is_std), channel_layout(port.format_info.channel_layout), + device_registered(false), device_name(GetDeviceName(port.type)) { + + if (!Initialize(port.type)) { + LOG_ERROR(Lib_AudioOut, "Failed to initialize OpenAL audio backend"); + } + } + + ~OpenALPortBackend() override { + // Unregister port before cleanup + if (device_registered) { + OpenALDevice::GetInstance().UnregisterPort(device_name); + } + Cleanup(); + } + + void Output(void* ptr) override { + if (!source || !convert) [[unlikely]] { + return; + } + if (ptr == nullptr) [[unlikely]] { + return; + } + if (!device_context->MakeCurrent(device_name)) { + return; + } + + UpdateVolumeIfChanged(); + const u64 current_time = Kernel::sceKernelGetProcessTime(); + + // Convert audio data ONCE per call + if (use_native_float) { + convert(ptr, al_buffer_float.data(), buffer_frames, nullptr); + } else { + convert(ptr, al_buffer_s16.data(), buffer_frames, nullptr); + } + + // Reclaim processed buffers + ALint processed = 0; + alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); + + while (processed > 0) { + ALuint buffer_id; + alSourceUnqueueBuffers(source, 1, &buffer_id); + if (alGetError() == AL_NO_ERROR) { + available_buffers.push_back(buffer_id); + processed--; + } else { + break; + } + } + + // Queue buffer + if (!available_buffers.empty()) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + + if (use_native_float) { + alBufferData(buffer_id, format, al_buffer_float.data(), buffer_size_bytes, + sample_rate); + } else { + alBufferData(buffer_id, format, al_buffer_s16.data(), buffer_size_bytes, + sample_rate); + } + alSourceQueueBuffers(source, 1, &buffer_id); + } + + // Check state and queue health + ALint state = 0; + ALint queued = 0; + alGetSourcei(source, AL_SOURCE_STATE, &state); + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + + if (state != AL_PLAYING && queued > 0) { + LOG_DEBUG(Lib_AudioOut, "Audio underrun detected (queued: {}), restarting source", + queued); + alSourcePlay(source); + } + + // Only sleep if we have healthy buffer queue + if (queued >= 2) { + HandleTiming(current_time); + } else { + next_output_time = current_time + period_us; + } + + last_output_time.store(current_time, std::memory_order_release); + output_count++; + } + void SetVolume(const std::array& ch_volumes) override { + if (!device_context->MakeCurrent(device_name)) { + return; + } + + if (!source) [[unlikely]] { + return; + } + + float max_channel_gain = 0.0f; + const u32 channels_to_check = std::min(num_channels, 8u); + + for (u32 i = 0; i < channels_to_check; i++) { + const float channel_gain = static_cast(ch_volumes[i]) * INV_VOLUME_0DB; + max_channel_gain = std::max(max_channel_gain, channel_gain); + } + + const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; + const float total_gain = max_channel_gain * slider_gain; + + const float current = current_gain.load(std::memory_order_acquire); + if (std::abs(total_gain - current) < VOLUME_EPSILON) { + return; + } + + alSourcef(source, AL_GAIN, total_gain); + + ALenum error = alGetError(); + if (error == AL_NO_ERROR) { + current_gain.store(total_gain, std::memory_order_release); + LOG_DEBUG(Lib_AudioOut, + "Set combined audio gain to {:.3f} (channel: {:.3f}, slider: {:.3f})", + total_gain, max_channel_gain, slider_gain); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to set OpenAL source gain: {}", + GetALErrorString(error)); + } + } + + u64 GetLastOutputTime() const { + return last_output_time.load(std::memory_order_acquire); + } + +private: + bool Initialize(OrbisAudioOutPort type) { + // Register this port with the device manager + if (!OpenALDevice::GetInstance().RegisterPort(device_name)) { + if (device_name == "None") { + LOG_INFO(Lib_AudioOut, "Audio device disabled for port type {}", + static_cast(type)); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to register OpenAL device '{}'", device_name); + } + return false; + } + + device_registered = true; + device_context = &OpenALDevice::GetInstance(); + + // Make this device's context current + if (!device_context->MakeCurrent(device_name)) { + LOG_ERROR(Lib_AudioOut, "Failed to make OpenAL context current for device '{}'", + device_name); + return false; + } + + // Log device info + LOG_INFO(Lib_AudioOut, "Using OpenAL device for port type {}: '{}'", static_cast(type), + device_name); + + // Calculate timing parameters + period_us = (1000000ULL * buffer_frames + sample_rate / 2) / sample_rate; + + // Check for AL_EXT_FLOAT32 extension + has_float_ext = alIsExtensionPresent("AL_EXT_FLOAT32"); + if (has_float_ext && is_float) { + LOG_INFO(Lib_AudioOut, "AL_EXT_FLOAT32 extension detected - using native float format"); + } + + // Determine OpenAL format + if (!DetermineOpenALFormat()) { + LOG_ERROR(Lib_AudioOut, "Unsupported audio format for OpenAL"); + return false; + } + + // Allocate buffers based on format + if (use_native_float) { + al_buffer_float.resize(buffer_frames * num_channels); + buffer_size_bytes = buffer_frames * num_channels * sizeof(float); + } else { + al_buffer_s16.resize(buffer_frames * num_channels); + buffer_size_bytes = buffer_frames * num_channels * sizeof(s16); + } + + // Select optimal converter function + if (!SelectConverter()) { + return false; + } + + // Generate OpenAL source and buffers + if (!CreateOpenALObjects()) { + return false; + } + + // Initialize current gain + current_gain.store(EmulatorSettings.GetVolumeSlider() * 0.01f, std::memory_order_relaxed); + alSourcef(source, AL_GAIN, current_gain.load(std::memory_order_relaxed)); + + // Prime buffers with silence + if (use_native_float) { + std::vector silence(buffer_frames * num_channels, 0.0f); + for (size_t i = 0; i < buffers.size() - 1; i++) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + alBufferData(buffer_id, format, silence.data(), buffer_size_bytes, sample_rate); + alSourceQueueBuffers(source, 1, &buffer_id); + } + } else { + std::vector silence(buffer_frames * num_channels, 0); + for (size_t i = 0; i < buffers.size() - 1; i++) { + ALuint buffer_id = available_buffers.back(); + available_buffers.pop_back(); + alBufferData(buffer_id, format, silence.data(), buffer_size_bytes, sample_rate); + alSourceQueueBuffers(source, 1, &buffer_id); + } + } + + alSourcePlay(source); + + LOG_INFO(Lib_AudioOut, + "Initialized OpenAL backend ({} Hz, {} ch, {} format, {}) for device '{}'", + sample_rate, num_channels, is_float ? "float" : "int16", + use_native_float ? "native" : "converted", device_name); + return true; + } + + void Cleanup() { + if (!device_context || !device_context->MakeCurrent(device_name)) { + return; + } + + if (source) { + alSourceStop(source); + + ALint queued = 0; + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + while (queued-- > 0) { + ALuint buf; + alSourceUnqueueBuffers(source, 1, &buf); + } + + alDeleteSources(1, &source); + source = 0; + } + + if (!buffers.empty()) { + alDeleteBuffers(static_cast(buffers.size()), buffers.data()); + buffers.clear(); + } + } + + std::string GetDeviceName(OrbisAudioOutPort type) const { + switch (type) { + case OrbisAudioOutPort::Main: + case OrbisAudioOutPort::Bgm: + return EmulatorSettings.GetOpenALMainOutputDevice(); + case OrbisAudioOutPort::PadSpk: + return EmulatorSettings.GetOpenALPadSpkOutputDevice(); + default: + return EmulatorSettings.GetOpenALMainOutputDevice(); + } + } + + void UpdateVolumeIfChanged() { + const u64 current_time = Kernel::sceKernelGetProcessTime(); + + if (current_time - last_volume_check_time < VOLUME_CHECK_INTERVAL_US) { + return; + } + + last_volume_check_time = current_time; + + const float config_volume = EmulatorSettings.GetVolumeSlider() * 0.01f; + const float stored_gain = current_gain.load(std::memory_order_acquire); + + if (std::abs(config_volume - stored_gain) > VOLUME_EPSILON) { + alSourcef(source, AL_GAIN, config_volume); + + ALenum error = alGetError(); + if (error == AL_NO_ERROR) { + current_gain.store(config_volume, std::memory_order_release); + LOG_DEBUG(Lib_AudioOut, "Updated audio gain to {:.3f}", config_volume); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to set audio gain: {}", GetALErrorString(error)); + } + } + } + + void HandleTiming(u64 current_time) { + if (next_output_time == 0) [[unlikely]] { + next_output_time = current_time + period_us; + return; + } + + const s64 time_diff = static_cast(current_time - next_output_time); + + if (time_diff > static_cast(TIMING_RESYNC_THRESHOLD_US)) [[unlikely]] { + next_output_time = current_time + period_us; + } else if (time_diff < 0) { + const u64 time_to_wait = static_cast(-time_diff); + next_output_time += period_us; + + if (time_to_wait > MIN_SLEEP_THRESHOLD_US) { + const u64 sleep_duration = time_to_wait - MIN_SLEEP_THRESHOLD_US; + std::this_thread::sleep_for(std::chrono::microseconds(sleep_duration)); + } + } else { + next_output_time += period_us; + } + } + + bool DetermineOpenALFormat() { + // Try to use native float formats if extension is available + if (is_float && has_float_ext) { + switch (num_channels) { + case 1: + format = AL_FORMAT_MONO_FLOAT32; + use_native_float = true; + return true; + case 2: + format = AL_FORMAT_STEREO_FLOAT32; + use_native_float = true; + return true; + case 4: + format = alGetEnumValue("AL_FORMAT_QUAD32"); + if (format != 0 && alGetError() == AL_NO_ERROR) { + use_native_float = true; + return true; + } + break; + case 6: + format = alGetEnumValue("AL_FORMAT_51CHN32"); + if (format != 0 && alGetError() == AL_NO_ERROR) { + use_native_float = true; + return true; + } + break; + case 8: + format = alGetEnumValue("AL_FORMAT_71CHN32"); + if (format != 0 && alGetError() == AL_NO_ERROR) { + use_native_float = true; + return true; + } + break; + } + + LOG_WARNING( + Lib_AudioOut, + "Float format for {} channels not supported, falling back to S16 conversion", + num_channels); + } + + // Fall back to S16 formats (with conversion if needed) + use_native_float = false; + + if (is_float) { + // Will need to convert float to S16 + format = AL_FORMAT_MONO16; + + switch (num_channels) { + case 1: + format = AL_FORMAT_MONO16; + break; + case 2: + format = AL_FORMAT_STEREO16; + break; + case 6: + format = alGetEnumValue("AL_FORMAT_51CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + case 8: + format = alGetEnumValue("AL_FORMAT_71CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else { + // Native 16-bit integer formats + switch (num_channels) { + case 1: + format = AL_FORMAT_MONO16; + break; + case 2: + format = AL_FORMAT_STEREO16; + break; + case 6: + format = alGetEnumValue("AL_FORMAT_51CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "5.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + case 8: + format = alGetEnumValue("AL_FORMAT_71CHN16"); + if (format == 0 || alGetError() != AL_NO_ERROR) { + LOG_WARNING(Lib_AudioOut, "7.1 format not supported, falling back to stereo"); + format = AL_FORMAT_STEREO16; + } + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels); + return false; + } + } + + return true; + } + + bool CreateOpenALObjects() { + alGenSources(1, &source); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_AudioOut, "Failed to generate OpenAL source"); + return false; + } + + buffers.resize(NUM_BUFFERS); + alGenBuffers(static_cast(buffers.size()), buffers.data()); + if (alGetError() != AL_NO_ERROR) { + LOG_ERROR(Lib_AudioOut, "Failed to generate OpenAL buffers"); + alDeleteSources(1, &source); + source = 0; + return false; + } + + available_buffers = buffers; + + alSourcef(source, AL_PITCH, 1.0f); + alSourcef(source, AL_GAIN, 1.0f); + alSource3f(source, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + alSourcei(source, AL_LOOPING, AL_FALSE); + alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE); + + LOG_DEBUG(Lib_AudioOut, "Created OpenAL source {} with {} buffers", source, buffers.size()); + return true; + } + + bool SelectConverter() { + if (is_float && use_native_float) { + // Native float - just copy/remap if needed + switch (num_channels) { + case 1: + convert = &ConvertF32Mono; + break; + case 2: + convert = &ConvertF32Stereo; + break; + case 8: + convert = is_std ? &ConvertF32Std8CH : &ConvertF32_8CH; + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else if (is_float && !use_native_float) { + // Float to S16 conversion needed + switch (num_channels) { + case 1: + convert = &ConvertF32ToS16Mono; + break; + case 2: +#ifdef HAS_SSE2 + convert = &ConvertF32ToS16StereoSIMD; +#else + convert = &ConvertF32ToS16Stereo; +#endif + break; + case 8: +#ifdef HAS_SSE2 + convert = is_std ? &ConvertF32ToS16Std8CH : &ConvertF32ToS16_8CH_SIMD; +#else + convert = is_std ? &ConvertF32ToS16Std8CH : &ConvertF32ToS16_8CH; +#endif + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + return false; + } + } else { + // S16 native - just copy + switch (num_channels) { + case 1: + convert = &ConvertS16Mono; + break; + case 2: + convert = &ConvertS16Stereo; + break; + case 8: + convert = &ConvertS16_8CH; + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels); + return false; + } + } + + return true; + } + + const char* GetALErrorString(ALenum error) { + switch (error) { + case AL_NO_ERROR: + return "AL_NO_ERROR"; + case AL_INVALID_NAME: + return "AL_INVALID_NAME"; + case AL_INVALID_ENUM: + return "AL_INVALID_ENUM"; + case AL_INVALID_VALUE: + return "AL_INVALID_VALUE"; + case AL_INVALID_OPERATION: + return "AL_INVALID_OPERATION"; + case AL_OUT_OF_MEMORY: + return "AL_OUT_OF_MEMORY"; + default: + return "Unknown AL error"; + } + } + + // Converter function type + using ConverterFunc = void (*)(const void* src, void* dst, u32 frames, const float* volumes); + + static inline s16 OrbisFloatToS16(float v) { + if (std::abs(v) < 1.0e-20f) + v = 0.0f; + + // Sony behavior: +1.0f -> 32767, -1.0f -> -32768 + const float scaled = v * 32768.0f; + + if (scaled >= 32767.0f) + return 32767; + if (scaled <= -32768.0f) + return -32768; + + return static_cast(scaled + (scaled >= 0 ? 0.5f : -0.5f)); + } + static void ConvertS16Mono(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + std::memcpy(d, s, frames * sizeof(s16)); + } + + static void ConvertS16Stereo(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 1; + std::memcpy(d, s, num_samples * sizeof(s16)); + } + + static void ConvertS16_8CH(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 3; + std::memcpy(d, s, num_samples * sizeof(s16)); + } + + // Float passthrough converters (for AL_EXT_FLOAT32) + static void ConvertF32Mono(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + std::memcpy(d, s, frames * sizeof(float)); + } + + static void ConvertF32Stereo(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + std::memcpy(d, s, frames * 2 * sizeof(float)); + } + + static void ConvertF32_8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + std::memcpy(d, s, frames * 8 * sizeof(float)); + } + + static void ConvertF32Std8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) { + const u32 offset = i << 3; + d[offset + FL] = s[offset + FL]; + d[offset + FR] = s[offset + FR]; + d[offset + FC] = s[offset + FC]; + d[offset + LF] = s[offset + LF]; + d[offset + SL] = s[offset + STD_SL]; + d[offset + SR] = s[offset + STD_SR]; + d[offset + BL] = s[offset + STD_BL]; + d[offset + BR] = s[offset + STD_BR]; + } + } + + // Float to S16 converters for OpenAL + static void ConvertF32ToS16Mono(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) + d[i] = OrbisFloatToS16(s[i]); + } +#ifdef HAS_SSE2 + static void ConvertF32ToS16StereoSIMD(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(32768.0f); + const __m128 min_val = _mm_set1_ps(-32768.0f); + const __m128 max_val = _mm_set1_ps(32767.0f); + + const u32 num_samples = frames << 1; + u32 i = 0; + + // Process 8 samples at a time + for (; i + 8 <= num_samples; i += 8) { + // Load 8 floats + __m128 f1 = _mm_loadu_ps(&s[i]); + __m128 f2 = _mm_loadu_ps(&s[i + 4]); + + // Scale and clamp + f1 = _mm_mul_ps(f1, scale); + f2 = _mm_mul_ps(f2, scale); + f1 = _mm_max_ps(f1, min_val); + f2 = _mm_max_ps(f2, min_val); + f1 = _mm_min_ps(f1, max_val); + f2 = _mm_min_ps(f2, max_val); + + // Convert to int32 + __m128i i1 = _mm_cvtps_epi32(f1); + __m128i i2 = _mm_cvtps_epi32(f2); + + // Pack to int16 + __m128i packed = _mm_packs_epi32(i1, i2); + + // Store + _mm_storeu_si128(reinterpret_cast<__m128i*>(&d[i]), packed); + } + + // Handle remaining samples + for (; i < num_samples; i++) { + d[i] = OrbisFloatToS16(s[i]); + } + } +#elif + static void ConvertF32ToS16Stereo(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 1; + for (u32 i = 0; i < num_samples; i++) + d[i] = OrbisFloatToS16(s[i]); + } +#endif + +#ifdef HAS_SSE2 + static void ConvertF32ToS16_8CH_SIMD(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(32768.0f); + const __m128 min_val = _mm_set1_ps(-32768.0f); + const __m128 max_val = _mm_set1_ps(32767.0f); + + const u32 num_samples = frames << 3; + u32 i = 0; + + // Process 8 samples at a time (1 frame of 8CH audio) + for (; i + 8 <= num_samples; i += 8) { + __m128 f1 = _mm_loadu_ps(&s[i]); + __m128 f2 = _mm_loadu_ps(&s[i + 4]); + + f1 = _mm_mul_ps(f1, scale); + f2 = _mm_mul_ps(f2, scale); + f1 = _mm_max_ps(_mm_min_ps(f1, max_val), min_val); + f2 = _mm_max_ps(_mm_min_ps(f2, max_val), min_val); + + __m128i i1 = _mm_cvtps_epi32(f1); + __m128i i2 = _mm_cvtps_epi32(f2); + __m128i packed = _mm_packs_epi32(i1, i2); + + _mm_storeu_si128(reinterpret_cast<__m128i*>(&d[i]), packed); + } + + for (; i < num_samples; i++) { + d[i] = OrbisFloatToS16(s[i]); + } + } +#elif + static void ConvertF32ToS16_8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + const u32 num_samples = frames << 3; + for (u32 i = 0; i < num_samples; i++) + d[i] = OrbisFloatToS16(s[i]); + } +#endif + static void ConvertF32ToS16Std8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + s16* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) { + const u32 offset = i << 3; + + d[offset + FL] = OrbisFloatToS16(s[offset + FL]); + d[offset + FR] = OrbisFloatToS16(s[offset + FR]); + d[offset + FC] = OrbisFloatToS16(s[offset + FC]); + d[offset + LF] = OrbisFloatToS16(s[offset + LF]); + d[offset + SL] = OrbisFloatToS16(s[offset + STD_SL]); + d[offset + SR] = OrbisFloatToS16(s[offset + STD_SR]); + d[offset + BL] = OrbisFloatToS16(s[offset + STD_BL]); + d[offset + BR] = OrbisFloatToS16(s[offset + STD_BR]); + } + } + + // Audio format parameters + const u32 frame_size; + const u32 guest_buffer_size; + const u32 buffer_frames; + const u32 sample_rate; + const u32 num_channels; + const bool is_float; + const bool is_std; + const std::array channel_layout; + + alignas(64) u64 period_us{0}; + alignas(64) std::atomic last_output_time{0}; + u64 next_output_time{0}; + u64 last_volume_check_time{0}; + u32 output_count{0}; + + // OpenAL objects + OpenALDevice* device_context{nullptr}; + ALuint source{0}; + std::vector buffers; + std::vector available_buffers; + ALenum format{AL_FORMAT_STEREO16}; + + // Buffer management + u32 buffer_size_bytes{0}; + std::vector al_buffer_s16; // For S16 formats + std::vector al_buffer_float; // For float formats + + // Extension support + bool has_float_ext{false}; + bool use_native_float{false}; + + // Converter function pointer + ConverterFunc convert{nullptr}; + + // Volume management + alignas(64) std::atomic current_gain{1.0f}; + + std::string device_name; + bool device_registered; +}; + +std::unique_ptr OpenALAudioOut::Open(PortOut& port) { + return std::make_unique(port); +} + +} // namespace Libraries::AudioOut \ No newline at end of file diff --git a/src/core/libraries/audio/openal_manager.h b/src/core/libraries/audio/openal_manager.h new file mode 100644 index 000000000..4a6ef7920 --- /dev/null +++ b/src/core/libraries/audio/openal_manager.h @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include +#include +#include +#include +#include +#include + +namespace Libraries::AudioOut { + +struct DeviceContext { + ALCdevice* device{nullptr}; + ALCcontext* context{nullptr}; + std::string device_name; + int port_count{0}; + + bool IsValid() const { + return device != nullptr && context != nullptr; + } + + void Cleanup() { + if (context) { + alcDestroyContext(context); + context = nullptr; + } + if (device) { + alcCloseDevice(device); + device = nullptr; + } + port_count = 0; + } +}; + +class OpenALDevice { +public: + static OpenALDevice& GetInstance() { + static OpenALDevice instance; + return instance; + } + + // Register a port that uses this device + bool RegisterPort(const std::string& device_name) { + std::lock_guard lock(mutex); + + // Handle "Default Device" alias + std::string actual_device_name = device_name; + if (actual_device_name.empty() || actual_device_name == "Default Device") { + actual_device_name = GetDefaultDeviceName(); + } + + // Find or create device context for this device name + auto it = devices.find(actual_device_name); + if (it != devices.end()) { + // Device exists, increment count + it->second.port_count++; + LOG_INFO(Lib_AudioOut, "Reusing OpenAL device '{}', port count: {}", actual_device_name, + it->second.port_count); + return true; + } + + // Create new device + DeviceContext ctx; + if (!InitializeDevice(ctx, actual_device_name)) { + LOG_ERROR(Lib_AudioOut, "Failed to initialize OpenAL device '{}'", actual_device_name); + return false; + } + + ctx.port_count = 1; + devices[actual_device_name] = ctx; + + LOG_INFO(Lib_AudioOut, "Created new OpenAL device '{}'", actual_device_name); + return true; + } + + // Unregister a port + void UnregisterPort(const std::string& device_name) { + std::lock_guard lock(mutex); + + std::string actual_device_name = device_name; + if (actual_device_name.empty() || actual_device_name == "Default Device") { + actual_device_name = GetDefaultDeviceName(); + } + + auto it = devices.find(actual_device_name); + if (it != devices.end()) { + it->second.port_count--; + LOG_INFO(Lib_AudioOut, "Port unregistered from '{}', remaining ports: {}", + actual_device_name, it->second.port_count); + + if (it->second.port_count <= 0) { + LOG_INFO(Lib_AudioOut, "Cleaning up OpenAL device '{}'", actual_device_name); + it->second.Cleanup(); + devices.erase(it); + } + } + } + + bool MakeCurrent(const std::string& device_name) { + std::lock_guard lock(mutex); + + std::string actual_device_name = device_name; + if (actual_device_name.empty() || actual_device_name == "Default Device") { + actual_device_name = GetDefaultDeviceName(); + } + + auto it = devices.find(actual_device_name); + if (it == devices.end() || !it->second.IsValid()) { + return false; + } + + // Store current device for this thread (simplified - in practice you might want + // thread-local storage) + current_context = it->second.context; + return alcMakeContextCurrent(it->second.context); + } + + void ReleaseContext() { + std::lock_guard lock(mutex); + alcMakeContextCurrent(nullptr); + current_context = nullptr; + } + + // Get the default device name + static std::string GetDefaultDeviceName() { + const ALCchar* default_device = alcGetString(nullptr, ALC_DEFAULT_DEVICE_SPECIFIER); + return default_device ? default_device : "Default Device"; + } + + // Check if device enumeration is supported + static bool IsDeviceEnumerationSupported() { + return alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT") || + alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT"); + } + + // Get list of available devices + static std::vector GetAvailableDevices() { + std::vector devices_list; + + if (!alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT")) + return devices_list; + + const ALCchar* devices = nullptr; + if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) { + devices = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); + } else { + devices = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); + } + + if (!devices) + return devices_list; + + const ALCchar* ptr = devices; + while (*ptr != '\0') { + devices_list.emplace_back(ptr); + ptr += std::strlen(ptr) + 1; + } + + return devices_list; + } + +private: + OpenALDevice() = default; + ~OpenALDevice() { + std::lock_guard lock(mutex); + for (auto& [name, ctx] : devices) { + ctx.Cleanup(); + } + devices.clear(); + } + + OpenALDevice(const OpenALDevice&) = delete; + OpenALDevice& operator=(const OpenALDevice&) = delete; + + bool InitializeDevice(DeviceContext& ctx, const std::string& device_name) { + // Handle disabled audio + if (device_name == "None") { + return false; + } + + // Open the requested device + if (device_name.empty() || device_name == "Default Device") { + ctx.device = alcOpenDevice(nullptr); + } else { + ctx.device = alcOpenDevice(device_name.c_str()); + if (!ctx.device) { + LOG_WARNING(Lib_AudioOut, "Device '{}' not found, falling back to default", + device_name); + ctx.device = alcOpenDevice(nullptr); + } + } + + if (!ctx.device) { + LOG_ERROR(Lib_AudioOut, "Failed to open OpenAL device"); + return false; + } + + // Create context + ctx.context = alcCreateContext(ctx.device, nullptr); + if (!ctx.context) { + LOG_ERROR(Lib_AudioOut, "Failed to create OpenAL context"); + alcCloseDevice(ctx.device); + ctx.device = nullptr; + return false; + } + + // Get actual device name + const ALCchar* actual_name = nullptr; + if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATE_ALL_EXT")) { + actual_name = alcGetString(ctx.device, ALC_ALL_DEVICES_SPECIFIER); + } else { + actual_name = alcGetString(ctx.device, ALC_DEVICE_SPECIFIER); + } + ctx.device_name = actual_name ? actual_name : "Unknown"; + + LOG_INFO(Lib_AudioOut, "OpenAL device initialized: '{}'", ctx.device_name); + return true; + } + + std::unordered_map devices; + mutable std::mutex mutex; + ALCcontext* current_context{nullptr}; // For thread-local tracking +}; + +} // namespace Libraries::AudioOut \ No newline at end of file diff --git a/src/core/libraries/audio/sdl_audio_in.cpp b/src/core/libraries/audio/sdl_audio_in.cpp index d36811175..6e7a7bdbd 100644 --- a/src/core/libraries/audio/sdl_audio_in.cpp +++ b/src/core/libraries/audio/sdl_audio_in.cpp @@ -3,8 +3,8 @@ #include #include -#include #include +#include #include "audioin.h" #include "audioin_backend.h" @@ -21,7 +21,7 @@ public: fmt.channels = static_cast(port.channels_num); fmt.freq = static_cast(port.freq); - std::string micDevStr = Config::getMicDevice(); + std::string micDevStr = EmulatorSettings.GetSDLMicDevice(); uint32_t devId = 0; if (micDevStr == "None") { nullDevice = true; diff --git a/src/core/libraries/audio/sdl_audio_out.cpp b/src/core/libraries/audio/sdl_audio_out.cpp index ce2598759..b6706eff7 100644 --- a/src/core/libraries/audio/sdl_audio_out.cpp +++ b/src/core/libraries/audio/sdl_audio_out.cpp @@ -9,8 +9,8 @@ #include #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/audio/audioout.h" #include "core/libraries/audio/audioout_backend.h" #include "core/libraries/kernel/threads.h" @@ -110,7 +110,7 @@ public: max_channel_gain = std::max(max_channel_gain, channel_gain); } - const float slider_gain = Config::getVolumeSlider() * 0.01f; // Faster than /100.0f + const float slider_gain = EmulatorSettings.GetVolumeSlider() * 0.01f; // Faster than /100.0f const float total_gain = max_channel_gain * slider_gain; const float current = current_gain.load(std::memory_order_acquire); @@ -156,7 +156,7 @@ private: } // Initialize current gain - current_gain.store(Config::getVolumeSlider() * 0.01f, std::memory_order_relaxed); + current_gain.store(EmulatorSettings.GetVolumeSlider() * 0.01f, std::memory_order_relaxed); if (!SelectConverter()) { FreeAlignedBuffer(); @@ -201,7 +201,7 @@ private: last_volume_check_time = current_time; - const float config_volume = Config::getVolumeSlider() * 0.01f; + const float config_volume = EmulatorSettings.GetVolumeSlider() * 0.01f; const float stored_gain = current_gain.load(std::memory_order_acquire); // Only update if the difference is significant @@ -368,11 +368,11 @@ private: switch (type) { case OrbisAudioOutPort::Main: case OrbisAudioOutPort::Bgm: - return Config::getMainOutputDevice(); + return EmulatorSettings.GetSDLMainOutputDevice(); case OrbisAudioOutPort::PadSpk: - return Config::getPadSpkOutputDevice(); + return EmulatorSettings.GetSDLPadSpkOutputDevice(); default: - return Config::getMainOutputDevice(); + return EmulatorSettings.GetSDLMainOutputDevice(); } } diff --git a/src/core/libraries/audio3d/audio3d_openal.cpp b/src/core/libraries/audio3d/audio3d_openal.cpp new file mode 100644 index 000000000..53bbb8b24 --- /dev/null +++ b/src/core/libraries/audio3d/audio3d_openal.cpp @@ -0,0 +1,997 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include "common/assert.h" +#include "common/logging/log.h" +#include "core/libraries/audio/audioout.h" +#include "core/libraries/audio/audioout_error.h" +#include "core/libraries/audio3d/audio3d_error.h" +#include "core/libraries/audio3d/audio3d_openal.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" + +namespace Libraries::Audio3dOpenAL { + +static constexpr u32 AUDIO3D_SAMPLE_RATE = 48000; + +static constexpr AudioOut::OrbisAudioOutParamFormat AUDIO3D_OUTPUT_FORMAT = + AudioOut::OrbisAudioOutParamFormat::S16Stereo; +static constexpr u32 AUDIO3D_OUTPUT_NUM_CHANNELS = 2; + +static std::unique_ptr state; + +s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) { + LOG_INFO(Lib_Audio3d, "called, handle = {}", handle); + + // Remove from any port that was tracking this handle. + if (state) { + for (auto& [port_id, port] : state->ports) { + std::scoped_lock lock{port.mutex}; + auto& handles = port.audioout_handles; + handles.erase(std::remove(handles.begin(), handles.end(), handle), handles.end()); + } + } + + return AudioOut::sceAudioOutClose(handle); +} + +s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen( + const OrbisAudio3dPortId port_id, const Libraries::UserService::OrbisUserServiceUserId user_id, + s32 type, const s32 index, const u32 len, const u32 freq, + const AudioOut::OrbisAudioOutParamExtendedInformation param) { + LOG_INFO(Lib_Audio3d, + "called, port_id = {}, user_id = {}, type = {}, index = {}, len = {}, freq = {}", + port_id, user_id, type, index, len, freq); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + std::scoped_lock lock{state->ports[port_id].mutex}; + if (len != state->ports[port_id].parameters.granularity) { + LOG_ERROR(Lib_Audio3d, "len != state->ports[port_id].parameters.granularity"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const s32 handle = sceAudioOutOpen(user_id, static_cast(type), + index, len, freq, param); + if (handle < 0) { + return handle; + } + + // Track this handle in the port so sceAudio3dPortFlush can use it for sync. + state->ports[port_id].audioout_handles.push_back(handle); + return handle; +} + +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(const s32 handle, void* ptr) { + LOG_DEBUG(Lib_Audio3d, "called, handle = {}, ptr = {}", handle, ptr); + + if (!ptr) { + LOG_ERROR(Lib_Audio3d, "!ptr"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (handle < 0 || (handle & 0xFFFF) > 25) { + LOG_ERROR(Lib_Audio3d, "handle < 0 || (handle & 0xFFFF) > 25"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + return AudioOut::sceAudioOutOutput(handle, ptr); +} + +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* param, + const u32 num) { + LOG_DEBUG(Lib_Audio3d, "called, param = {}, num = {}", static_cast(param), num); + + if (!param || !num) { + LOG_ERROR(Lib_Audio3d, "!param || !num"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + return AudioOut::sceAudioOutOutputs(param, num); +} + +static s32 ConvertAndEnqueue(std::deque& queue, const OrbisAudio3dPcm& pcm, + const u32 num_channels, const u32 granularity) { + if (!pcm.sample_buffer || !pcm.num_samples) { + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const u32 bytes_per_sample = + (pcm.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) ? sizeof(s16) : sizeof(float); + + // Always allocate exactly granularity samples (zeroed = silence for padding). + const u32 dst_bytes = granularity * num_channels * bytes_per_sample; + u8* copy = static_cast(std::calloc(1, dst_bytes)); + if (!copy) { + return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; + } + + // Copy min(provided, granularity) samples — extra are dropped, shortage stays zero. + const u32 samples_to_copy = std::min(pcm.num_samples, granularity); + std::memcpy(copy, pcm.sample_buffer, samples_to_copy * num_channels * bytes_per_sample); + + queue.emplace_back(AudioData{ + .sample_buffer = copy, + .num_samples = granularity, + .num_channels = num_channels, + .format = pcm.format, + }); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dBedWrite(const OrbisAudio3dPortId port_id, const u32 num_channels, + const OrbisAudio3dFormat format, void* buffer, + const u32 num_samples) { + return sceAudio3dBedWrite2(port_id, num_channels, format, buffer, num_samples, + OrbisAudio3dOutputRoute::ORBIS_AUDIO3D_OUTPUT_BOTH, false); +} + +s32 PS4_SYSV_ABI sceAudio3dBedWrite2(const OrbisAudio3dPortId port_id, const u32 num_channels, + const OrbisAudio3dFormat format, void* buffer, + const u32 num_samples, + const OrbisAudio3dOutputRoute output_route, + const bool restricted) { + LOG_DEBUG( + Lib_Audio3d, + "called, port_id = {}, num_channels = {}, format = {}, num_samples = {}, output_route " + "= {}, restricted = {}", + port_id, num_channels, magic_enum::enum_name(format), num_samples, + magic_enum::enum_name(output_route), restricted); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (output_route > OrbisAudio3dOutputRoute::ORBIS_AUDIO3D_OUTPUT_BOTH) { + LOG_ERROR(Lib_Audio3d, "output_route > ORBIS_AUDIO3D_OUTPUT_BOTH"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (format > OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_FLOAT) { + LOG_ERROR(Lib_Audio3d, "format > ORBIS_AUDIO3D_FORMAT_FLOAT"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (num_channels != 2 && num_channels != 6 && num_channels != 8) { + LOG_ERROR(Lib_Audio3d, "num_channels must be 2, 6, or 8"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (!buffer || !num_samples) { + LOG_ERROR(Lib_Audio3d, "!buffer || !num_samples"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_FLOAT) { + if ((reinterpret_cast(buffer) & 3) != 0) { + LOG_ERROR(Lib_Audio3d, "buffer & 3 != 0"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + } else if (format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) { + if ((reinterpret_cast(buffer) & 1) != 0) { + LOG_ERROR(Lib_Audio3d, "buffer & 1 != 0"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + } + + std::scoped_lock lock{state->ports[port_id].mutex}; + return ConvertAndEnqueue(state->ports[port_id].bed_queue, + OrbisAudio3dPcm{ + .format = format, + .sample_buffer = buffer, + .num_samples = num_samples, + }, + num_channels, state->ports[port_id].parameters.granularity); +} + +s32 PS4_SYSV_ABI sceAudio3dCreateSpeakerArray() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* params) { + LOG_DEBUG(Lib_Audio3d, "called"); + if (params) { + auto default_params = OrbisAudio3dOpenParameters{ + .size_this = 0x20, + .granularity = 0x100, + .rate = OrbisAudio3dRate::ORBIS_AUDIO3D_RATE_48000, + .max_objects = 512, + .queue_depth = 2, + .buffer_mode = OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH, + }; + memcpy(params, &default_params, 0x20); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMemorySize() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients2() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dInitialize(const s64 reserved) { + LOG_INFO(Lib_Audio3d, "called, reserved = {}", reserved); + + if (reserved != 0) { + LOG_ERROR(Lib_Audio3d, "reserved != 0"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + if (state) { + LOG_ERROR(Lib_Audio3d, "already initialized"); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + state = std::make_unique(); + + if (const auto init_ret = AudioOut::sceAudioOutInit(); + init_ret < 0 && init_ret != ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT) { + return init_ret; + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectReserve(const OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId* object_id) { + LOG_INFO(Lib_Audio3d, "called, port_id = {}, object_id = {}", port_id, + static_cast(object_id)); + + if (!object_id) { + LOG_ERROR(Lib_Audio3d, "!object_id"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + *object_id = ORBIS_AUDIO3D_OBJECT_INVALID; + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + + // Enforce the max_objects limit set at PortOpen time. + if (port.objects.size() >= port.parameters.max_objects) { + LOG_ERROR(Lib_Audio3d, "port has no available objects (max_objects = {})", + port.parameters.max_objects); + return ORBIS_AUDIO3D_ERROR_OUT_OF_RESOURCES; + } + + // Counter lives in the Port so it resets when the port is closed and reopened. + do { + ++port.next_object_id; + } while (port.next_object_id == 0 || + port.next_object_id == static_cast(ORBIS_AUDIO3D_OBJECT_INVALID) || + port.objects.contains(port.next_object_id)); + + *object_id = port.next_object_id; + port.objects.emplace(*object_id, ObjectState{}); + LOG_INFO(Lib_Audio3d, "reserved object_id = {}", *object_id); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttribute(const OrbisAudio3dPortId port_id, + const OrbisAudio3dObjectId object_id, + const OrbisAudio3dAttributeId attribute_id, + const void* attribute, const u64 attribute_size) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, object_id = {}, attribute_id = {:#x}, size = {}", + port_id, object_id, static_cast(attribute_id), attribute_size); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + if (!port.objects.contains(object_id)) { + LOG_DEBUG(Lib_Audio3d, "object_id {} not reserved (race with Unreserve?), no-op", + object_id); + return ORBIS_OK; + } + + if (!attribute_size && + attribute_id != OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE) { + LOG_ERROR(Lib_Audio3d, "!attribute_size for non-reset attribute"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + auto& obj = port.objects[object_id]; + + // RESET_STATE clears all attributes and queued PCM; it takes no value. + if (attribute_id == OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE) { + for (auto& data : obj.pcm_queue) { + std::free(data.sample_buffer); + } + obj.pcm_queue.clear(); + obj.persistent_attributes.clear(); + LOG_DEBUG(Lib_Audio3d, "RESET_STATE for object {}", object_id); + return ORBIS_OK; + } + + // Store the attribute so it's available when we implement it. + const auto* src = static_cast(attribute); + obj.persistent_attributes[static_cast(attribute_id)].assign(src, src + attribute_size); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(const OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id, + const u64 num_attributes, + const OrbisAudio3dAttribute* attribute_array) { + LOG_DEBUG(Lib_Audio3d, + "called, port_id = {}, object_id = {}, num_attributes = {}, attribute_array = {}", + port_id, object_id, num_attributes, fmt::ptr(attribute_array)); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (!num_attributes || !attribute_array) { + LOG_ERROR(Lib_Audio3d, "!num_attributes || !attribute_array"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + if (!port.objects.contains(object_id)) { + LOG_DEBUG(Lib_Audio3d, "object_id {} not reserved", object_id); + return ORBIS_OK; + } + + auto& obj = port.objects[object_id]; + + // First pass: handle RESET_STATE. + for (u64 i = 0; i < num_attributes; i++) { + if (attribute_array[i].attribute_id == + OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE) { + for (auto& data : obj.pcm_queue) { + std::free(data.sample_buffer); + } + obj.pcm_queue.clear(); + obj.persistent_attributes.clear(); + LOG_DEBUG(Lib_Audio3d, "RESET_STATE for object {}", object_id); + break; // Only one reset is needed even if listed multiple times. + } + } + + // Second pass: apply all other attributes. + for (u64 i = 0; i < num_attributes; i++) { + const auto& attr = attribute_array[i]; + + switch (attr.attribute_id) { + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE: + break; // Already applied in first pass above. + case OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_PCM: { + if (attr.value_size < sizeof(OrbisAudio3dPcm)) { + LOG_ERROR(Lib_Audio3d, "PCM attribute value_size too small"); + continue; + } + const auto pcm = static_cast(attr.value); + // Object audio is always mono (1 channel). + if (const auto ret = + ConvertAndEnqueue(obj.pcm_queue, *pcm, 1, port.parameters.granularity); + ret != ORBIS_OK) { + return ret; + } + break; + } + default: { + // Store the other attributes in the ObjectState so they're available when we + // implement them. + if (attr.value && attr.value_size > 0) { + const auto* src = static_cast(attr.value); + obj.persistent_attributes[static_cast(attr.attribute_id)].assign( + src, src + attr.value_size); + } + LOG_DEBUG(Lib_Audio3d, "Stored attribute {:#x} for object {}", + static_cast(attr.attribute_id), object_id); + break; + } + } + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(const OrbisAudio3dPortId port_id, + const OrbisAudio3dObjectId object_id) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, object_id = {}", port_id, object_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + + if (!port.objects.contains(object_id)) { + LOG_ERROR(Lib_Audio3d, "object_id not reserved"); + return ORBIS_AUDIO3D_ERROR_INVALID_OBJECT; + } + + // Free any queued PCM audio for this object. + for (auto& data : port.objects[object_id].pcm_queue) { + std::free(data.sample_buffer); + } + + port.objects.erase(object_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortAdvance(const OrbisAudio3dPortId port_id) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}", port_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + + if (port.parameters.buffer_mode == OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_NO_ADVANCE) { + LOG_ERROR(Lib_Audio3d, "port doesn't have advance capability"); + return ORBIS_AUDIO3D_ERROR_NOT_SUPPORTED; + } + + if (port.mixed_queue.size() >= port.parameters.queue_depth) { + LOG_WARNING(Lib_Audio3d, "mixed queue full (depth={}), dropping advance", + port.parameters.queue_depth); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + const u32 granularity = port.parameters.granularity; + const u32 out_samples = granularity * AUDIO3D_OUTPUT_NUM_CHANNELS; + + // ---- FLOAT MIX BUFFER ---- + float* mix_float = static_cast(std::calloc(out_samples, sizeof(float))); + if (!mix_float) + return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; + + auto mix_in = [&](std::deque& queue, const float gain) { + if (queue.empty()) + return; + + // default gain is 0.0 — objects with no GAIN set are silent. + if (gain == 0.0f) { + AudioData data = queue.front(); + queue.pop_front(); + std::free(data.sample_buffer); + return; + } + + AudioData data = queue.front(); + queue.pop_front(); + + const u32 frames = std::min(granularity, data.num_samples); + const u32 channels = data.num_channels; + + if (data.format == OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16) { + const s16* src = reinterpret_cast(data.sample_buffer); + + for (u32 i = 0; i < frames; i++) { + float left = 0.0f; + float right = 0.0f; + + if (channels == 1) { + float v = src[i] / 32768.0f; + left = v; + right = v; + } else { + left = src[i * channels + 0] / 32768.0f; + right = src[i * channels + 1] / 32768.0f; + } + + mix_float[i * 2 + 0] += left * gain; + mix_float[i * 2 + 1] += right * gain; + } + } else { // FLOAT input + const float* src = reinterpret_cast(data.sample_buffer); + + for (u32 i = 0; i < frames; i++) { + float left = 0.0f; + float right = 0.0f; + + if (channels == 1) { + left = src[i]; + right = src[i]; + } else { + left = src[i * channels + 0]; + right = src[i * channels + 1]; + } + + mix_float[i * 2 + 0] += left * gain; + mix_float[i * 2 + 1] += right * gain; + } + } + + std::free(data.sample_buffer); + }; + + // Bed is mixed at full gain (1.0). + mix_in(port.bed_queue, 1.0f); + + // Mix all object PCM queues, applying each object's GAIN persistent attribute. + for (auto& [obj_id, obj] : port.objects) { + float gain = 0.0f; + const auto gain_key = + static_cast(OrbisAudio3dAttributeId::ORBIS_AUDIO3D_ATTRIBUTE_GAIN); + if (obj.persistent_attributes.contains(gain_key)) { + const auto& blob = obj.persistent_attributes.at(gain_key); + if (blob.size() >= sizeof(float)) { + std::memcpy(&gain, blob.data(), sizeof(float)); + } + } + mix_in(obj.pcm_queue, gain); + } + + s16* mix_s16 = static_cast(std::malloc(out_samples * sizeof(s16))); + if (!mix_s16) { + std::free(mix_float); + return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY; + } + + for (u32 i = 0; i < out_samples; i++) { + float v = std::clamp(mix_float[i], -1.0f, 1.0f); + mix_s16[i] = static_cast(v * 32767.0f); + } + + std::free(mix_float); + + port.mixed_queue.push_back(AudioData{.sample_buffer = reinterpret_cast(mix_s16), + .num_samples = granularity, + .num_channels = AUDIO3D_OUTPUT_NUM_CHANNELS, + .format = OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortClose(const OrbisAudio3dPortId port_id) { + LOG_INFO(Lib_Audio3d, "called, port_id = {}", port_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + { + std::scoped_lock lock{port.mutex}; + + if (port.audio_out_handle >= 0) { + AudioOut::sceAudioOutClose(port.audio_out_handle); + port.audio_out_handle = -1; + } + + for (const s32 handle : port.audioout_handles) { + AudioOut::sceAudioOutClose(handle); + } + port.audioout_handles.clear(); + + for (auto& data : port.mixed_queue) { + std::free(data.sample_buffer); + } + + for (auto& data : port.bed_queue) { + std::free(data.sample_buffer); + } + + for (auto& [obj_id, obj] : port.objects) { + for (auto& data : obj.pcm_queue) { + std::free(data.sample_buffer); + } + } + } + + state->ports.erase(port_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortCreate() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortDestroy() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortFlush(const OrbisAudio3dPortId port_id) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}", port_id); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + + if (!port.audioout_handles.empty()) { + for (const s32 handle : port.audioout_handles) { + const s32 ret = AudioOut::sceAudioOutOutput(handle, nullptr); + if (ret < 0) { + return ret; + } + } + return ORBIS_OK; + } + + if (port.mixed_queue.empty()) { + // Only mix if there's actually something to mix. + if (!port.bed_queue.empty() || + std::any_of(port.objects.begin(), port.objects.end(), + [](const auto& kv) { return !kv.second.pcm_queue.empty(); })) { + const s32 ret = sceAudio3dPortAdvance(port_id); + if (ret != ORBIS_OK && ret != ORBIS_AUDIO3D_ERROR_NOT_READY) { + return ret; + } + } + } + + if (port.mixed_queue.empty()) { + return ORBIS_OK; + } + + if (port.audio_out_handle < 0) { + AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; + ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); + port.audio_out_handle = + AudioOut::sceAudioOutOpen(0xFF, AudioOut::OrbisAudioOutPort::Audio3d, 0, + port.parameters.granularity, AUDIO3D_SAMPLE_RATE, ext_info); + if (port.audio_out_handle < 0) { + return port.audio_out_handle; + } + } + + // Drain all queued mixed frames, blocking on each until consumed. + while (!port.mixed_queue.empty()) { + AudioData frame = port.mixed_queue.front(); + port.mixed_queue.pop_front(); + const s32 ret = AudioOut::sceAudioOutOutput(port.audio_out_handle, frame.sample_buffer); + std::free(frame.sample_buffer); + if (ret < 0) { + return ret; + } + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortFreeState() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetList() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetParameters() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(const OrbisAudio3dPortId port_id, u32* queue_level, + u32* queue_available) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, queue_level = {}, queue_available = {}", port_id, + static_cast(queue_level), static_cast(queue_available)); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (!queue_level && !queue_available) { + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const auto& port = state->ports[port_id]; + std::scoped_lock lock{port.mutex}; + const size_t size = port.mixed_queue.size(); + + if (queue_level) { + *queue_level = static_cast(size); + } + + if (queue_available) { + const u32 depth = port.parameters.queue_depth; + *queue_available = (size < depth) ? static_cast(depth - size) : 0u; + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetState() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortGetStatus() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortOpen(const Libraries::UserService::OrbisUserServiceUserId user_id, + const OrbisAudio3dOpenParameters* parameters, + OrbisAudio3dPortId* port_id) { + LOG_INFO(Lib_Audio3d, "called, user_id = {}, parameters = {}, id = {}", user_id, + static_cast(parameters), static_cast(port_id)); + + if (!state) { + LOG_ERROR(Lib_Audio3d, "!initialized"); + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + if (!parameters || !port_id) { + LOG_ERROR(Lib_Audio3d, "!parameters || !id"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + const int id = static_cast(state->ports.size()) + 1; + + if (id > 3) { + LOG_ERROR(Lib_Audio3d, "id > 3"); + return ORBIS_AUDIO3D_ERROR_OUT_OF_RESOURCES; + } + + *port_id = id; + auto& port = state->ports[id]; + std::memcpy( + &port.parameters, parameters, + std::min(parameters->size_this, static_cast(sizeof(OrbisAudio3dOpenParameters)))); + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortPush(const OrbisAudio3dPortId port_id, + const OrbisAudio3dBlocking blocking) { + LOG_DEBUG(Lib_Audio3d, "called, port_id = {}, blocking = {}", port_id, + magic_enum::enum_name(blocking)); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + auto& port = state->ports[port_id]; + + if (port.parameters.buffer_mode != + OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH) { + LOG_ERROR(Lib_Audio3d, "port doesn't have push capability"); + return ORBIS_AUDIO3D_ERROR_NOT_SUPPORTED; + } + + const u32 depth = port.parameters.queue_depth; + + if (port.audio_out_handle < 0) { + AudioOut::OrbisAudioOutParamExtendedInformation ext_info{}; + ext_info.data_format.Assign(AUDIO3D_OUTPUT_FORMAT); + + port.audio_out_handle = + AudioOut::sceAudioOutOpen(0xFF, AudioOut::OrbisAudioOutPort::Audio3d, 0, + port.parameters.granularity, AUDIO3D_SAMPLE_RATE, ext_info); + + if (port.audio_out_handle < 0) + return port.audio_out_handle; + } + + // Function that submits exactly one frame (if available). + auto submit_one_frame = [&](bool& submitted) -> s32 { + AudioData frame; + { + std::scoped_lock lock{port.mutex}; + + if (port.mixed_queue.empty()) { + submitted = false; + return ORBIS_OK; + } + + frame = port.mixed_queue.front(); + port.mixed_queue.pop_front(); + } + + const s32 ret = AudioOut::sceAudioOutOutput(port.audio_out_handle, frame.sample_buffer); + std::free(frame.sample_buffer); + + if (ret < 0) + return ret; + + submitted = true; + return ORBIS_OK; + }; + + // If not full, return immediately. + { + std::scoped_lock lock{port.mutex}; + if (port.mixed_queue.size() < depth) { + return ORBIS_OK; + } + } + + // Submit one frame to free space. + bool submitted = false; + s32 ret = submit_one_frame(submitted); + if (ret < 0) + return ret; + + if (!submitted) + return ORBIS_OK; + + // ASYNC: free exactly one slot and return. + if (blocking == OrbisAudio3dBlocking::ORBIS_AUDIO3D_BLOCKING_ASYNC) { + return ORBIS_OK; + } + + // SYNC: ensure at least one slot is free (drain until size < depth). + while (true) { + { + std::scoped_lock lock{port.mutex}; + if (port.mixed_queue.size() < depth) + break; + } + + bool drained = false; + ret = submit_one_frame(drained); + if (ret < 0) + return ret; + + if (!drained) + break; + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(const OrbisAudio3dPortId port_id, + const OrbisAudio3dAttributeId attribute_id, + void* attribute, const u64 attribute_size) { + LOG_INFO(Lib_Audio3d, + "called, port_id = {}, attribute_id = {}, attribute = {}, attribute_size = {}", + port_id, static_cast(attribute_id), attribute, attribute_size); + + if (!state->ports.contains(port_id)) { + LOG_ERROR(Lib_Audio3d, "!state->ports.contains(port_id)"); + return ORBIS_AUDIO3D_ERROR_INVALID_PORT; + } + + if (!attribute) { + LOG_ERROR(Lib_Audio3d, "!attribute"); + return ORBIS_AUDIO3D_ERROR_INVALID_PARAMETER; + } + + // TODO + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dReportUnregisterHandler() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dSetGpuRenderer() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dStrError() { + LOG_ERROR(Lib_Audio3d, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudio3dTerminate() { + LOG_INFO(Lib_Audio3d, "called"); + + if (!state) { + return ORBIS_AUDIO3D_ERROR_NOT_READY; + } + + std::vector port_ids; + for (const auto& [id, _] : state->ports) { + port_ids.push_back(id); + } + for (const auto id : port_ids) { + sceAudio3dPortClose(id); + } + + state.reset(); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("pZlOm1aF3aA", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutClose); + LIB_FUNCTION("ucEsi62soTo", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutOpen); + LIB_FUNCTION("7NYEzJ9SJbM", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutOutput); + LIB_FUNCTION("HbxYY27lK6E", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dAudioOutOutputs); + LIB_FUNCTION("9tEwE0GV0qo", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dBedWrite); + LIB_FUNCTION("xH4Q9UILL3o", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dBedWrite2); + LIB_FUNCTION("lvWMW6vEqFU", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dCreateSpeakerArray); + LIB_FUNCTION("8hm6YdoQgwg", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dDeleteSpeakerArray); + LIB_FUNCTION("Im+jOoa5WAI", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetDefaultOpenParameters); + LIB_FUNCTION("kEqqyDkmgdI", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetSpeakerArrayMemorySize); + LIB_FUNCTION("-R1DukFq7Dk", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetSpeakerArrayMixCoefficients); + LIB_FUNCTION("-Re+pCWvwjQ", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dGetSpeakerArrayMixCoefficients2); + LIB_FUNCTION("UmCvjSmuZIw", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dInitialize); + LIB_FUNCTION("jO2tec4dJ2M", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectReserve); + LIB_FUNCTION("V1FBFpNIAzk", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectSetAttribute); + LIB_FUNCTION("4uyHN9q4ZeU", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectSetAttributes); + LIB_FUNCTION("1HXxo-+1qCw", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dObjectUnreserve); + LIB_FUNCTION("lw0qrdSjZt8", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortAdvance); + LIB_FUNCTION("OyVqOeVNtSk", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortClose); + LIB_FUNCTION("UHFOgVNz0kk", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortCreate); + LIB_FUNCTION("Mw9mRQtWepY", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortDestroy); + LIB_FUNCTION("ZOGrxWLgQzE", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortFlush); + LIB_FUNCTION("uJ0VhGcxCTQ", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortFreeState); + LIB_FUNCTION("9ZA23Ia46Po", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dPortGetAttributesSupported); + LIB_FUNCTION("SEggctIeTcI", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetList); + LIB_FUNCTION("flPcUaXVXcw", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetParameters); + LIB_FUNCTION("YaaDbDwKpFM", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetQueueLevel); + LIB_FUNCTION("CKHlRW2E9dA", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetState); + LIB_FUNCTION("iRX6GJs9tvE", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortGetStatus); + LIB_FUNCTION("XeDDK0xJWQA", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortOpen); + LIB_FUNCTION("VEVhZ9qd4ZY", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortPush); + LIB_FUNCTION("-pzYDZozm+M", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortQueryDebug); + LIB_FUNCTION("Yq9bfUQ0uJg", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dPortSetAttribute); + LIB_FUNCTION("QfNXBrKZeI0", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dReportRegisterHandler); + LIB_FUNCTION("psv2gbihC1A", "libSceAudio3d", 1, "libSceAudio3d", + sceAudio3dReportUnregisterHandler); + LIB_FUNCTION("yEYXcbAGK14", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dSetGpuRenderer); + LIB_FUNCTION("Aacl5qkRU6U", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dStrError); + LIB_FUNCTION("WW1TS2iz5yc", "libSceAudio3d", 1, "libSceAudio3d", sceAudio3dTerminate); +} + +} // namespace Libraries::Audio3dOpenAL diff --git a/src/core/libraries/audio3d/audio3d_openal.h b/src/core/libraries/audio3d/audio3d_openal.h new file mode 100644 index 000000000..75be72066 --- /dev/null +++ b/src/core/libraries/audio3d/audio3d_openal.h @@ -0,0 +1,181 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "common/types.h" +#include "core/libraries/audio/audioout.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Audio3dOpenAL { + +constexpr int ORBIS_AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF; + +enum class OrbisAudio3dRate : u32 { + ORBIS_AUDIO3D_RATE_48000 = 0, +}; + +enum class OrbisAudio3dBufferMode : u32 { + ORBIS_AUDIO3D_BUFFER_NO_ADVANCE = 0, + ORBIS_AUDIO3D_BUFFER_ADVANCE_NO_PUSH = 1, + ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2, +}; + +struct OrbisAudio3dOpenParameters { + u64 size_this; + u32 granularity; + OrbisAudio3dRate rate; + u32 max_objects; + u32 queue_depth; + OrbisAudio3dBufferMode buffer_mode; + int : 32; + u32 num_beds; +}; + +enum class OrbisAudio3dFormat : u32 { + ORBIS_AUDIO3D_FORMAT_S16 = 0, + ORBIS_AUDIO3D_FORMAT_FLOAT = 1, +}; + +enum class OrbisAudio3dOutputRoute : u32 { + ORBIS_AUDIO3D_OUTPUT_BOTH = 0, + ORBIS_AUDIO3D_OUTPUT_HMU_ONLY = 1, + ORBIS_AUDIO3D_OUTPUT_TV_ONLY = 2, +}; + +enum class OrbisAudio3dBlocking : u32 { + ORBIS_AUDIO3D_BLOCKING_ASYNC = 0, + ORBIS_AUDIO3D_BLOCKING_SYNC = 1, +}; + +struct OrbisAudio3dPcm { + OrbisAudio3dFormat format; + void* sample_buffer; + u32 num_samples; +}; + +enum class OrbisAudio3dAttributeId : u32 { + ORBIS_AUDIO3D_ATTRIBUTE_PCM = 1, + ORBIS_AUDIO3D_ATTRIBUTE_POSITION = 2, + ORBIS_AUDIO3D_ATTRIBUTE_GAIN = 3, + ORBIS_AUDIO3D_ATTRIBUTE_SPREAD = 4, + ORBIS_AUDIO3D_ATTRIBUTE_PRIORITY = 5, + ORBIS_AUDIO3D_ATTRIBUTE_PASSTHROUGH = 6, + ORBIS_AUDIO3D_ATTRIBUTE_AMBISONICS = 7, + ORBIS_AUDIO3D_ATTRIBUTE_APPLICATION_SPECIFIC = 8, + ORBIS_AUDIO3D_ATTRIBUTE_RESET_STATE = 9, + ORBIS_AUDIO3D_ATTRIBUTE_RESTRICTED = 10, + ORBIS_AUDIO3D_ATTRIBUTE_OUTPUT_ROUTE = 11, +}; + +using OrbisAudio3dPortId = u32; +using OrbisAudio3dObjectId = u32; +using OrbisAudio3dAmbisonics = u32; + +struct OrbisAudio3dAttribute { + OrbisAudio3dAttributeId attribute_id; + int : 32; + void* value; + u64 value_size; +}; + +struct AudioData { + u8* sample_buffer; + u32 num_samples; + u32 num_channels{1}; + OrbisAudio3dFormat format{OrbisAudio3dFormat::ORBIS_AUDIO3D_FORMAT_S16}; +}; + +struct ObjectState { + std::deque pcm_queue; + std::unordered_map> persistent_attributes; +}; + +struct Port { + mutable std::recursive_mutex mutex; + OrbisAudio3dOpenParameters parameters{}; + // Opened lazily on the first sceAudio3dPortPush call. + s32 audio_out_handle{-1}; + // Handles explicitly opened by the game via sceAudio3dAudioOutOpen. + std::vector audioout_handles; + // Reserved objects and their state. + std::unordered_map objects; + // Increasing counter for generating unique object IDs within this port. + OrbisAudio3dObjectId next_object_id{0}; + // Bed audio queue. + std::deque bed_queue; + // Mixed stereo frames ready to be consumed by sceAudio3dPortPush. + std::deque mixed_queue; +}; + +struct Audio3dState { + std::unordered_map ports; +}; + +s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(s32 handle); +s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id, + Libraries::UserService::OrbisUserServiceUserId user_id, + s32 type, s32 index, u32 len, u32 freq, + AudioOut::OrbisAudioOutParamExtendedInformation param); +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(s32 handle, void* ptr); +s32 PS4_SYSV_ABI sceAudio3dAudioOutOutputs(AudioOut::OrbisAudioOutOutputParam* param, u32 num); +s32 PS4_SYSV_ABI sceAudio3dBedWrite(OrbisAudio3dPortId port_id, u32 num_channels, + OrbisAudio3dFormat format, void* buffer, u32 num_samples); +s32 PS4_SYSV_ABI sceAudio3dBedWrite2(OrbisAudio3dPortId port_id, u32 num_channels, + OrbisAudio3dFormat format, void* buffer, u32 num_samples, + OrbisAudio3dOutputRoute output_route, bool restricted); +s32 PS4_SYSV_ABI sceAudio3dCreateSpeakerArray(); +s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray(); +s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* params); +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMemorySize(); +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients(); +s32 PS4_SYSV_ABI sceAudio3dGetSpeakerArrayMixCoefficients2(); +s32 PS4_SYSV_ABI sceAudio3dInitialize(s64 reserved); +s32 PS4_SYSV_ABI sceAudio3dObjectReserve(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId* object_id); +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttribute(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id, + OrbisAudio3dAttributeId attribute_id, + const void* attribute, u64 attribute_size); +s32 PS4_SYSV_ABI sceAudio3dObjectSetAttributes(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id, u64 num_attributes, + const OrbisAudio3dAttribute* attribute_array); +s32 PS4_SYSV_ABI sceAudio3dObjectUnreserve(OrbisAudio3dPortId port_id, + OrbisAudio3dObjectId object_id); +s32 PS4_SYSV_ABI sceAudio3dPortAdvance(OrbisAudio3dPortId port_id); +s32 PS4_SYSV_ABI sceAudio3dPortClose(OrbisAudio3dPortId port_id); +s32 PS4_SYSV_ABI sceAudio3dPortCreate(); +s32 PS4_SYSV_ABI sceAudio3dPortDestroy(); +s32 PS4_SYSV_ABI sceAudio3dPortFlush(OrbisAudio3dPortId port_id); +s32 PS4_SYSV_ABI sceAudio3dPortFreeState(); +s32 PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported(); +s32 PS4_SYSV_ABI sceAudio3dPortGetList(); +s32 PS4_SYSV_ABI sceAudio3dPortGetParameters(); +s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(OrbisAudio3dPortId port_id, u32* queue_level, + u32* queue_available); +s32 PS4_SYSV_ABI sceAudio3dPortGetState(); +s32 PS4_SYSV_ABI sceAudio3dPortGetStatus(); +s32 PS4_SYSV_ABI sceAudio3dPortOpen(Libraries::UserService::OrbisUserServiceUserId user_id, + const OrbisAudio3dOpenParameters* parameters, + OrbisAudio3dPortId* port_id); +s32 PS4_SYSV_ABI sceAudio3dPortPush(OrbisAudio3dPortId port_id, OrbisAudio3dBlocking blocking); +s32 PS4_SYSV_ABI sceAudio3dPortQueryDebug(); +s32 PS4_SYSV_ABI sceAudio3dPortSetAttribute(OrbisAudio3dPortId port_id, + OrbisAudio3dAttributeId attribute_id, void* attribute, + u64 attribute_size); +s32 PS4_SYSV_ABI sceAudio3dReportRegisterHandler(); +s32 PS4_SYSV_ABI sceAudio3dReportUnregisterHandler(); +s32 PS4_SYSV_ABI sceAudio3dSetGpuRenderer(); +s32 PS4_SYSV_ABI sceAudio3dStrError(); +s32 PS4_SYSV_ABI sceAudio3dTerminate(); + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Audio3dOpenAL diff --git a/src/core/libraries/camera/camera.cpp b/src/core/libraries/camera/camera.cpp index 56ed28f2d..14aad8e84 100644 --- a/src/core/libraries/camera/camera.cpp +++ b/src/core/libraries/camera/camera.cpp @@ -3,17 +3,27 @@ #include "common/elf_info.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/camera/camera.h" #include "core/libraries/camera/camera_error.h" #include "core/libraries/error_codes.h" #include "core/libraries/kernel/process.h" #include "core/libraries/libs.h" +#include + +#include +#include "SDL3/SDL_camera.h" + namespace Libraries::Camera { static bool g_library_opened = false; static s32 g_firmware_version = 0; static s32 g_handles = 0; +static constexpr s32 c_width = 1280, c_height = 800; + +SDL_Camera* sdl_camera = nullptr; +OrbisCameraConfigExtention output_config0, output_config1; s32 PS4_SYSV_ABI sceCameraAccGetData() { LOG_ERROR(Lib_Camera, "(STUBBED) called"); @@ -325,16 +335,126 @@ s32 PS4_SYSV_ABI sceCameraGetExposureGain(s32 handle, OrbisCameraChannel channel return ORBIS_OK; } +static std::vector raw16_buffer1, raw16_buffer2; +static std::vector raw8_buffer1, raw8_buffer2; + +static void ConvertRGBA8888ToRAW16(const u8* src, u16* dst, int width, int height) { + for (int y = 0; y < height; ++y) { + const u8* row = src + y * width * 4; + u16* outRow = dst + y * width; + + for (int x = 0; x < width; ++x) { + const u8* px = row + x * 4; + + u16 b = u16(px[1]) << 4; + u16 g = u16(px[2]) << 4; + u16 r = u16(px[3]) << 4; + + // BGGR Bayer layout + // B G + // G R + bool evenRow = (y & 1) == 0; + bool evenCol = (x & 1) == 0; + + if (evenRow && evenCol) { + outRow[x] = b; + } else if (evenRow && !evenCol) { + outRow[x] = g; + } else if (!evenRow && evenCol) { + outRow[x] = g; + } else { + outRow[x] = r; + } + } + } +} + +static void ConvertRGBA8888ToRAW8(const u8* src, u8* dst, int width, int height) { + for (int y = 0; y < height; ++y) { + const u8* row = src + y * width * 4; + u8* outRow = dst + y * width; + + for (int x = 0; x < width; ++x) { + const u8* px = row + x * 4; + + u8 b = px[1]; + u8 g = px[2]; + u8 r = px[3]; + + // BGGR Bayer layout + // B G + // G R + bool evenRow = (y & 1) == 0; + bool evenCol = (x & 1) == 0; + + if (evenRow && evenCol) { + outRow[x] = b; + } else if (evenRow && !evenCol) { + outRow[x] = g; + } else if (!evenRow && evenCol) { + outRow[x] = g; + } else { + outRow[x] = r; + } + } + } +} + s32 PS4_SYSV_ABI sceCameraGetFrameData(s32 handle, OrbisCameraFrameData* frame_data) { LOG_DEBUG(Lib_Camera, "called"); if (handle < 1 || frame_data == nullptr || frame_data->sizeThis > 584) { return ORBIS_CAMERA_ERROR_PARAM; } - if (!g_library_opened) { + if (!g_library_opened || !sdl_camera) { return ORBIS_CAMERA_ERROR_NOT_OPEN; } + if (EmulatorSettings.GetCameraId() == -1) { + return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + } + Uint64 timestampNS = 0; + static SDL_Surface* frame = nullptr; + if (frame) { // release previous frame, if it exists + SDL_ReleaseCameraFrame(sdl_camera, frame); + } + frame = SDL_AcquireCameraFrame(sdl_camera, ×tampNS); - return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + if (!frame) { + return ORBIS_CAMERA_ERROR_BUSY; + } + + switch (output_config0.format.formatLevel0) { + case ORBIS_CAMERA_FORMAT_YUV422: + frame_data->pFramePointerList[0][0] = frame->pixels; + break; + case ORBIS_CAMERA_FORMAT_RAW16: + ConvertRGBA8888ToRAW16((u8*)frame->pixels, raw16_buffer1.data(), c_width, c_height); + frame_data->pFramePointerList[0][0] = raw16_buffer1.data(); + break; + case ORBIS_CAMERA_FORMAT_RAW8: + ConvertRGBA8888ToRAW8((u8*)frame->pixels, raw8_buffer1.data(), c_width, c_height); + frame_data->pFramePointerList[0][0] = raw8_buffer1.data(); + break; + default: + UNREACHABLE(); + } + switch (output_config1.format.formatLevel0) { + case ORBIS_CAMERA_FORMAT_YUV422: + frame_data->pFramePointerList[1][0] = frame->pixels; + break; + case ORBIS_CAMERA_FORMAT_RAW16: + ConvertRGBA8888ToRAW16((u8*)frame->pixels, raw16_buffer2.data(), c_width, c_height); + frame_data->pFramePointerList[1][0] = raw16_buffer2.data(); + break; + case ORBIS_CAMERA_FORMAT_RAW8: + ConvertRGBA8888ToRAW8((u8*)frame->pixels, raw8_buffer2.data(), c_width, c_height); + frame_data->pFramePointerList[1][0] = raw8_buffer2.data(); + break; + default: + UNREACHABLE(); + } + frame_data->meta.format[0][0] = output_config0.format.formatLevel0; + frame_data->meta.format[1][0] = output_config1.format.formatLevel0; + return ORBIS_OK; } s32 PS4_SYSV_ABI sceCameraGetGamma(s32 handle, OrbisCameraChannel channel, OrbisCameraGamma* gamma, @@ -499,7 +619,7 @@ s32 PS4_SYSV_ABI sceCameraIsAttached(s32 index) { return ORBIS_CAMERA_ERROR_PARAM; } // 0 = disconnected, 1 = connected - return 0; + return EmulatorSettings.GetCameraId() == -1 ? 0 : 1; } s32 PS4_SYSV_ABI sceCameraIsConfigChangeDone() { @@ -516,16 +636,16 @@ s32 PS4_SYSV_ABI sceCameraIsValidFrameData(s32 handle, OrbisCameraFrameData* fra return ORBIS_CAMERA_ERROR_NOT_OPEN; } - return ORBIS_OK; + return 1; // valid } s32 PS4_SYSV_ABI sceCameraOpen(Libraries::UserService::OrbisUserServiceUserId user_id, s32 type, s32 index, OrbisCameraOpenParameter* param) { + LOG_INFO(Lib_Camera, "called"); if (user_id != Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_SYSTEM || type != 0 || index != 0) { return ORBIS_CAMERA_ERROR_PARAM; } - LOG_WARNING(Lib_Camera, "Cameras are not supported yet"); g_library_opened = true; return ++g_handles; @@ -609,15 +729,44 @@ s32 PS4_SYSV_ABI sceCameraSetCalibData() { } s32 PS4_SYSV_ABI sceCameraSetConfig(s32 handle, OrbisCameraConfig* config) { - LOG_DEBUG(Lib_Camera, "called"); + LOG_INFO(Lib_Camera, "called"); if (handle < 1 || config == nullptr || config->sizeThis != sizeof(OrbisCameraConfig)) { return ORBIS_CAMERA_ERROR_PARAM; } if (!g_library_opened) { return ORBIS_CAMERA_ERROR_NOT_OPEN; } + if (EmulatorSettings.GetCameraId() == -1) { + return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + } - return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + switch (config->configType) { + case ORBIS_CAMERA_CONFIG_TYPE1: + case ORBIS_CAMERA_CONFIG_TYPE2: + case ORBIS_CAMERA_CONFIG_TYPE3: + case ORBIS_CAMERA_CONFIG_TYPE4: + output_config0 = camera_config_types[config->configType - 1][0]; + output_config1 = camera_config_types[config->configType - 1][1]; + break; + case ORBIS_CAMERA_CONFIG_TYPE5: + int sdk_ver; + Libraries::Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver); + if (sdk_ver < Common::ElfInfo::FW_45) { + return ORBIS_CAMERA_ERROR_UNKNOWN_CONFIG; + } + output_config0 = camera_config_types[config->configType - 1][0]; + output_config1 = camera_config_types[config->configType - 1][1]; + break; + case ORBIS_CAMERA_CONFIG_EXTENTION: + output_config0 = config->configExtention[0]; + output_config1 = config->configExtention[1]; + break; + default: + LOG_ERROR(Lib_Camera, "Invalid config type {}", std::to_underlying(config->configType)); + return ORBIS_CAMERA_ERROR_PARAM; + } + + return ORBIS_OK; } s32 PS4_SYSV_ABI sceCameraSetConfigInternal(s32 handle, OrbisCameraConfig* config) { @@ -851,7 +1000,7 @@ s32 PS4_SYSV_ABI sceCameraSetWhiteBalance(s32 handle, OrbisCameraChannel channel } s32 PS4_SYSV_ABI sceCameraStart(s32 handle, OrbisCameraStartParameter* param) { - LOG_DEBUG(Lib_Camera, "called"); + LOG_INFO(Lib_Camera, "called"); if (handle < 1 || param == nullptr || param->sizeThis != sizeof(OrbisCameraStartParameter)) { return ORBIS_CAMERA_ERROR_PARAM; } @@ -864,6 +1013,79 @@ s32 PS4_SYSV_ABI sceCameraStart(s32 handle, OrbisCameraStartParameter* param) { return ORBIS_CAMERA_ERROR_FORMAT_UNKNOWN; } + if (param->formatLevel[0] > 1 || param->formatLevel[1] > 1) { + LOG_ERROR(Lib_Camera, "Downscaled image retrieval isn't supported yet!"); + } + + SDL_CameraID* devices = NULL; + int devcount = 0; + devices = SDL_GetCameras(&devcount); + if (devices == NULL) { + LOG_ERROR(Lib_Camera, "Couldn't enumerate camera devices: {}", SDL_GetError()); + return ORBIS_CAMERA_ERROR_FATAL; + } else if (devcount == 0) { + LOG_INFO(Lib_Camera, "No camera devices connected"); + return ORBIS_CAMERA_ERROR_NOT_CONNECTED; + } + raw8_buffer1.resize(c_width * c_height); + raw16_buffer1.resize(c_width * c_height); + raw8_buffer2.resize(c_width * c_height); + raw16_buffer2.resize(c_width * c_height); + SDL_CameraSpec cam_spec{}; + switch (output_config0.format.formatLevel0) { + case ORBIS_CAMERA_FORMAT_YUV422: + cam_spec.format = SDL_PIXELFORMAT_YUY2; + break; + case ORBIS_CAMERA_FORMAT_RAW8: + cam_spec.format = SDL_PIXELFORMAT_RGBA8888; // to be swizzled + break; + case ORBIS_CAMERA_FORMAT_RAW16: + cam_spec.format = SDL_PIXELFORMAT_RGBA8888; // to be swizzled + break; + + default: + LOG_ERROR(Lib_Camera, "Invalid format {}", + std::to_underlying(output_config0.format.formatLevel0)); + break; + } + cam_spec.height = c_height; + cam_spec.width = c_width; + cam_spec.framerate_numerator = 60; + cam_spec.framerate_denominator = 1; + sdl_camera = SDL_OpenCamera(devices[EmulatorSettings.GetCameraId()], &cam_spec); + LOG_INFO(Lib_Camera, "SDL backend in use: {}", SDL_GetCurrentCameraDriver()); + char const* camera_name = SDL_GetCameraName(devices[EmulatorSettings.GetCameraId()]); + if (camera_name) + LOG_INFO(Lib_Camera, "SDL camera name: {}", camera_name); + SDL_CameraSpec spec; + SDL_GetCameraFormat(sdl_camera, &spec); + LOG_INFO(Lib_Camera, "SDL camera format: {:#x}", std::to_underlying(spec.format)); + LOG_INFO(Lib_Camera, "SDL camera framerate: {}", + (float)spec.framerate_numerator / (float)spec.framerate_denominator); + LOG_INFO(Lib_Camera, "SDL camera dimensions: {}x{}", spec.width, spec.height); + + SDL_free(devices); + + // "warm up" the device, as recommended by SDL + u64 timestamp; + SDL_Surface* frame = nullptr; + frame = SDL_AcquireCameraFrame(sdl_camera, ×tamp); + if (!frame) { + for (int i = 0; i < 1000; i++) { + frame = SDL_AcquireCameraFrame(sdl_camera, ×tamp); + if (frame) { + SDL_ReleaseCameraFrame(sdl_camera, frame); + break; + } + std::this_thread::sleep_for(std::chrono::nanoseconds(10)); + } + } + + if (!sdl_camera) { + LOG_ERROR(Lib_Camera, "Failed to open camera: {}", SDL_GetError()); + return ORBIS_CAMERA_ERROR_FATAL; + } + return ORBIS_OK; } diff --git a/src/core/libraries/camera/camera.h b/src/core/libraries/camera/camera.h index 86bb12135..eba7f4720 100644 --- a/src/core/libraries/camera/camera.h +++ b/src/core/libraries/camera/camera.h @@ -102,6 +102,123 @@ struct OrbisCameraConfig { OrbisCameraConfigExtention configExtention[ORBIS_CAMERA_MAX_DEVICE_NUM]; }; +constexpr OrbisCameraConfigExtention camera_config_types[5][ORBIS_CAMERA_MAX_DEVICE_NUM]{ + { + // type 1 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }, + { + // type 2 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }, + { + // type 3 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }, + { + // type 4 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }, + { + // type 5 + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + { + .format = + { + .formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16, + .formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + .formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422, + }, + .framerate = ORBIS_CAMERA_FRAMERATE_60, + }, + }}; + enum OrbisCameraAecAgcTarget { ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_DEF = 0x00, ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_2_0 = 0x20, diff --git a/src/core/libraries/gnmdriver/gnmdriver.cpp b/src/core/libraries/gnmdriver/gnmdriver.cpp index 1993d8cd7..f8886e3ff 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.cpp +++ b/src/core/libraries/gnmdriver/gnmdriver.cpp @@ -1,17 +1,17 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "gnm_error.h" #include "gnmdriver.h" #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/elf_info.h" #include "common/logging/log.h" #include "common/slot_vector.h" #include "core/address_space.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/libraries/gnmdriver/gnm_error.h" #include "core/libraries/gnmdriver/gnmdriver_init.h" #include "core/libraries/kernel/orbis_error.h" @@ -1172,13 +1172,14 @@ bool PS4_SYSV_ABI sceGnmIsUserPaEnabled() { } int PS4_SYSV_ABI sceGnmLogicalCuIndexToPhysicalCuIndex() { - LOG_ERROR(Lib_GnmDriver, "(STUBBED) called"); + LOG_TRACE(Lib_GnmDriver, "called"); + // Not available in retail firmware return ORBIS_OK; } -int PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask() { - LOG_ERROR(Lib_GnmDriver, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask(s64, s32 logical_cu_mask) { + LOG_INFO(Lib_GnmDriver, "called, logical_cu_mask: {}", logical_cu_mask); + return logical_cu_mask; } int PS4_SYSV_ABI sceGnmLogicalTcaUnitToPhysical() { @@ -2874,7 +2875,7 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { sdk_version = 0; } - if (Config::copyGPUCmdBuffers()) { + if (EmulatorSettings.IsCopyGpuBuffers()) { liverpool->ReserveCopyBufferSpace(); } diff --git a/src/core/libraries/gnmdriver/gnmdriver.h b/src/core/libraries/gnmdriver/gnmdriver.h index 4ece58ebd..2208ca05d 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.h +++ b/src/core/libraries/gnmdriver/gnmdriver.h @@ -121,7 +121,7 @@ s32 PS4_SYSV_ABI sceGnmInsertWaitFlipDone(u32* cmdbuf, u32 size, s32 vo_handle, int PS4_SYSV_ABI sceGnmIsCoredumpValid(); bool PS4_SYSV_ABI sceGnmIsUserPaEnabled(); int PS4_SYSV_ABI sceGnmLogicalCuIndexToPhysicalCuIndex(); -int PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask(); +s32 PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask(s64, s32 logical_cu_mask); int PS4_SYSV_ABI sceGnmLogicalTcaUnitToPhysical(); int PS4_SYSV_ABI sceGnmMapComputeQueue(u32 pipe_id, u32 queue_id, VAddr ring_base_addr, u32 ring_size_dw, u32* read_ptr_addr); diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index 184343801..085e18b9f 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -87,11 +87,13 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { if (!read && !write && !rdwr) { // Start by checking for invalid flags. *__Error() = POSIX_EINVAL; + LOG_ERROR(Kernel_Fs, "Opening path {} failed, invalid flags {:#x}", raw_path, flags); return -1; } if (strlen(raw_path) > 255) { *__Error() = POSIX_ENAMETOOLONG; + LOG_ERROR(Kernel_Fs, "Opening path {} failed, path is too long", raw_path); return -1; } @@ -137,6 +139,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Error if file exists h->DeleteHandle(handle); *__Error() = POSIX_EEXIST; + LOG_ERROR(Kernel_Fs, "Creating {} failed, file already exists", raw_path); return -1; } @@ -145,6 +148,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Can't create files in a read only directory h->DeleteHandle(handle); *__Error() = POSIX_EROFS; + LOG_ERROR(Kernel_Fs, "Creating {} failed, path is read-only", raw_path); return -1; } // Create a file if it doesn't exist @@ -154,6 +158,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // If we're not creating a file, and it doesn't exist, return ENOENT h->DeleteHandle(handle); *__Error() = POSIX_ENOENT; + LOG_ERROR(Kernel_Fs, "Opening path {} failed, file does not exist", raw_path); return -1; } @@ -169,6 +174,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // This will trigger when create & directory is specified, this is expected. h->DeleteHandle(handle); *__Error() = POSIX_ENOTDIR; + LOG_ERROR(Kernel_Fs, "Opening directory {} failed, file is not a directory", raw_path); return -1; } @@ -176,6 +182,8 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Cannot open directories with any type of write access h->DeleteHandle(handle); *__Error() = POSIX_EISDIR; + LOG_ERROR(Kernel_Fs, "Opening directory {} failed, cannot open directories for writing", + raw_path); return -1; } @@ -183,6 +191,8 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Cannot open directories with truncate h->DeleteHandle(handle); *__Error() = POSIX_EISDIR; + LOG_ERROR(Kernel_Fs, "Opening directory {} failed, cannot truncate directories", + raw_path); return -1; } @@ -201,6 +211,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Can't open files with truncate flag in a read only directory h->DeleteHandle(handle); *__Error() = POSIX_EROFS; + LOG_ERROR(Kernel_Fs, "Truncating {} failed, path is read-only", raw_path); return -1; } else if (truncate) { // Open the file as read-write so we can truncate regardless of flags. @@ -219,6 +230,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Can't open files with write/read-write access in a read only directory h->DeleteHandle(handle); *__Error() = POSIX_EROFS; + LOG_ERROR(Kernel_Fs, "Opening {} for writing failed, path is read-only", raw_path); return -1; } else if (write) { if (append) { @@ -244,6 +256,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { // Open failed in platform-specific code, errno needs to be converted. h->DeleteHandle(handle); SetPosixErrno(e); + LOG_ERROR(Kernel_Fs, "Opening {} failed, error = {}", raw_path, *__Error()); return -1; } @@ -258,7 +271,6 @@ s32 PS4_SYSV_ABI posix_open(const char* filename, s32 flags, u16 mode) { s32 PS4_SYSV_ABI sceKernelOpen(const char* path, s32 flags, /* SceKernelMode*/ u16 mode) { s32 result = open(path, flags, mode); if (result < 0) { - LOG_ERROR(Kernel_Fs, "error = {}", *__Error()); return ErrnoToSceKernelError(*__Error()); } return result; diff --git a/src/core/libraries/kernel/kernel.h b/src/core/libraries/kernel/kernel.h index ce6446129..315af6b06 100644 --- a/src/core/libraries/kernel/kernel.h +++ b/src/core/libraries/kernel/kernel.h @@ -37,7 +37,7 @@ struct OrbisWrapperImpl { #define ORBIS(func) (Libraries::Kernel::OrbisWrapperImpl::wrap) -#define CURRENT_FIRMWARE_VERSION 0x13020011 +#define CURRENT_FIRMWARE_VERSION 0x13500011 s32* PS4_SYSV_ABI __Error(); diff --git a/src/core/libraries/kernel/orbis_error.h b/src/core/libraries/kernel/orbis_error.h index d19b3f3f1..6ebff0ba3 100644 --- a/src/core/libraries/kernel/orbis_error.h +++ b/src/core/libraries/kernel/orbis_error.h @@ -106,3 +106,5 @@ constexpr int ORBIS_KERNEL_ERROR_ECAPMODE = 0x8002005E; constexpr int ORBIS_KERNEL_ERROR_ENOBLK = 0x8002005F; constexpr int ORBIS_KERNEL_ERROR_EICV = 0x80020060; constexpr int ORBIS_KERNEL_ERROR_ENOPLAYGOENT = 0x80020061; +constexpr int ORBIS_KERNEL_ERROR_ESDKVERSION = 0x80020063; +constexpr int ORBIS_KERNEL_ERROR_ESTART = 0x80020064; \ No newline at end of file diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index a79da62ee..2af5aa1bf 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/elf_info.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/file_sys/fs.h" #include "core/libraries/kernel/orbis_error.h" #include "core/libraries/kernel/process.h" @@ -17,19 +17,19 @@ s32 PS4_SYSV_ABI sceKernelIsInSandbox() { } s32 PS4_SYSV_ABI sceKernelIsNeoMode() { - return Config::isNeoModeConsole() && + return EmulatorSettings.IsNeo() && Common::ElfInfo::Instance().GetPSFAttributes().support_neo_mode; } s32 PS4_SYSV_ABI sceKernelHasNeoMode() { - return Config::isNeoModeConsole(); + return EmulatorSettings.IsNeo(); } s32 PS4_SYSV_ABI sceKernelGetMainSocId() { // These hardcoded values are based on hardware observations. // Different models of PS4/PS4 Pro likely return slightly different values. LOG_DEBUG(Lib_Kernel, "called"); - if (Config::isNeoModeConsole()) { + if (EmulatorSettings.IsNeo()) { return 0x740f30; } return 0x710f10; diff --git a/src/core/libraries/kernel/threads/exception.cpp b/src/core/libraries/kernel/threads/exception.cpp index 247c387fe..e2fd032f5 100644 --- a/src/core/libraries/kernel/threads/exception.cpp +++ b/src/core/libraries/kernel/threads/exception.cpp @@ -2,7 +2,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" +#include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/kernel/posix_error.h" #include "core/libraries/kernel/threads/exception.h" #include "core/libraries/kernel/threads/pthread.h" #include "core/libraries/libs.h" @@ -13,23 +15,24 @@ #else #include #endif +#include namespace Libraries::Kernel { #ifdef _WIN32 // Windows doesn't have native versions of these, and we don't need to use them either. -static s32 NativeToOrbisSignal(s32 s) { +s32 NativeToOrbisSignal(s32 s) { return s; } -static s32 OrbisToNativeSignal(s32 s) { +s32 OrbisToNativeSignal(s32 s) { return s; } #else -static s32 NativeToOrbisSignal(s32 s) { +s32 NativeToOrbisSignal(s32 s) { switch (s) { case SIGHUP: return POSIX_SIGHUP; @@ -89,12 +92,21 @@ static s32 NativeToOrbisSignal(s32 s) { return POSIX_SIGUSR1; case SIGUSR2: return POSIX_SIGUSR2; + case _SIGEMT: + return POSIX_SIGEMT; + case _SIGINFO: + return POSIX_SIGINFO; + case 0: + return 128; default: + if (s > 0 && s < 128) { + return s; + } UNREACHABLE_MSG("Unknown signal {}", s); } } -static s32 OrbisToNativeSignal(s32 s) { +s32 OrbisToNativeSignal(s32 s) { switch (s) { case POSIX_SIGHUP: return SIGHUP; @@ -108,6 +120,8 @@ static s32 OrbisToNativeSignal(s32 s) { return SIGTRAP; case POSIX_SIGABRT: return SIGABRT; + case POSIX_SIGEMT: + return _SIGEMT; case POSIX_SIGFPE: return SIGFPE; case POSIX_SIGKILL: @@ -150,22 +164,33 @@ static s32 OrbisToNativeSignal(s32 s) { return SIGPROF; case POSIX_SIGWINCH: return SIGWINCH; + case POSIX_SIGINFO: + return _SIGINFO; case POSIX_SIGUSR1: return SIGUSR1; case POSIX_SIGUSR2: return SIGUSR2; + case 128: + return 0; default: + if (s > 0 && s < 128) { + return s; + } UNREACHABLE_MSG("Unknown signal {}", s); } } #endif -std::array Handlers{}; +#ifdef __APPLE__ +#define sigisemptyset(x) (*(x) == 0) +#endif + +std::array Handlers{}; #ifndef _WIN64 void SigactionHandler(int native_signum, siginfo_t* inf, ucontext_t* raw_context) { - const auto handler = Handlers[native_signum]; + const auto handler = Handlers[NativeToOrbisSignal(native_signum)]; if (handler) { auto ctx = Ucontext{}; #ifdef __APPLE__ @@ -214,6 +239,8 @@ void SigactionHandler(int native_signum, siginfo_t* inf, ucontext_t* raw_context ctx.uc_mcontext.mc_addr = reinterpret_cast(inf->si_addr); #endif handler(NativeToOrbisSignal(native_signum), &ctx); + } else { + UNREACHABLE_MSG("Unhandled exception"); } } #else @@ -221,7 +248,7 @@ void ExceptionHandler(void* arg1, void* arg2, void* arg3, PCONTEXT context) { const char* thrName = (char*)arg1; int native_signum = reinterpret_cast(arg2); LOG_INFO(Lib_Kernel, "Exception raised successfully on thread '{}'", thrName); - const auto handler = Handlers[native_signum]; + const auto handler = Handlers[NativeToOrbisSignal(native_signum)]; if (handler) { auto ctx = Ucontext{}; ctx.uc_mcontext.mc_r8 = context->R8; @@ -243,76 +270,105 @@ void ExceptionHandler(void* arg1, void* arg2, void* arg3, PCONTEXT context) { ctx.uc_mcontext.mc_fs = context->SegFs; ctx.uc_mcontext.mc_gs = context->SegGs; handler(NativeToOrbisSignal(native_signum), &ctx); + } else { + UNREACHABLE_MSG("Unhandled exception"); } } #endif -int PS4_SYSV_ABI sceKernelInstallExceptionHandler(s32 signum, SceKernelExceptionHandler handler) { - if (signum > POSIX_SIGUSR2) { - return ORBIS_KERNEL_ERROR_EINVAL; +s32 PS4_SYSV_ABI posix_sigemptyset(Sigset* s) { + s->bits[0] = 0; + s->bits[1] = 0; + return 0; +} + +bool PS4_SYSV_ABI posix_sigisemptyset(Sigset* s) { + return s->bits[0] == 0 && s->bits[1] == 0; +} + +s32 PS4_SYSV_ABI posix_sigaction(s32 sig, Sigaction* act, Sigaction* oact) { + if (sig < 1 || sig > 128 || sig == POSIX_SIGTHR || sig == POSIX_SIGKILL || + sig == POSIX_SIGSTOP) { + *__Error() = POSIX_EINVAL; + return ORBIS_FAIL; + } +#ifdef _WIN32 + LOG_ERROR(Lib_Kernel, "(STUBBED) called, sig: {}", sig); + Handlers[sig] = reinterpret_cast( + act ? act->__sigaction_handler.sigaction : nullptr); +#else + s32 native_sig = OrbisToNativeSignal(sig); + if (native_sig == SIGVTALRM) { + LOG_ERROR(Lib_Kernel, "Guest is attempting to use the HLE-reserved signal {}!", sig); + *__Error() = POSIX_EINVAL; + return ORBIS_FAIL; + } +#ifndef __APPLE__ + if (native_sig >= __SIGRTMIN && native_sig < SIGRTMIN) { + LOG_ERROR(Lib_Kernel, "Guest is attempting to use the HLE libc-reserved signal {}!", sig); + *__Error() = POSIX_EINVAL; + return ORBIS_FAIL; + } +#else + if (native_sig > SIGUSR2) { + LOG_ERROR(Lib_Kernel, + "Guest is attempting to use SIGRT signals, which aren't available on this " + "platform (signal: {})!", + sig); } - LOG_INFO(Lib_Kernel, "Installing signal handler for {}", signum); - int const native_signum = OrbisToNativeSignal(signum); -#ifdef __APPLE__ - ASSERT_MSG(native_signum != SIGVTALRM, "SIGVTALRM is HLE-reserved on macOS!"); #endif - ASSERT_MSG(!Handlers[native_signum], "Invalid parameters"); - Handlers[native_signum] = handler; -#ifndef _WIN64 - if (native_signum == SIGSEGV || native_signum == SIGBUS || native_signum == SIGILL) { + LOG_INFO(Lib_Kernel, "called, sig: {}, native sig: {}", sig, native_sig); + struct sigaction native_act{}; + if (act) { + native_act.sa_flags = act->sa_flags; // todo check compatibility, on Linux it seems fine + native_act.sa_sigaction = + reinterpret_cast(SigactionHandler); + if (!posix_sigisemptyset(&act->sa_mask)) { + LOG_ERROR(Lib_Kernel, "Unhandled sa_mask: {:x}", act->sa_mask.bits[0]); + } + } + auto const prev_handler = Handlers[sig]; + Handlers[sig] = reinterpret_cast( + act ? act->__sigaction_handler.sigaction : nullptr); + + if (native_sig == SIGSEGV || native_sig == SIGBUS || native_sig == SIGILL) { return ORBIS_OK; // These are handled in Core::SignalHandler } - struct sigaction act = {}; - act.sa_flags = SA_SIGINFO | SA_RESTART; - act.sa_sigaction = reinterpret_cast(SigactionHandler); - sigemptyset(&act.sa_mask); - sigaction(native_signum, &act, nullptr); -#endif - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceKernelRemoveExceptionHandler(s32 signum) { - if (signum > POSIX_SIGUSR2) { - return ORBIS_KERNEL_ERROR_EINVAL; + if (native_sig > 127) { + LOG_WARNING(Lib_Kernel, "We can't install a handler for native signal {}!", native_sig); + return ORBIS_OK; } - int const native_signum = OrbisToNativeSignal(signum); - if (!Handlers[native_signum]) { - LOG_WARNING(Lib_Kernel, "removing non-installed handler for signum {}", signum); - return ORBIS_KERNEL_ERROR_EINVAL; + struct sigaction native_oact{}; + s32 ret = sigaction(native_sig, act ? &native_act : nullptr, oact ? &native_oact : nullptr); + if (oact) { + oact->sa_flags = native_oact.sa_flags; + oact->__sigaction_handler.sigaction = + reinterpret_cast__sigaction_handler.sigaction)>(prev_handler); + if (!sigisemptyset(&native_oact.sa_mask)) { + LOG_ERROR(Lib_Kernel, "Unhandled sa_mask"); + } } - Handlers[native_signum] = nullptr; -#ifndef _WIN64 - if (native_signum == SIGSEGV || native_signum == SIGBUS || native_signum == SIGILL) { - struct sigaction action{}; - action.sa_sigaction = Core::SignalHandler; - action.sa_flags = SA_SIGINFO | SA_ONSTACK; - sigemptyset(&action.sa_mask); - - ASSERT_MSG(sigaction(native_signum, &action, nullptr) == 0, - "Failed to reinstate original signal handler for signal {}", native_signum); - } else { - struct sigaction act = {}; - act.sa_flags = SA_SIGINFO | SA_RESTART; - act.sa_sigaction = nullptr; - sigemptyset(&act.sa_mask); - sigaction(native_signum, &act, nullptr); + if (ret < 0) { + LOG_ERROR(Lib_Kernel, "sigaction failed: {}", strerror(errno)); + *__Error() = ErrnoToSceKernelError(errno); + return ORBIS_FAIL; } #endif return ORBIS_OK; } -int PS4_SYSV_ABI sceKernelRaiseException(PthreadT thread, int signum) { - if (signum != POSIX_SIGUSR1) { - return ORBIS_KERNEL_ERROR_EINVAL; +s32 PS4_SYSV_ABI posix_pthread_kill(PthreadT thread, s32 sig) { + if (sig < 1 || sig > 128) { // off-by-one error? + return POSIX_EINVAL; } - LOG_WARNING(Lib_Kernel, "Raising exception on thread '{}'", thread->name); - int const native_signum = OrbisToNativeSignal(signum); + LOG_WARNING(Lib_Kernel, "Raising signal {} on thread '{}'", sig, thread->name); + int const native_signum = OrbisToNativeSignal(sig); #ifndef _WIN64 const auto pthr = reinterpret_cast(thread->native_thr.GetHandle()); const auto ret = pthread_kill(pthr, native_signum); if (ret != 0) { LOG_ERROR(Kernel, "Failed to send exception signal to thread '{}': {}", thread->name, - strerror(ret)); + strerror(errno)); } #else USER_APC_OPTION option; @@ -326,6 +382,67 @@ int PS4_SYSV_ABI sceKernelRaiseException(PthreadT thread, int signum) { return ORBIS_OK; } +// libkernel has a check in sceKernelInstallExceptionHandler and sceKernelRemoveExceptionHandler for +// validating if the application requested a handler for an allowed signal or not. However, that is +// just a wrapper for sigaction, which itself does not have any such restrictions, and therefore +// this check is ridiculously trivial to go around. This, however, means that we need to support all +// 127 - 3 possible signals, even if realistically, only homebrew will use most of them. +static std::unordered_set orbis_allowed_signals{ + POSIX_SIGHUP, POSIX_SIGILL, POSIX_SIGFPE, POSIX_SIGBUS, POSIX_SIGSEGV, POSIX_SIGUSR1, +}; + +int PS4_SYSV_ABI sceKernelInstallExceptionHandler(s32 signum, OrbisKernelExceptionHandler handler) { + if (!orbis_allowed_signals.contains(signum)) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + if (Handlers[signum] != nullptr) { + return ORBIS_KERNEL_ERROR_EAGAIN; + } + LOG_INFO(Lib_Kernel, "Installing signal handler for {}", signum); + Sigaction act = {}; + act.sa_flags = POSIX_SA_SIGINFO | POSIX_SA_RESTART; + act.__sigaction_handler.sigaction = + reinterpret_cast(handler); + posix_sigemptyset(&act.sa_mask); + s32 ret = posix_sigaction(signum, &act, nullptr); + if (ret < 0) { + LOG_ERROR(Lib_Kernel, "Failed to add handler for signal {}: {}", signum, + strerror(*__Error())); + return ErrnoToSceKernelError(*__Error()); + } + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceKernelRemoveExceptionHandler(s32 signum) { + if (!orbis_allowed_signals.contains(signum)) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + int const native_signum = OrbisToNativeSignal(signum); + Handlers[signum] = nullptr; + Sigaction act = {}; + act.sa_flags = POSIX_SA_SIGINFO; + act.__sigaction_handler.sigaction = nullptr; + posix_sigemptyset(&act.sa_mask); + s32 ret = posix_sigaction(signum, &act, nullptr); + if (ret < 0) { + LOG_ERROR(Lib_Kernel, "Failed to remove handler for signal {}: {}", signum, + strerror(*__Error())); + return ErrnoToSceKernelError(*__Error()); + } + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceKernelRaiseException(PthreadT thread, int signum) { + if (signum != POSIX_SIGUSR1) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + s32 ret = posix_pthread_kill(thread, signum); + if (ret < 0) { + return ErrnoToSceKernelError(ret); + } + return ret; +} + s32 PS4_SYSV_ABI sceKernelDebugRaiseException(s32 error, s64 unk) { if (unk != 0) { return ORBIS_KERNEL_ERROR_EINVAL; @@ -352,6 +469,13 @@ void RegisterException(Core::Loader::SymbolsResolver* sym) { sceKernelDebugRaiseExceptionOnReleaseMode); LIB_FUNCTION("WkwEd3N7w0Y", "libkernel", 1, "libkernel", sceKernelInstallExceptionHandler); LIB_FUNCTION("Qhv5ARAoOEc", "libkernel", 1, "libkernel", sceKernelRemoveExceptionHandler); + + LIB_FUNCTION("KiJEPEWRyUY", "libkernel", 1, "libkernel", posix_sigaction); + LIB_FUNCTION("+F7C-hdk7+E", "libkernel", 1, "libkernel", posix_sigemptyset); + LIB_FUNCTION("yH-uQW3LbX0", "libkernel", 1, "libkernel", posix_pthread_kill); + LIB_FUNCTION("KiJEPEWRyUY", "libScePosix", 1, "libkernel", posix_sigaction); + LIB_FUNCTION("+F7C-hdk7+E", "libScePosix", 1, "libkernel", posix_sigemptyset); + LIB_FUNCTION("yH-uQW3LbX0", "libScePosix", 1, "libkernel", posix_pthread_kill); } } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/threads/exception.h b/src/core/libraries/kernel/threads/exception.h index 42c92ab2e..c07242c1d 100644 --- a/src/core/libraries/kernel/threads/exception.h +++ b/src/core/libraries/kernel/threads/exception.h @@ -11,7 +11,7 @@ class SymbolsResolver; namespace Libraries::Kernel { -using SceKernelExceptionHandler = PS4_SYSV_ABI void (*)(int, void*); +using OrbisKernelExceptionHandler = PS4_SYSV_ABI void (*)(int, void*); constexpr s32 POSIX_SIGHUP = 1; constexpr s32 POSIX_SIGINT = 2; @@ -47,6 +47,23 @@ constexpr s32 POSIX_SIGUSR2 = 31; constexpr s32 POSIX_SIGTHR = 32; constexpr s32 POSIX_SIGLIBRT = 33; +#ifdef __linux__ +constexpr s32 _SIGEMT = 128; +constexpr s32 _SIGINFO = 129; +#elif !defined(_WIN32) +constexpr s32 _SIGEMT = SIGEMT; +constexpr s32 _SIGINFO = SIGINFO; +#endif + +constexpr s32 POSIX_SA_NOCLDSTOP = 1; +constexpr s32 POSIX_SA_NOCLDWAIT = 2; +constexpr s32 POSIX_SA_SIGINFO = 4; +constexpr s32 POSIX_SA_ONSTACK = 0x08000000; +constexpr s32 POSIX_SA_RESTART = 0x10000000; +constexpr s32 POSIX_SA_NODEFER = 0x40000000; +constexpr s32 POSIX_SA_RESETHAND = 0x80000000; +constexpr s32 POSIX_SA_RESTORER = 0x04000000; + struct Mcontext { u64 mc_onstack; u64 mc_rdi; @@ -101,17 +118,74 @@ struct Sigset { u64 bits[2]; }; +union Sigval { + /* Members as suggested by Annex C of POSIX 1003.1b. */ + int sival_int; + void* sival_ptr; + /* 6.0 compatibility */ + int sigval_int; + void* sigval_ptr; +}; + +struct Siginfo { + int _si_signo; /* signal number */ + int _si_errno; /* errno association */ + /* + * Cause of signal, one of the SI_ macros or signal-specific + * values, i.e. one of the FPE_... values for SIGFPE. This + * value is equivalent to the second argument to an old-style + * FreeBSD signal handler. + */ + int _si_code; /* signal code */ + s32 _si_pid; /* sending process */ + u32 _si_uid; /* sender's ruid */ + int _si_status; /* exit value */ + void* _si_addr; /* faulting instruction */ + union Sigval _si_value; /* signal value */ + union { + struct { + int _trapno; /* machine specific trap code */ + } _fault; + struct { + int _timerid; + int _overrun; + } _timer; + struct { + int _mqd; + } _mesgq; + struct { + long _band; /* band event for SIGPOLL */ + } _poll; /* was this ever used ? */ + struct { + long __spare1__; + int __spare2__[7]; + } __spare__; + } _reason; +}; + +struct Sigaction { + union { + void (*handler)(int); + void (*sigaction)(int, struct Siginfo*, void*); + } __sigaction_handler; + int sa_flags; + Sigset sa_mask; +}; + struct Ucontext { struct Sigset uc_sigmask; int field1_0x10[12]; - struct Mcontext uc_mcontext; - struct Ucontext* uc_link; - struct ExStack uc_stack; + Mcontext uc_mcontext; + Ucontext* uc_link; + ExStack uc_stack; int uc_flags; int __spare[4]; int field7_0x4f4[3]; }; +s32 NativeToOrbisSignal(s32 s); +s32 OrbisToNativeSignal(s32 s); + void RegisterException(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index da9e1600f..3742db5cf 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -242,7 +242,7 @@ int PS4_SYSV_ABI posix_pthread_create_name_np(PthreadT* thread, const PthreadAtt new_thread->attr.sched_policy = curthread->attr.sched_policy; } - static int TidCounter = 1; + static std::atomic TidCounter = 1; new_thread->tid = ++TidCounter; if (new_thread->attr.stackaddr_attr == nullptr) { @@ -665,6 +665,7 @@ void RegisterThread(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("Z4QosVuAsA0", "libkernel", 1, "libkernel", posix_pthread_once); LIB_FUNCTION("EotR8a3ASf4", "libkernel", 1, "libkernel", posix_pthread_self); LIB_FUNCTION("OxhIB8LB-PQ", "libkernel", 1, "libkernel", posix_pthread_create); + LIB_FUNCTION("Jmi+9w9u0E4", "libkernel", 1, "libkernel", posix_pthread_create_name_np); LIB_FUNCTION("lZzFeSxPl08", "libkernel", 1, "libkernel", posix_pthread_setcancelstate); LIB_FUNCTION("CBNtXOoef-E", "libkernel", 1, "libkernel", posix_sched_get_priority_max); LIB_FUNCTION("m0iS6jNsXds", "libkernel", 1, "libkernel", posix_sched_get_priority_min); diff --git a/src/core/libraries/libc_internal/libc_internal_io.cpp b/src/core/libraries/libc_internal/libc_internal_io.cpp index 504ba5b48..0bb23eb78 100644 --- a/src/core/libraries/libc_internal/libc_internal_io.cpp +++ b/src/core/libraries/libc_internal/libc_internal_io.cpp @@ -199,7 +199,11 @@ OrbisFILE* PS4_SYSV_ABI internal_fopen(const char* path, const char* mode) { std::scoped_lock lk{g_file_mtx}; LOG_INFO(Lib_LibcInternal, "called, path {}, mode {}", path, mode); OrbisFILE* file = internal__Fofind(); - return internal__Foprep(path, mode, file, -1, 0, 0); + OrbisFILE* ret_file = internal__Foprep(path, mode, file, -1, 0, 0); + if (ret_file == nullptr) { + LOG_ERROR(Lib_LibcInternal, "failed to open file {}", path); + } + return ret_file; } s32 PS4_SYSV_ABI internal_fflush(OrbisFILE* file) { diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index eebb991dc..6db1ba18d 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -1,12 +1,12 @@ -// 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/config.h" #include "core/libraries/ajm/ajm.h" #include "core/libraries/app_content/app_content.h" #include "core/libraries/audio/audioin.h" #include "core/libraries/audio/audioout.h" #include "core/libraries/audio3d/audio3d.h" +#include "core/libraries/audio3d/audio3d_openal.h" #include "core/libraries/avplayer/avplayer.h" #include "core/libraries/camera/camera.h" #include "core/libraries/companion/companion_httpd.h" @@ -57,10 +57,10 @@ #include "core/libraries/screenshot/screenshot.h" #include "core/libraries/share_play/shareplay.h" #include "core/libraries/signin_dialog/signindialog.h" +#include "core/libraries/sysmodule/sysmodule.h" #include "core/libraries/system/commondialog.h" #include "core/libraries/system/msgdialog.h" #include "core/libraries/system/posix.h" -#include "core/libraries/system/sysmodule.h" #include "core/libraries/system/systemservice.h" #include "core/libraries/system/userservice.h" #include "core/libraries/system_gesture/system_gesture.h" @@ -127,7 +127,11 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::AvPlayer::RegisterLib(sym); Libraries::Videodec::RegisterLib(sym); Libraries::Videodec2::RegisterLib(sym); - Libraries::Audio3d::RegisterLib(sym); + if (EmulatorSettings.GetAudioBackend() == AudioBackend::OpenAL) { + Libraries::Audio3dOpenAL::RegisterLib(sym); + } else { + Libraries::Audio3d::RegisterLib(sym); + } Libraries::Ime::RegisterLib(sym); Libraries::GameLiveStreaming::RegisterLib(sym); Libraries::SharePlay::RegisterLib(sym); diff --git a/src/core/libraries/network/net.cpp b/src/core/libraries/network/net.cpp index ca75ad394..6bf4764c4 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -1447,7 +1447,7 @@ int PS4_SYSV_ABI sceNetResolverStartNtoa(OrbisNetId resolverid, const char* host return ORBIS_NET_ERROR_EBADF; } - if (!Config::getIsConnectedToNetwork()) { + if (!EmulatorSettings.IsConnectedToNetwork()) { *sceNetErrnoLoc() = ORBIS_NET_RESOLVER_ENODNS; file->resolver->resolution_error = ORBIS_NET_ERROR_RESOLVER_ENODNS; return ORBIS_NET_ERROR_RESOLVER_ENODNS; diff --git a/src/core/libraries/network/net_ctl_obj.cpp b/src/core/libraries/network/net_ctl_obj.cpp index a4081cd11..5eb6403c2 100644 --- a/src/core/libraries/network/net_ctl_obj.cpp +++ b/src/core/libraries/network/net_ctl_obj.cpp @@ -1,9 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/network/net_ctl_codes.h" #include "core/libraries/network/net_ctl_obj.h" #include "core/tls.h" @@ -46,8 +46,9 @@ s32 NetCtlInternal::RegisterNpToolkitCallback(OrbisNetCtlCallbackForNpToolkit fu void NetCtlInternal::CheckCallback() { std::scoped_lock lock{m_mutex}; - const auto event = Config::getIsConnectedToNetwork() ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED - : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; + const auto event = EmulatorSettings.IsConnectedToNetwork() + ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED + : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; for (const auto [func, arg] : callbacks) { if (func != nullptr) { func(event, arg); @@ -57,8 +58,9 @@ void NetCtlInternal::CheckCallback() { void NetCtlInternal::CheckNpToolkitCallback() { std::scoped_lock lock{m_mutex}; - const auto event = Config::getIsConnectedToNetwork() ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED - : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; + const auto event = EmulatorSettings.IsConnectedToNetwork() + ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED + : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; for (const auto [func, arg] : nptool_callbacks) { if (func != nullptr) { func(event, arg); diff --git a/src/core/libraries/network/net_resolver.cpp b/src/core/libraries/network/net_resolver.cpp index 7eb4c4001..a66f1519b 100644 --- a/src/core/libraries/network/net_resolver.cpp +++ b/src/core/libraries/network/net_resolver.cpp @@ -2,9 +2,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" -#include "common/config.h" #include "common/singleton.h" #include "common/types.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "net_error.h" #include "net_resolver.h" @@ -27,7 +27,7 @@ int Resolver::ResolveAsync(const char* hostname, OrbisNetInAddr* addr, int timeo } void Resolver::Resolve() { - if (!Config::getIsConnectedToNetwork()) { + if (!EmulatorSettings.IsConnectedToNetwork()) { resolution_error = ORBIS_NET_ERROR_RESOLVER_ENODNS; return; } diff --git a/src/core/libraries/network/netctl.cpp b/src/core/libraries/network/netctl.cpp index 8d60d3627..136d63810 100644 --- a/src/core/libraries/network/netctl.cpp +++ b/src/core/libraries/network/netctl.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 #ifdef WIN32 @@ -13,8 +13,8 @@ #endif #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/network/net_ctl_codes.h" @@ -162,7 +162,7 @@ int PS4_SYSV_ABI sceNetCtlGetIfStat() { int PS4_SYSV_ABI sceNetCtlGetInfo(int code, OrbisNetCtlInfo* info) { LOG_DEBUG(Lib_NetCtl, "code = {}", code); - if (!Config::getIsConnectedToNetwork()) { + if (!EmulatorSettings.IsConnectedToNetwork()) { return ORBIS_NET_CTL_ERROR_NOT_CONNECTED; } @@ -180,8 +180,8 @@ int PS4_SYSV_ABI sceNetCtlGetInfo(int code, OrbisNetCtlInfo* info) { info->mtu = 1500; // default value break; case ORBIS_NET_CTL_INFO_LINK: - info->link = Config::getIsConnectedToNetwork() ? ORBIS_NET_CTL_LINK_CONNECTED - : ORBIS_NET_CTL_LINK_DISCONNECTED; + info->link = EmulatorSettings.IsConnectedToNetwork() ? ORBIS_NET_CTL_LINK_CONNECTED + : ORBIS_NET_CTL_LINK_DISCONNECTED; break; case ORBIS_NET_CTL_INFO_IP_ADDRESS: { strcpy(info->ip_address, @@ -318,7 +318,7 @@ int PS4_SYSV_ABI sceNetCtlGetScanInfoForSsidScanIpcInt() { } int PS4_SYSV_ABI sceNetCtlGetState(int* state) { - const auto connected = Config::getIsConnectedToNetwork(); + const auto connected = EmulatorSettings.IsConnectedToNetwork(); LOG_DEBUG(Lib_NetCtl, "connected = {}", connected); const auto current_state = connected ? ORBIS_NET_CTL_STATE_IPOBTAINED : ORBIS_NET_CTL_STATE_DISCONNECTED; diff --git a/src/core/libraries/network/ssl2.cpp b/src/core/libraries/network/ssl2.cpp index 0b408d094..3a7fd71e5 100644 --- a/src/core/libraries/network/ssl2.cpp +++ b/src/core/libraries/network/ssl2.cpp @@ -114,7 +114,13 @@ int PS4_SYSV_ABI sceSslFreeCaCerts(s32 ssl_ctx_id, OrbisSslCaCerts* certs) { if (certs == nullptr) { return ORBIS_SSL_ERROR_INVALID_ARGUMENT; } - delete (certs->certs); + if (certs->certs != nullptr) { + for (s32 data = 0; data < certs->num; data++) { + free(certs->certs[data].ptr); + } + delete (certs->certs); + } + // delete (certs->pool); return ORBIS_OK; } @@ -139,7 +145,12 @@ int PS4_SYSV_ABI sceSslGetCaCerts(s32 ssl_ctx_id, OrbisSslCaCerts* certs) { if (certs == nullptr) { return ORBIS_SSL_ERROR_INVALID_ARGUMENT; } - certs->certs = new OrbisSslData{nullptr, 0}; + // Allocate a buffer to store dummy data in. + const char* dummy_data = "dummy"; + u64 dummy_length = strlen(dummy_data) + 1; + char* data = static_cast(malloc(dummy_length)); + strncpy(data, dummy_data, dummy_length); + certs->certs = new OrbisSslData{data, dummy_length}; certs->num = 1; certs->pool = nullptr; return ORBIS_OK; diff --git a/src/core/libraries/np/np_auth.cpp b/src/core/libraries/np/np_auth.cpp index b6091723c..a9c2181b9 100644 --- a/src/core/libraries/np/np_auth.cpp +++ b/src/core/libraries/np/np_auth.cpp @@ -1,9 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_auth.h" @@ -363,7 +363,7 @@ s32 PS4_SYSV_ABI sceNpAuthDeleteRequest(s32 req_id) { } void RegisterLib(Core::Loader::SymbolsResolver* sym) { - g_signed_in = Config::getPSNSignedIn(); + g_signed_in = EmulatorSettings.IsPSNSignedIn(); LIB_FUNCTION("6bwFkosYRQg", "libSceNpAuth", 1, "libSceNpAuth", sceNpAuthCreateRequest); LIB_FUNCTION("N+mr7GjTvr8", "libSceNpAuth", 1, "libSceNpAuth", sceNpAuthCreateAsyncRequest); diff --git a/src/core/libraries/np/np_manager.cpp b/src/core/libraries/np/np_manager.cpp index 229ae33af..0ffbb682a 100644 --- a/src/core/libraries/np/np_manager.cpp +++ b/src/core/libraries/np/np_manager.cpp @@ -1,12 +1,13 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include -#include "common/config.h" +#include #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" @@ -631,7 +632,8 @@ s32 PS4_SYSV_ABI sceNpGetNpId(Libraries::UserService::OrbisUserServiceUserId use return ORBIS_NP_ERROR_SIGNED_OUT; } memset(np_id, 0, sizeof(OrbisNpId)); - strncpy(np_id->handle.data, Config::getUserName().c_str(), sizeof(np_id->handle.data)); + strncpy(np_id->handle.data, UserManagement.GetDefaultUser().user_name.c_str(), + sizeof(np_id->handle.data)); return ORBIS_OK; } @@ -645,7 +647,8 @@ s32 PS4_SYSV_ABI sceNpGetOnlineId(Libraries::UserService::OrbisUserServiceUserId return ORBIS_NP_ERROR_SIGNED_OUT; } memset(online_id, 0, sizeof(OrbisNpOnlineId)); - strncpy(online_id->data, Config::getUserName().c_str(), sizeof(online_id->data)); + strncpy(online_id->data, UserManagement.GetDefaultUser().user_name.c_str(), + sizeof(online_id->data)); return ORBIS_OK; } @@ -784,7 +787,7 @@ void DeregisterNpCallback(std::string key) { } void RegisterLib(Core::Loader::SymbolsResolver* sym) { - g_signed_in = Config::getPSNSignedIn(); + g_signed_in = EmulatorSettings.IsPSNSignedIn(); LIB_FUNCTION("GpLQDNKICac", "libSceNpManager", 1, "libSceNpManager", sceNpCreateRequest); LIB_FUNCTION("eiqMCt9UshI", "libSceNpManager", 1, "libSceNpManager", sceNpCreateAsyncRequest); diff --git a/src/core/libraries/np/np_matching2.cpp b/src/core/libraries/np/np_matching2.cpp index 423b84257..dcd2a9c23 100644 --- a/src/core/libraries/np/np_matching2.cpp +++ b/src/core/libraries/np/np_matching2.cpp @@ -4,8 +4,8 @@ #include #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_manager.h" @@ -376,7 +376,7 @@ int PS4_SYSV_ABI sceNpMatching2ContextStart(OrbisNpMatching2ContextId ctxId, u64 } std::scoped_lock lk{g_events_mutex}; - if (Config::getIsConnectedToNetwork() && Config::getPSNSignedIn()) { + if (EmulatorSettings.IsConnectedToNetwork() && EmulatorSettings.IsPSNSignedIn()) { g_ctx_events.emplace_back(ctxId, ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED, ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ACTION, 0); } else { diff --git a/src/core/libraries/np/np_trophy.cpp b/src/core/libraries/np/np_trophy.cpp index 976d614c0..287d8d295 100644 --- a/src/core/libraries/np/np_trophy.cpp +++ b/src/core/libraries/np/np_trophy.cpp @@ -1,22 +1,118 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include +#include "common/elf_info.h" #include "common/logging/log.h" #include "common/path_util.h" #include "common/slot_vector.h" +#include "core/emulator_settings.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" #include "core/libraries/np/np_trophy.h" #include "core/libraries/np/np_trophy_error.h" #include "core/libraries/np/trophy_ui.h" +#include "core/libraries/system/userservice.h" #include "core/memory.h" namespace Libraries::Np::NpTrophy { -std::string game_serial; +// PS4 system language IDs map directly to TROP00.XML .. TROP30.XML. +// Index = OrbisSystemServiceParamId language value reported by the system. +// clang-format off +static constexpr std::array s_language_xml_names = { + "TROP_00.XML", // 00 Japanese + "TROP_01.XML", // 01 English (US) + "TROP_02.XML", // 02 French + "TROP_03.XML", // 03 Spanish (ES) + "TROP_04.XML", // 04 German + "TROP_05.XML", // 05 Italian + "TROP_06.XML", // 06 Dutch + "TROP_07.XML", // 07 Portuguese (PT) + "TROP_08.XML", // 08 Russian + "TROP_09.XML", // 09 Korean + "TROP_10.XML", // 10 Traditional Chinese + "TROP_11.XML", // 11 Simplified Chinese + "TROP_12.XML", // 12 Finnish + "TROP_13.XML", // 13 Swedish + "TROP_14.XML", // 14 Danish + "TROP_15.XML", // 15 Norwegian + "TROP_16.XML", // 16 Polish + "TROP_17.XML", // 17 Portuguese (BR) + "TROP_18.XML", // 18 English (GB) + "TROP_19.XML", // 19 Turkish + "TROP_20.XML", // 20 Spanish (LA) + "TROP_21.XML", // 21 Arabic + "TROP_22.XML", // 22 French (CA) + "TROP_23.XML", // 23 Czech + "TROP_24.XML", // 24 Hungarian + "TROP_25.XML", // 25 Greek + "TROP_26.XML", // 26 Romanian + "TROP_27.XML", // 27 Thai + "TROP_28.XML", // 28 Vietnamese + "TROP_29.XML", // 29 Indonesian + "TROP_30.XML", // 30 Unkrainian +}; +// clang-format on + +// Returns the best available trophy XML path for the current system language. +// Resolution order: +// 1. TROP_XX.XML for the active system language (e.g. TROP01.XML for English) +// 2. TROP.XML (master / language-neutral fallback) +static std::filesystem::path GetTrophyXmlPath(const std::filesystem::path& xml_dir, + int system_language) { + // Try the exact language file first. + if (system_language >= 0 && system_language < static_cast(s_language_xml_names.size())) { + auto lang_path = xml_dir / s_language_xml_names[system_language]; + if (std::filesystem::exists(lang_path)) { + return lang_path; + } + } + // Final fallback: master TROP.XML (always present). + return xml_dir / "TROP.XML"; +} + +static void ApplyUnlockToXmlFile(const std::filesystem::path& xml_path, OrbisNpTrophyId trophyId, + u64 trophyTimestamp, bool unlock_platinum, + OrbisNpTrophyId platinumId, u64 platinumTimestamp) { + pugi::xml_document doc; + if (!doc.load_file(xml_path.native().c_str())) { + LOG_WARNING(Lib_NpTrophy, "ApplyUnlock: failed to load {}", xml_path.string()); + return; + } + + auto trophyconf = doc.child("trophyconf"); + for (pugi::xml_node& node : trophyconf.children()) { + if (std::string_view(node.name()) != "trophy") { + continue; + } + int id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + + auto set_unlock = [&](u64 ts) { + if (node.attribute("unlockstate").empty()) { + node.append_attribute("unlockstate") = "true"; + } else { + node.attribute("unlockstate").set_value("true"); + } + const auto ts_str = std::to_string(ts); + if (node.attribute("timestamp").empty()) { + node.append_attribute("timestamp") = ts_str.c_str(); + } else { + node.attribute("timestamp").set_value(ts_str.c_str()); + } + }; + + if (id == trophyId) { + set_unlock(trophyTimestamp); + } else if (unlock_platinum && id == platinumId) { + set_unlock(platinumTimestamp); + } + } + + doc.save_file(xml_path.native().c_str()); +} static constexpr auto MaxTrophyHandles = 4u; static constexpr auto MaxTrophyContexts = 8u; @@ -30,6 +126,11 @@ struct ContextKeyHash { struct TrophyContext { u32 context_id; + bool registered = false; + std::filesystem::path trophy_xml_path; // resolved once at CreateContext + std::filesystem::path xml_dir; // .../Xml/ + std::filesystem::path xml_save_file; // The actual file for tracking progress per-user. + std::filesystem::path icons_dir; // .../Icons/ }; static Common::SlotVector trophy_handles{}; static Common::SlotVector trophy_contexts{}; @@ -94,66 +195,10 @@ OrbisNpTrophyGrade GetTrophyGradeFromChar(char trophyType) { } } -int PS4_SYSV_ABI sceNpTrophyAbortHandle(OrbisNpTrophyHandle handle) { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyCaptureScreenshot() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyDetails() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyFlagArray() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupArray() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupDetails() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfo() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - return ORBIS_OK; -} - s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, Libraries::UserService::OrbisUserServiceUserId user_id, uint32_t service_label, u64 options) { - ASSERT(options == 0ull); - if (!context) { + if (!context || options != 0ull) { return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; } @@ -169,7 +214,20 @@ s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, const auto ctx_id = trophy_contexts.insert(user_id, service_label); *context = ctx_id.index + 1; - contexts_internal[key].context_id = *context; + + auto& ctx = contexts_internal[key]; + ctx.context_id = *context; + + // Resolve and cache all paths once so callers never recompute them. + const std::string np_comm_id = Common::ElfInfo::Instance().GetNpCommIds()[service_label]; + const auto trophy_base = + Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "trophy" / np_comm_id; + ctx.xml_save_file = Common::FS::GetUserPath(Common::FS::PathType::HomeDir) / + std::to_string(user_id) / "trophy" / (np_comm_id + ".xml"); + ctx.xml_dir = trophy_base / "Xml"; + ctx.icons_dir = trophy_base / "Icons"; + ctx.trophy_xml_path = GetTrophyXmlPath(ctx.xml_dir, EmulatorSettings.GetConsoleLanguage()); + LOG_INFO(Lib_NpTrophy, "New context = {}, user_id = {} service label = {}", *context, user_id, service_label); @@ -206,6 +264,10 @@ int PS4_SYSV_ABI sceNpTrophyDestroyContext(OrbisNpTrophyContext context) { return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } + if (!trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + ContextKey contextkey = trophy_contexts[contextId]; trophy_contexts.erase(contextId); contexts_internal.erase(contextkey); @@ -251,12 +313,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGameIcon(OrbisNpTrophyContext context, OrbisNpTro return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto icon_file = trophy_dir / trophy_folder / "Icons" / "ICON0.PNG"; + const auto& ctx = contexts_internal[contextkey]; + + auto icon_file = ctx.icons_dir / "ICON0.PNG"; Common::FS::IOFile icon(icon_file, Common::FS::FileAccessMode::Read); if (!icon.IsOpen()) { @@ -304,12 +364,11 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.trophy_xml_path; + const auto& trophy_save_file = ctx.xml_save_file; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -336,7 +395,18 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro if (node_name == "group") game_info.num_groups++; + } + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); if (node_name == "trophy") { bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); std::string_view current_trophy_grade = node.attribute("ttype").value(); @@ -368,8 +438,9 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro data->unlocked_silver = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER]; data->unlocked_bronze = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE]; - // maybe this should be 1 instead of 100? - data->progress_percentage = 100; + data->progress_percentage = (game_info.num_trophies > 0) + ? (game_info.unlocked_trophies * 100u) / game_info.num_trophies + : 0; return ORBIS_OK; } @@ -411,12 +482,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.trophy_xml_path; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -450,7 +519,18 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr details->group_id = groupId; data->group_id = groupId; + } + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); if (node_name == "trophy") { bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); std::string_view current_trophy_grade = node.attribute("ttype").value(); @@ -484,15 +564,84 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr data->unlocked_silver = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER]; data->unlocked_bronze = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE]; - // maybe this should be 1 instead of 100? - data->progress_percentage = 100; + data->progress_percentage = + (group_info.num_trophies > 0) + ? (group_info.unlocked_trophies * 100u) / group_info.num_trophies + : 0; return ORBIS_OK; } int PS4_SYSV_ABI sceNpTrophyGetTrophyIcon(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle, OrbisNpTrophyId trophyId, void* buffer, u64* size) { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + if (size == nullptr) + return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; + + if (trophyId < 0 || trophyId >= ORBIS_NP_TROPHY_NUM_MAX) + return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; + + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + + if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + + Common::SlotId contextId; + contextId.index = context - 1; + if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size() || + !trophy_handles.is_allocated({static_cast(handle_index)})) { + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + } + + ContextKey contextkey = trophy_contexts[contextId]; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + + // Check that the trophy is unlocked and icons are only available for earned trophies. + pugi::xml_document doc; + if (!doc.load_file(ctx.xml_save_file.native().c_str())) { + LOG_ERROR(Lib_NpTrophy, "Failed to open trophy XML: {}", ctx.xml_save_file.string()); + return ORBIS_NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; + } + + bool unlocked = false; + bool found = false; + for (const pugi::xml_node& node : doc.child("trophyconf").children()) { + if (std::string_view(node.name()) != "trophy") + continue; + if (node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID) == trophyId) { + found = true; + unlocked = node.attribute("unlockstate").as_bool(); + break; + } + } + + if (!found) + return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; + + if (!unlocked) + return ORBIS_NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED; + + const std::string icon_name = fmt::format("TROP{:03d}.PNG", trophyId); + const auto icon_path = ctx.icons_dir / icon_name; + + Common::FS::IOFile icon(icon_path, Common::FS::FileAccessMode::Read); + if (!icon.IsOpen()) { + LOG_ERROR(Lib_NpTrophy, "Failed to open trophy icon: {}", icon_path.string()); + return ORBIS_NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; + } + + if (buffer != nullptr) { + ReadFile(icon, buffer, *size); + } else { + *size = icon.GetSize(); + } return ORBIS_OK; } @@ -507,7 +656,7 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; - if (trophyId >= 127) + if (trophyId >= ORBIS_NP_TROPHY_NUM_MAX) return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; if (details == nullptr || data == nullptr) @@ -522,12 +671,10 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.trophy_xml_path; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -545,12 +692,34 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT if (node_name == "trophy") { int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); if (current_trophy_id == trophyId) { - bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); - std::string_view current_trophy_grade = node.attribute("ttype").value(); std::string_view current_trophy_name = node.child("name").text().as_string(); std::string_view current_trophy_description = node.child("detail").text().as_string(); + strncpy(details->name, current_trophy_name.data(), ORBIS_NP_TROPHY_NAME_MAX_SIZE); + strncpy(details->description, current_trophy_description.data(), + ORBIS_NP_TROPHY_DESCR_MAX_SIZE); + } + } + } + + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); + + if (node_name == "trophy") { + int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + if (current_trophy_id == trophyId) { + bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); + std::string_view current_trophy_grade = node.attribute("ttype").value(); + uint64_t current_trophy_timestamp = node.attribute("timestamp").as_ullong(); int current_trophy_groupid = node.attribute("gid").as_int(-1); bool current_trophy_hidden = node.attribute("hidden").as_bool(); @@ -560,10 +729,6 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT details->group_id = current_trophy_groupid; details->hidden = current_trophy_hidden; - strncpy(details->name, current_trophy_name.data(), ORBIS_NP_TROPHY_NAME_MAX_SIZE); - strncpy(details->description, current_trophy_description.data(), - ORBIS_NP_TROPHY_DESCR_MAX_SIZE); - data->trophy_id = trophyId; data->unlocked = current_trophy_unlockstate; data->timestamp.tick = current_trophy_timestamp; @@ -579,29 +744,34 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context, OrbisNpTrophyFlagArray* flags, u32* count) { LOG_INFO(Lib_NpTrophy, "called"); + if (flags == nullptr || count == nullptr) + return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; - if (flags == nullptr || count == nullptr) - return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; - - ORBIS_NP_TROPHY_FLAG_ZERO(flags); - Common::SlotId contextId; contextId.index = context - 1; - if (contextId.index >= trophy_contexts.size()) { + if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) { return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } - ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size() || + !trophy_handles.is_allocated({static_cast(handle_index)})) { + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + } + + ContextKey contextkey = trophy_contexts[contextId]; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.xml_save_file; + + ORBIS_NP_TROPHY_FLAG_ZERO(flags); pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -622,10 +792,9 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context, if (node_name == "trophy") { num_trophies++; - } - - if (current_trophy_unlockstate) { - ORBIS_NP_TROPHY_FLAG_SET(current_trophy_id, flags); + if (current_trophy_unlockstate) { + ORBIS_NP_TROPHY_FLAG_SET(current_trophy_id, flags); + } } } @@ -633,6 +802,200 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context, return ORBIS_OK; } +int PS4_SYSV_ABI sceNpTrophyRegisterContext(OrbisNpTrophyContext context, + OrbisNpTrophyHandle handle, uint64_t options) { + if (options != 0ull) + return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; + + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + + if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + + Common::SlotId contextId; + contextId.index = context - 1; + if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size() || + !trophy_handles.is_allocated({static_cast(handle_index)})) { + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + } + + ContextKey contextkey = trophy_contexts[contextId]; + auto& ctx = contexts_internal[contextkey]; + + if (ctx.registered) + return ORBIS_NP_TROPHY_ERROR_ALREADY_REGISTERED; + + if (!std::filesystem::exists(ctx.trophy_xml_path)) + return ORBIS_NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED; + + ctx.registered = true; + LOG_INFO(Lib_NpTrophy, "Context {} registered", context); + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle, + OrbisNpTrophyId trophyId, OrbisNpTrophyId* platinumId) { + LOG_INFO(Lib_NpTrophy, "Unlocking trophy id {}", trophyId); + + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + + if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + + if (trophyId >= ORBIS_NP_TROPHY_NUM_MAX) + return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; + + if (platinumId == nullptr) + return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; + + Common::SlotId contextId; + contextId.index = context - 1; + if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size() || + !trophy_handles.is_allocated({static_cast(handle_index)})) { + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + } + + ContextKey contextkey = trophy_contexts[contextId]; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& xml_dir = ctx.xml_dir; + const auto& trophy_file = ctx.trophy_xml_path; + + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", save_result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); + if (std::string_view(node.name()) != "trophy") + continue; + + int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); + + if (current_trophy_id == trophyId) { + if (current_trophy_unlockstate) { + LOG_INFO(Lib_NpTrophy, "Trophy already unlocked"); + return ORBIS_NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; + } + } + } + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); + + if (!result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse trophy xml : {}", result.description()); + return ORBIS_NP_TROPHY_ERROR_TITLE_NOT_FOUND; + } + + *platinumId = ORBIS_NP_TROPHY_INVALID_TROPHY_ID; + + int num_trophies = 0; + int num_trophies_unlocked = 0; + pugi::xml_node platinum_node; + + // Outputs filled during the scan. + bool trophy_found = false; + const char* trophy_name = ""; + std::string_view trophy_type; + std::filesystem::path trophy_icon_path; + + auto trophyconf = doc.child("trophyconf"); + + for (pugi::xml_node& node : trophyconf.children()) { + if (std::string_view(node.name()) != "trophy") + continue; + + int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); + std::string_view current_trophy_type = node.attribute("ttype").value(); + + if (current_trophy_type == "P") { + platinum_node = node; + if (trophyId == current_trophy_id) { + return ORBIS_NP_TROPHY_ERROR_PLATINUM_CANNOT_UNLOCK; + } + } + + if (node.attribute("pid").as_int(-1) != ORBIS_NP_TROPHY_INVALID_TROPHY_ID) { + num_trophies++; + if (current_trophy_unlockstate) { + num_trophies_unlocked++; + } + } + + if (current_trophy_id == trophyId) { + trophy_found = true; + trophy_name = node.child("name").text().as_string(); + trophy_type = current_trophy_type; + + const std::string icon_file = fmt::format("TROP{:03d}.PNG", current_trophy_id); + trophy_icon_path = ctx.icons_dir / icon_file; + } + } + + if (!trophy_found) + return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; + + // Capture timestamps once so every file gets the exact same value. + const auto now_secs = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + const u64 trophy_timestamp = static_cast(now_secs); + + // Decide platinum. + bool unlock_platinum = false; + OrbisNpTrophyId platinum_id = ORBIS_NP_TROPHY_INVALID_TROPHY_ID; + u64 platinum_timestamp = 0; + const char* platinum_name = ""; + std::filesystem::path platinum_icon_path; + + if (!platinum_node.attribute("unlockstate").as_bool()) { + if ((num_trophies - 1) == num_trophies_unlocked) { + unlock_platinum = true; + platinum_id = platinum_node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); + platinum_timestamp = trophy_timestamp; // same second is fine + platinum_name = platinum_node.child("name").text().as_string(); + + const std::string plat_icon_file = fmt::format("TROP{:03d}.PNG", platinum_id); + platinum_icon_path = ctx.icons_dir / plat_icon_file; + + *platinumId = platinum_id; + } + } + + // Queue UI notifications (only once, using the primary XML's strings). + AddTrophyToQueue(trophy_icon_path, trophy_name, trophy_type); + if (unlock_platinum) { + AddTrophyToQueue(platinum_icon_path, platinum_name, "P"); + } + + ApplyUnlockToXmlFile(ctx.xml_save_file, trophyId, trophy_timestamp, unlock_platinum, + platinum_id, platinum_timestamp); + LOG_INFO(Lib_NpTrophy, "Trophy {} successfully saved.", trophyId); + + return ORBIS_OK; +} + int PS4_SYSV_ABI sceNpTrophyGroupArrayGetNum() { LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); return ORBIS_OK; @@ -698,19 +1061,6 @@ int PS4_SYSV_ABI sceNpTrophyNumInfoGetTotal() { return ORBIS_OK; } -int PS4_SYSV_ABI sceNpTrophyRegisterContext(OrbisNpTrophyContext context, - OrbisNpTrophyHandle handle, uint64_t options) { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); - - if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) - return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; - - if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) - return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; - - return ORBIS_OK; -} - int PS4_SYSV_ABI sceNpTrophySetInfoGetTrophyFlagArray() { LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); return ORBIS_OK; @@ -942,147 +1292,58 @@ int PS4_SYSV_ABI sceNpTrophySystemSetDbgParamInt() { return ORBIS_OK; } -int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle, - OrbisNpTrophyId trophyId, OrbisNpTrophyId* platinumId) { - LOG_INFO(Lib_NpTrophy, "Unlocking trophy id {}", trophyId); +int PS4_SYSV_ABI sceNpTrophyAbortHandle(OrbisNpTrophyHandle handle) { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) - return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; +int PS4_SYSV_ABI sceNpTrophyCaptureScreenshot() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) - return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyDetails() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (trophyId >= 127) - return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyFlagArray() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (platinumId == nullptr) - return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupArray() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - Common::SlotId contextId; - contextId.index = context - 1; - if (contextId.index >= trophy_contexts.size()) { - return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; - } - ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupDetails() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfo() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - if (!result) { - LOG_ERROR(Lib_NpTrophy, "Failed to parse trophy xml : {}", result.description()); - return ORBIS_OK; - } +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} - *platinumId = ORBIS_NP_TROPHY_INVALID_TROPHY_ID; - - int num_trophies = 0; - int num_trophies_unlocked = 0; - pugi::xml_node platinum_node; - - auto trophyconf = doc.child("trophyconf"); - - for (pugi::xml_node& node : trophyconf.children()) { - int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); - bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); - const char* current_trophy_name = node.child("name").text().as_string(); - std::string_view current_trophy_description = node.child("detail").text().as_string(); - std::string_view current_trophy_type = node.attribute("ttype").value(); - - if (current_trophy_type == "P") { - platinum_node = node; - if (trophyId == current_trophy_id) { - return ORBIS_NP_TROPHY_ERROR_PLATINUM_CANNOT_UNLOCK; - } - } - - if (std::string_view(node.name()) == "trophy") { - if (node.attribute("pid").as_int(-1) != ORBIS_NP_TROPHY_INVALID_TROPHY_ID) { - num_trophies++; - if (current_trophy_unlockstate) { - num_trophies_unlocked++; - } - } - - if (current_trophy_id == trophyId) { - if (current_trophy_unlockstate) { - LOG_INFO(Lib_NpTrophy, "Trophy already unlocked"); - return ORBIS_NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; - } else { - if (node.attribute("unlockstate").empty()) { - node.append_attribute("unlockstate") = "true"; - } else { - node.attribute("unlockstate").set_value("true"); - } - - auto trophyTimestamp = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - - if (node.attribute("timestamp").empty()) { - node.append_attribute("timestamp") = - std::to_string(trophyTimestamp).c_str(); - } else { - node.attribute("timestamp") - .set_value(std::to_string(trophyTimestamp).c_str()); - } - - std::string trophy_icon_file = "TROP"; - trophy_icon_file.append(node.attribute("id").value()); - trophy_icon_file.append(".PNG"); - - std::filesystem::path current_icon_path = - trophy_dir / trophy_folder / "Icons" / trophy_icon_file; - - AddTrophyToQueue(current_icon_path, current_trophy_name, current_trophy_type); - } - } - } - } - - if (!platinum_node.attribute("unlockstate").as_bool()) { - if ((num_trophies - 1) == num_trophies_unlocked) { - if (platinum_node.attribute("unlockstate").empty()) { - platinum_node.append_attribute("unlockstate") = "true"; - } else { - platinum_node.attribute("unlockstate").set_value("true"); - } - - auto trophyTimestamp = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - - if (platinum_node.attribute("timestamp").empty()) { - platinum_node.append_attribute("timestamp") = - std::to_string(trophyTimestamp).c_str(); - } else { - platinum_node.attribute("timestamp") - .set_value(std::to_string(trophyTimestamp).c_str()); - } - - int platinum_trophy_id = - platinum_node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID); - const char* platinum_trophy_name = platinum_node.child("name").text().as_string(); - - std::string platinum_icon_file = "TROP"; - platinum_icon_file.append(platinum_node.attribute("id").value()); - platinum_icon_file.append(".PNG"); - - std::filesystem::path platinum_icon_path = - trophy_dir / trophy_folder / "Icons" / platinum_icon_file; - - *platinumId = platinum_trophy_id; - AddTrophyToQueue(platinum_icon_path, platinum_trophy_name, "P"); - } - } - - doc.save_file((trophy_dir / trophy_folder / "Xml" / "TROP.XML").native().c_str()); +int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + return ORBIS_OK; +} +int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() { + LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); return ORBIS_OK; } diff --git a/src/core/libraries/np/np_trophy.h b/src/core/libraries/np/np_trophy.h index ab187ae13..590e58c0d 100644 --- a/src/core/libraries/np/np_trophy.h +++ b/src/core/libraries/np/np_trophy.h @@ -13,8 +13,6 @@ class SymbolsResolver; namespace Libraries::Np::NpTrophy { -extern std::string game_serial; - constexpr int ORBIS_NP_TROPHY_FLAG_SETSIZE = 128; constexpr int ORBIS_NP_TROPHY_FLAG_BITS_SHIFT = 5; diff --git a/src/core/libraries/np/np_trophy_error.h b/src/core/libraries/np/np_trophy_error.h index 8ac356225..173d7dd59 100644 --- a/src/core/libraries/np/np_trophy_error.h +++ b/src/core/libraries/np/np_trophy_error.h @@ -47,3 +47,4 @@ constexpr int ORBIS_NP_TROPHY_ERROR_INCONSISTENT_TITLE_CONF = 0x80551628; constexpr int ORBIS_NP_TROPHY_ERROR_TITLE_BACKGROUND = 0x80551629; constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISABLED = 0x8055162B; constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISPLAY_BUFFER_NOT_IN_USE = 0x8055162D; +constexpr int ORBIS_NP_TROPHY_ERROR_TITLE_NOT_FOUND = 0x805516C2; diff --git a/src/core/libraries/np/np_web_api2.cpp b/src/core/libraries/np/np_web_api2.cpp index c03636e73..a7c7ee3f3 100644 --- a/src/core/libraries/np/np_web_api2.cpp +++ b/src/core/libraries/np/np_web_api2.cpp @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_web_api2.h" @@ -115,10 +115,10 @@ s32 PS4_SYSV_ABI sceNpWebApi2IntInitialize2(const OrbisNpWebApi2IntInitialize2Ar if (args == nullptr || args->struct_size != sizeof(OrbisNpWebApi2IntInitialize2Args)) { return ORBIS_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; } - LOG_ERROR( - Lib_NpWebApi2, - "(STUBBED) called, lib_http_ctx_id = {:#x}, pool_size = {:#x}, name = '{}', group = {:#x}", - args->lib_http_ctx_id, args->pool_size, args->name, args->push_config_group); + LOG_ERROR(Lib_NpWebApi2, + "(STUBBED) called, lib_http_ctx_id = {:#x}, pool_size = {:#x}, name = '{}', " + "group = {:#x}", + args->lib_http_ctx_id, args->pool_size, args->name, args->push_config_group); return ORBIS_OK; } @@ -207,7 +207,7 @@ s32 PS4_SYSV_ABI sceNpWebApi2SendMultipartRequest() { } s32 PS4_SYSV_ABI sceNpWebApi2SendRequest() { - if (!Config::getPSNSignedIn()) { + if (!EmulatorSettings.IsPSNSignedIn()) { LOG_INFO(Lib_NpWebApi2, "called, returning PSN signed out."); return ORBIS_NP_WEBAPI2_ERROR_NOT_SIGNED_IN; } diff --git a/src/core/libraries/np/np_web_api_internal.cpp b/src/core/libraries/np/np_web_api_internal.cpp index f598344c7..66a09b493 100644 --- a/src/core/libraries/np/np_web_api_internal.cpp +++ b/src/core/libraries/np/np_web_api_internal.cpp @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/elf_info.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/process.h" #include "core/libraries/kernel/time.h" #include "core/libraries/network/http.h" @@ -606,7 +606,7 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s unlockContext(context); // Stubbing sceNpManagerIntGetSigninState call with a config check. - if (!Config::getPSNSignedIn()) { + if (!EmulatorSettings.IsPSNSignedIn()) { releaseRequest(request); releaseUserContext(user_context); releaseContext(context); @@ -1025,7 +1025,7 @@ s32 createServicePushEventFilterInternal( auto& handle = context->handles[handleId]; handle->userCount++; - if (pNpServiceName != nullptr && !Config::getPSNSignedIn()) { + if (pNpServiceName != nullptr && !EmulatorSettings.IsPSNSignedIn()) { // Seems sceNpManagerIntGetUserList fails? LOG_DEBUG(Lib_NpWebApi, "Cannot create service push event while PSN is disabled"); handle->userCount--; @@ -1202,7 +1202,7 @@ s32 createExtendedPushEventFilterInternal( auto& handle = context->handles[handleId]; handle->userCount++; - if (pNpServiceName != nullptr && !Config::getPSNSignedIn()) { + if (pNpServiceName != nullptr && !EmulatorSettings.IsPSNSignedIn()) { // Seems sceNpManagerIntGetUserList fails? LOG_DEBUG(Lib_NpWebApi, "Cannot create extended push event while PSN is disabled"); handle->userCount--; diff --git a/src/core/libraries/np/trophy_ui.cpp b/src/core/libraries/np/trophy_ui.cpp index b803403c4..a4fd21a33 100644 --- a/src/core/libraries/np/trophy_ui.cpp +++ b/src/core/libraries/np/trophy_ui.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 @@ -7,15 +7,10 @@ #include #include #include - -#ifdef ENABLE_QT_GUI -#include -#endif - #include "common/assert.h" -#include "common/config.h" #include "common/path_util.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/libraries/np/trophy_ui.h" #include "imgui/imgui_std.h" @@ -36,9 +31,9 @@ TrophyUI::TrophyUI(const std::filesystem::path& trophyIconPath, const std::strin const std::string_view& rarity) : trophy_name(trophyName), trophy_type(rarity) { - side = Config::sideTrophy(); + side = EmulatorSettings.GetTrophyNotificationSide(); - trophy_timer = Config::getTrophyNotificationDuration(); + trophy_timer = EmulatorSettings.GetTrophyNotificationDuration(); if (std::filesystem::exists(trophyIconPath)) { trophy_icon = RefCountedTexture::DecodePngFile(trophyIconPath); @@ -98,7 +93,7 @@ TrophyUI::TrophyUI(const std::filesystem::path& trophyIconPath, const std::strin return; } - MIX_SetMasterGain(mixer, static_cast(Config::getVolumeSlider() / 100.f)); + MIX_SetMasterGain(mixer, static_cast(EmulatorSettings.GetVolumeSlider() / 100.f)); auto musicPathMp3 = CustomTrophy_Dir / "trophy.mp3"; auto musicPathWav = CustomTrophy_Dir / "trophy.wav"; @@ -284,7 +279,7 @@ void AddTrophyToQueue(const std::filesystem::path& trophyIconPath, const std::st const std::string_view& rarity) { std::lock_guard lock(queueMtx); - if (Config::getisTrophyPopupDisabled()) { + if (EmulatorSettings.IsTrophyPopupDisabled()) { return; } else if (current_trophy_ui.has_value()) { current_trophy_ui.reset(); diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index b31ed1f0b..f38621191 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -1,20 +1,24 @@ // SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/logging/log.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/libraries/libs.h" #include "core/libraries/pad/pad_errors.h" +#include "core/user_settings.h" #include "input/controller.h" #include "pad.h" namespace Libraries::Pad { using Input::GameController; +using Input::GameControllers; +using namespace Libraries::UserService; static bool g_initialized = false; -static bool g_opened = false; +static std::unordered_map user_id_pad_handle_map{}; +static constexpr s32 tv_remote_handle = 5; int PS4_SYSV_ABI scePadClose(s32 handle) { LOG_ERROR(Lib_Pad, "(STUBBED) called"); @@ -30,8 +34,8 @@ int PS4_SYSV_ABI scePadDeviceClassGetExtendedInformation( s32 handle, OrbisPadDeviceClassExtendedInformation* pExtInfo) { LOG_ERROR(Lib_Pad, "(STUBBED) called"); std::memset(pExtInfo, 0, sizeof(OrbisPadDeviceClassExtendedInformation)); - if (Config::getUseSpecialPad()) { - pExtInfo->deviceClass = (OrbisPadDeviceClass)Config::getSpecialPadClass(); + if (EmulatorSettings.IsUsingSpecialPad()) { + pExtInfo->deviceClass = (OrbisPadDeviceClass)EmulatorSettings.GetSpecialPadClass(); } return ORBIS_OK; } @@ -107,9 +111,9 @@ int PS4_SYSV_ABI scePadGetControllerInformation(s32 handle, OrbisPadControllerIn return ORBIS_OK; } pInfo->connected = true; - if (Config::getUseSpecialPad()) { + if (EmulatorSettings.IsUsingSpecialPad()) { pInfo->connectionType = ORBIS_PAD_PORT_TYPE_SPECIAL; - pInfo->deviceClass = (OrbisPadDeviceClass)Config::getSpecialPadClass(); + pInfo->deviceClass = (OrbisPadDeviceClass)EmulatorSettings.GetSpecialPadClass(); } return ORBIS_OK; } @@ -156,11 +160,16 @@ int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId if (!g_initialized) { return ORBIS_PAD_ERROR_NOT_INITIALIZED; } - if (userId == -1 || !g_opened) { + if (userId == -1) { return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE; } - LOG_DEBUG(Lib_Pad, "(DUMMY) called"); - return 1; + auto it = user_id_pad_handle_map.find(userId); + if (it == user_id_pad_handle_map.end()) { + return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE; + } + s32 pad_handle = it->second; + LOG_DEBUG(Lib_Pad, "called, userid: {}, out pad handle: {}", userId, pad_handle); + return pad_handle; } int PS4_SYSV_ABI scePadGetIdleCount() { @@ -168,8 +177,19 @@ int PS4_SYSV_ABI scePadGetIdleCount() { return ORBIS_OK; } -int PS4_SYSV_ABI scePadGetInfo() { - LOG_ERROR(Lib_Pad, "(STUBBED) called"); +int PS4_SYSV_ABI scePadGetInfo(u32* data) { + LOG_WARNING(Lib_Pad, "(DUMMY) called"); + if (!data) { + return ORBIS_PAD_ERROR_INVALID_ARG; + } + data[0] = 0x1; // index but starting from one? + data[1] = 0x0; // index? + data[2] = 1; // pad handle + data[3] = 0x0101; // ??? + data[4] = 0x0; // ? + data[5] = 0x0; // ? + data[6] = 0x00ff0000; // colour(?) + data[7] = 0x0; // ? return ORBIS_OK; } @@ -254,34 +274,61 @@ int PS4_SYSV_ABI scePadOpen(Libraries::UserService::OrbisUserServiceUserId userI if (!g_initialized) { return ORBIS_PAD_ERROR_NOT_INITIALIZED; } - if (userId == -1) { - return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE; + if (userId < 0) { + return ORBIS_DEVICE_SERVICE_ERROR_INVALID_USER; } - if (Config::getUseSpecialPad()) { + if (userId == ORBIS_USER_SERVICE_USER_ID_SYSTEM) { + if (type == ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) { + LOG_INFO(Lib_Pad, "Opened a TV remote device"); + user_id_pad_handle_map[ORBIS_USER_SERVICE_USER_ID_SYSTEM] = tv_remote_handle; + return tv_remote_handle; + } + return ORBIS_DEVICE_SERVICE_ERROR_INVALID_USER; + } + if (type == ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) { + return ORBIS_PAD_ERROR_INVALID_ARG; + } + if (EmulatorSettings.IsUsingSpecialPad()) { if (type != ORBIS_PAD_PORT_TYPE_SPECIAL) return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED; } else { - if (type != ORBIS_PAD_PORT_TYPE_STANDARD && type != ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) + if (type != ORBIS_PAD_PORT_TYPE_STANDARD) return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED; } - LOG_INFO(Lib_Pad, "(DUMMY) called user_id = {} type = {} index = {}", userId, type, index); - g_opened = true; - scePadResetLightBar(userId); - scePadResetOrientation(userId); - return 1; // dummy + auto u = UserManagement.GetUserByID(userId); + if (!u) { + return ORBIS_DEVICE_SERVICE_ERROR_USER_NOT_LOGIN; + } + s32 pad_handle = u->player_index; + LOG_INFO(Lib_Pad, "called user_id = {} type = {} index = {}, pad_handle = {}", userId, type, + index, pad_handle); + scePadResetLightBar(pad_handle); + scePadResetOrientation(pad_handle); + user_id_pad_handle_map[userId] = pad_handle; + return pad_handle; } int PS4_SYSV_ABI scePadOpenExt(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, s32 index, const OrbisPadOpenExtParam* pParam) { LOG_ERROR(Lib_Pad, "(STUBBED) called"); - if (Config::getUseSpecialPad()) { + if (EmulatorSettings.IsUsingSpecialPad()) { if (type != ORBIS_PAD_PORT_TYPE_SPECIAL) return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED; } else { if (type != ORBIS_PAD_PORT_TYPE_STANDARD && type != ORBIS_PAD_PORT_TYPE_REMOTE_CONTROL) return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED; } - return 1; // dummy + auto u = UserManagement.GetUserByID(userId); + if (!u) { + return ORBIS_DEVICE_SERVICE_ERROR_USER_NOT_LOGIN; + } + s32 pad_handle = u->player_index; + LOG_INFO(Lib_Pad, "called user_id = {} type = {} index = {}, pad_handle = {}", userId, type, + index, pad_handle); + scePadResetLightBar(pad_handle); + scePadResetOrientation(pad_handle); + user_id_pad_handle_map[userId] = pad_handle; + return pad_handle; } int PS4_SYSV_ABI scePadOpenExt2() { @@ -294,8 +341,8 @@ int PS4_SYSV_ABI scePadOutputReport() { return ORBIS_OK; } -int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num, bool connected, - u32 connected_count) { +int ProcessStates(s32 handle, OrbisPadData* pData, Input::GameController& controller, + Input::State* states, s32 num, bool connected, u32 connected_count) { if (!connected) { pData[0] = {}; pData[0].orientation = {0.0f, 0.0f, 0.0f, 1.0f}; @@ -319,61 +366,57 @@ int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num pData[i].angularVelocity.z = states[i].angularVelocity.z; pData[i].orientation = {0.0f, 0.0f, 0.0f, 1.0f}; - auto* controller = Common::Singleton::Instance(); - const auto* engine = controller->GetEngine(); - if (engine && handle == 1) { - const auto gyro_poll_rate = engine->GetAccelPollRate(); - if (gyro_poll_rate != 0.0f) { - auto now = std::chrono::steady_clock::now(); - float deltaTime = std::chrono::duration_cast( - now - controller->GetLastUpdate()) - .count() / - 1000000.0f; - controller->SetLastUpdate(now); - Libraries::Pad::OrbisFQuaternion lastOrientation = controller->GetLastOrientation(); - Libraries::Pad::OrbisFQuaternion outputOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; - GameController::CalculateOrientation(pData->acceleration, pData->angularVelocity, - deltaTime, lastOrientation, outputOrientation); - pData[i].orientation = outputOrientation; - controller->SetLastOrientation(outputOrientation); - } + const auto gyro_poll_rate = controller.accel_poll_rate; + if (gyro_poll_rate != 0.0f) { + auto now = std::chrono::steady_clock::now(); + float deltaTime = std::chrono::duration_cast( + now - controller.GetLastUpdate()) + .count() / + 1000000.0f; + controller.SetLastUpdate(now); + Libraries::Pad::OrbisFQuaternion lastOrientation = controller.GetLastOrientation(); + Libraries::Pad::OrbisFQuaternion outputOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; + GameControllers::CalculateOrientation(pData->acceleration, pData->angularVelocity, + deltaTime, lastOrientation, outputOrientation); + pData[i].orientation = outputOrientation; + controller.SetLastOrientation(outputOrientation); } pData[i].touchData.touchNum = (states[i].touchpad[0].state ? 1 : 0) + (states[i].touchpad[1].state ? 1 : 0); if (handle == 1) { - if (controller->GetTouchCount() >= 127) { - controller->SetTouchCount(0); + if (controller.GetTouchCount() >= 127) { + controller.SetTouchCount(0); } - if (controller->GetSecondaryTouchCount() >= 127) { - controller->SetSecondaryTouchCount(0); + if (controller.GetSecondaryTouchCount() >= 127) { + controller.SetSecondaryTouchCount(0); } - if (pData->touchData.touchNum == 1 && controller->GetPreviousTouchNum() == 0) { - controller->SetTouchCount(controller->GetTouchCount() + 1); - controller->SetSecondaryTouchCount(controller->GetTouchCount()); - } else if (pData->touchData.touchNum == 2 && controller->GetPreviousTouchNum() == 1) { - controller->SetSecondaryTouchCount(controller->GetSecondaryTouchCount() + 1); - } else if (pData->touchData.touchNum == 0 && controller->GetPreviousTouchNum() > 0) { - if (controller->GetTouchCount() < controller->GetSecondaryTouchCount()) { - controller->SetTouchCount(controller->GetSecondaryTouchCount()); + if (pData->touchData.touchNum == 1 && controller.GetPreviousTouchNum() == 0) { + controller.SetTouchCount(controller.GetTouchCount() + 1); + controller.SetSecondaryTouchCount(controller.GetTouchCount()); + } else if (pData->touchData.touchNum == 2 && controller.GetPreviousTouchNum() == 1) { + controller.SetSecondaryTouchCount(controller.GetSecondaryTouchCount() + 1); + } else if (pData->touchData.touchNum == 0 && controller.GetPreviousTouchNum() > 0) { + if (controller.GetTouchCount() < controller.GetSecondaryTouchCount()) { + controller.SetTouchCount(controller.GetSecondaryTouchCount()); } else { - if (controller->WasSecondaryTouchReset()) { - controller->SetTouchCount(controller->GetSecondaryTouchCount()); - controller->UnsetSecondaryTouchResetBool(); + if (controller.WasSecondaryTouchReset()) { + controller.SetTouchCount(controller.GetSecondaryTouchCount()); + controller.UnsetSecondaryTouchResetBool(); } } } - controller->SetPreviousTouchNum(pData->touchData.touchNum); + controller.SetPreviousTouchNum(pData->touchData.touchNum); if (pData->touchData.touchNum == 1) { - states[i].touchpad[0].ID = controller->GetTouchCount(); + states[i].touchpad[0].ID = controller.GetTouchCount(); states[i].touchpad[1].ID = 0; } else if (pData->touchData.touchNum == 2) { - states[i].touchpad[0].ID = controller->GetTouchCount(); - states[i].touchpad[1].ID = controller->GetSecondaryTouchCount(); + states[i].touchpad[0].ID = controller.GetTouchCount(); + states[i].touchpad[1].ID = controller.GetSecondaryTouchCount(); } } else { states[i].touchpad[0].ID = 1; @@ -397,16 +440,18 @@ int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num) { LOG_TRACE(Lib_Pad, "called"); - if (handle < 1) { - return ORBIS_PAD_ERROR_INVALID_HANDLE; - } int connected_count = 0; bool connected = false; std::vector states(64); - auto* controller = Common::Singleton::Instance(); - const auto* engine = controller->GetEngine(); - int ret_num = controller->ReadStates(states.data(), num, &connected, &connected_count); - return ProcessStates(handle, pData, states.data(), ret_num, connected, connected_count); + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { + return ORBIS_PAD_ERROR_INVALID_HANDLE; + } + auto& controllers = *Common::Singleton::Instance(); + auto& controller = *controllers[*controller_id]; + int ret_num = controller.ReadStates(states.data(), num, &connected, &connected_count); + return ProcessStates(handle, pData, controller, states.data(), ret_num, connected, + connected_count); } int PS4_SYSV_ABI scePadReadBlasterForTracker() { @@ -430,17 +475,18 @@ int PS4_SYSV_ABI scePadReadHistory() { } int PS4_SYSV_ABI scePadReadState(s32 handle, OrbisPadData* pData) { - LOG_TRACE(Lib_Pad, "called"); - if (handle < 1) { + LOG_TRACE(Lib_Pad, "handle: {}", handle); + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { return ORBIS_PAD_ERROR_INVALID_HANDLE; } - auto* controller = Common::Singleton::Instance(); - const auto* engine = controller->GetEngine(); + auto& controllers = *Common::Singleton::Instance(); + auto& controller = *controllers[*controller_id]; int connected_count = 0; bool connected = false; Input::State state; - controller->ReadState(&state, &connected, &connected_count); - ProcessStates(handle, pData, &state, 1, connected, connected_count); + controller.ReadState(&state, &connected, &connected_count); + ProcessStates(handle, pData, controller, &state, 1, connected, connected_count); return ORBIS_OK; } @@ -450,13 +496,30 @@ int PS4_SYSV_ABI scePadReadStateExt() { } int PS4_SYSV_ABI scePadResetLightBar(s32 handle) { - LOG_INFO(Lib_Pad, "(DUMMY) called"); - if (handle != 1) { + LOG_DEBUG(Lib_Pad, "called, handle: {}", handle); + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { return ORBIS_PAD_ERROR_INVALID_HANDLE; } - auto* controller = Common::Singleton::Instance(); - int* rgb = Config::GetControllerCustomColor(); - controller->SetLightBarRGB(rgb[0], rgb[1], rgb[2]); + auto& controllers = *Common::Singleton::Instance(); + s32 colour_index = UserManagement.GetUserByPlayerIndex(handle)->user_color - 1; + Input::Colour colour{255, 0, 0}; + if (colour_index >= 0 && colour_index <= 3) { + static constexpr Input::Colour colours[4]{ + {0, 0, 255}, // blue + {255, 0, 0}, // red + {0, 255, 0}, // green + {255, 0, 255}, // pink + }; + colour = colours[colour_index]; + } else { + LOG_ERROR(Lib_Pad, "Invalid user colour value {} for controller {}, falling back to blue", + colour_index, handle); + } + if (auto oc = GameControllers::GetControllerCustomColor(*controller_id)) { + colour = *oc; + } + controllers[*controller_id]->SetLightBarRGB(colour.r, colour.g, colour.b); return ORBIS_OK; } @@ -473,14 +536,15 @@ int PS4_SYSV_ABI scePadResetLightBarAllByPortType() { int PS4_SYSV_ABI scePadResetOrientation(s32 handle) { LOG_INFO(Lib_Pad, "scePadResetOrientation called handle = {}", handle); - if (handle != 1) { + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { return ORBIS_PAD_ERROR_INVALID_HANDLE; } - auto* controller = Common::Singleton::Instance(); + auto& controllers = *Common::Singleton::Instance(); Libraries::Pad::OrbisFQuaternion defaultOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; - controller->SetLastOrientation(defaultOrientation); - controller->SetLastUpdate(std::chrono::steady_clock::now()); + controllers[*controller_id]->SetLastOrientation(defaultOrientation); + controllers[*controller_id]->SetLastUpdate(std::chrono::steady_clock::now()); return ORBIS_OK; } @@ -526,7 +590,11 @@ int PS4_SYSV_ABI scePadSetForceIntercepted() { } int PS4_SYSV_ABI scePadSetLightBar(s32 handle, const OrbisPadLightBarParam* pParam) { - if (Config::GetOverrideControllerColor()) { + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { + return ORBIS_PAD_ERROR_INVALID_HANDLE; + } + if (GameControllers::GetControllerCustomColor(*controller_id)) { return ORBIS_OK; } if (pParam != nullptr) { @@ -538,8 +606,8 @@ int PS4_SYSV_ABI scePadSetLightBar(s32 handle, const OrbisPadLightBarParam* pPar return ORBIS_PAD_ERROR_INVALID_LIGHTBAR_SETTING; } - auto* controller = Common::Singleton::Instance(); - controller->SetLightBarRGB(pParam->r, pParam->g, pParam->b); + auto& controllers = *Common::Singleton::Instance(); + controllers[*controller_id]->SetLightBarRGB(pParam->r, pParam->g, pParam->b); return ORBIS_OK; } return ORBIS_PAD_ERROR_INVALID_ARG; @@ -555,8 +623,14 @@ int PS4_SYSV_ABI scePadSetLightBarBlinking() { return ORBIS_OK; } -int PS4_SYSV_ABI scePadSetLightBarForTracker() { - LOG_ERROR(Lib_Pad, "(STUBBED) called"); +int PS4_SYSV_ABI scePadSetLightBarForTracker(s32 handle, const OrbisPadLightBarParam* pParam) { + LOG_INFO(Lib_Pad, "called, r: {} g: {} b: {}", pParam->r, pParam->g, pParam->b); + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { + return ORBIS_PAD_ERROR_INVALID_HANDLE; + } + auto& controllers = *Common::Singleton::Instance(); + controllers[*controller_id]->SetLightBarRGB(pParam->r, pParam->g, pParam->b); return ORBIS_OK; } @@ -603,11 +677,15 @@ int PS4_SYSV_ABI scePadSetUserColor() { } int PS4_SYSV_ABI scePadSetVibration(s32 handle, const OrbisPadVibrationParam* pParam) { + auto controller_id = GameControllers::GetControllerIndexFromControllerID(handle); + if (!controller_id) { + return ORBIS_PAD_ERROR_INVALID_HANDLE; + } if (pParam != nullptr) { LOG_DEBUG(Lib_Pad, "scePadSetVibration called handle = {} data = {} , {}", handle, pParam->smallMotor, pParam->largeMotor); - auto* controller = Common::Singleton::Instance(); - controller->SetVibration(pParam->smallMotor, pParam->largeMotor); + auto& controllers = *Common::Singleton::Instance(); + controllers[*controller_id]->SetVibration(pParam->smallMotor, pParam->largeMotor); return ORBIS_OK; } return ORBIS_PAD_ERROR_INVALID_ARG; diff --git a/src/core/libraries/pad/pad.h b/src/core/libraries/pad/pad.h index 02ceaf3d9..2f4cbcc6a 100644 --- a/src/core/libraries/pad/pad.h +++ b/src/core/libraries/pad/pad.h @@ -280,7 +280,7 @@ int PS4_SYSV_ABI scePadGetFeatureReport(); int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, s32 index); int PS4_SYSV_ABI scePadGetIdleCount(); -int PS4_SYSV_ABI scePadGetInfo(); +int PS4_SYSV_ABI scePadGetInfo(u32* data); int PS4_SYSV_ABI scePadGetInfoByPortType(); int PS4_SYSV_ABI scePadGetLicenseControllerInformation(); int PS4_SYSV_ABI scePadGetMotionSensorPosition(); @@ -324,7 +324,7 @@ int PS4_SYSV_ABI scePadSetForceIntercepted(); int PS4_SYSV_ABI scePadSetLightBar(s32 handle, const OrbisPadLightBarParam* pParam); int PS4_SYSV_ABI scePadSetLightBarBaseBrightness(); int PS4_SYSV_ABI scePadSetLightBarBlinking(); -int PS4_SYSV_ABI scePadSetLightBarForTracker(); +int PS4_SYSV_ABI scePadSetLightBarForTracker(s32 handle, const OrbisPadLightBarParam* pParam); int PS4_SYSV_ABI scePadSetLoginUserNumber(); int PS4_SYSV_ABI scePadSetMotionSensorState(s32 handle, bool bEnable); int PS4_SYSV_ABI scePadSetProcessFocus(); diff --git a/src/core/libraries/pad/pad_errors.h b/src/core/libraries/pad/pad_errors.h index 182c89219..bb2a2e9ac 100644 --- a/src/core/libraries/pad/pad_errors.h +++ b/src/core/libraries/pad/pad_errors.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -20,3 +20,6 @@ constexpr int ORBIS_PAD_ERROR_INVALID_BUFFER_LENGTH = 0x80920102; constexpr int ORBIS_PAD_ERROR_INVALID_REPORT_LENGTH = 0x80920103; constexpr int ORBIS_PAD_ERROR_INVALID_REPORT_ID = 0x80920104; constexpr int ORBIS_PAD_ERROR_SEND_AGAIN = 0x80920105; + +constexpr s32 ORBIS_DEVICE_SERVICE_ERROR_INVALID_USER = 0x809b0001; +constexpr s32 ORBIS_DEVICE_SERVICE_ERROR_USER_NOT_LOGIN = 0x809b0081; diff --git a/src/core/libraries/save_data/save_instance.cpp b/src/core/libraries/save_data/save_instance.cpp index baeec5d2c..463baa50b 100644 --- a/src/core/libraries/save_data/save_instance.cpp +++ b/src/core/libraries/save_data/save_instance.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -6,9 +6,9 @@ #include #include "common/assert.h" -#include "common/config.h" #include "common/path_util.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/file_sys/fs.h" #include "save_backup.h" #include "save_instance.h" @@ -48,12 +48,13 @@ namespace Libraries::SaveData { fs::path SaveInstance::MakeTitleSavePath(Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view game_serial) { - return Config::GetSaveDataPath() / std::to_string(user_id) / game_serial; + return EmulatorSettings.GetHomeDir() / std::to_string(user_id) / "savedata" / game_serial; } -fs::path SaveInstance::MakeDirSavePath(Libraries::UserService::OrbisUserServiceUserId user_id, - std::string_view game_serial, std::string_view dir_name) { - return Config::GetSaveDataPath() / std::to_string(user_id) / game_serial / dir_name; +fs::path SaveInstance::MakeDirSavePath(OrbisUserServiceUserId user_id, std::string_view game_serial, + std::string_view dir_name) { + return EmulatorSettings.GetHomeDir() / std::to_string(user_id) / "savedata" / game_serial / + dir_name; } uint64_t SaveInstance::GetMaxBlockFromSFO(const PSF& psf) { @@ -71,7 +72,7 @@ fs::path SaveInstance::GetParamSFOPath(const fs::path& dir_path) { void SaveInstance::SetupDefaultParamSFO(PSF& param_sfo, std::string dir_name, std::string game_serial) { - int locale = Config::GetLanguage(); + int locale = EmulatorSettings.GetConsoleLanguage(); if (!default_title.contains(locale)) { locale = 1; // default to en_US if not found } diff --git a/src/core/libraries/save_data/savedata.cpp b/src/core/libraries/save_data/savedata.cpp index 48b086457..70c66e4cd 100644 --- a/src/core/libraries/save_data/savedata.cpp +++ b/src/core/libraries/save_data/savedata.cpp @@ -1,6 +1,7 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -8,13 +9,13 @@ #include #include "common/assert.h" -#include "common/config.h" #include "common/cstring.h" #include "common/elf_info.h" #include "common/enum.h" #include "common/logging/log.h" #include "common/path_util.h" #include "common/string_util.h" +#include "core/emulator_settings.h" #include "core/file_format/psf.h" #include "core/file_sys/fs.h" #include "core/libraries/error_codes.h" @@ -441,7 +442,8 @@ static Error saveDataMount(const OrbisSaveDataMount2* mount_info, LOG_INFO(Lib_SaveData, "called with invalid block size"); } - const auto root_save = Config::GetSaveDataPath(); + const auto root_save = + EmulatorSettings.GetHomeDir() / std::to_string(mount_info->userId) / "savedata"; fs::create_directories(root_save); const auto available = fs::space(root_save).available; @@ -489,7 +491,9 @@ static Error Umount(const OrbisSaveDataMountPoint* mountPoint, bool call_backup return Error::PARAMETER; } LOG_DEBUG(Lib_SaveData, "Umount mountPoint:{}", mountPoint->data.to_view()); - const std::string_view mount_point_str{mountPoint->data}; + + std::string mount_point_str = mountPoint->data.to_string(); + for (auto& instance : g_mount_slots) { if (instance.has_value()) { const auto& slot_name = instance->GetMountPoint(); diff --git a/src/core/libraries/sysmodule/sysmodule.cpp b/src/core/libraries/sysmodule/sysmodule.cpp new file mode 100644 index 000000000..1ad9075e7 --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule.cpp @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "common/elf_info.h" +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/kernel/process.h" +#include "core/libraries/libs.h" +#include "core/libraries/sysmodule/sysmodule.h" +#include "core/libraries/sysmodule/sysmodule_error.h" +#include "core/libraries/sysmodule/sysmodule_internal.h" +#include "core/linker.h" + +namespace Libraries::SysModule { + +static std::mutex g_mutex{}; + +s32 PS4_SYSV_ABI sceSysmoduleGetModuleHandleInternal(OrbisSysModuleInternal id, s32* handle) { + LOG_INFO(Lib_SysModule, "called"); + if ((id & 0x7fffffff) == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return getModuleHandle(id, handle); +} + +s32 PS4_SYSV_ABI sceSysmoduleGetModuleInfoForUnwind(VAddr addr, s32 flags, + Kernel::OrbisModuleInfoForUnwind* info) { + LOG_TRACE(Lib_SysModule, "sceSysmoduleGetModuleInfoForUnwind called"); + s32 res = Kernel::sceKernelGetModuleInfoForUnwind(addr, flags, info); + if (res != ORBIS_OK) { + return res; + } + + if (shouldHideName(info->name.data())) { + std::ranges::fill(info->name, '\0'); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleIsCalledFromSysModule() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleIsCameraPreloaded() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleIsLoaded(OrbisSysModule id) { + if (id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return getModuleHandle(id, nullptr); +} + +s32 PS4_SYSV_ABI sceSysmoduleIsLoadedInternal(OrbisSysModuleInternal id) { + if ((id & 0x7fffffff) == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return getModuleHandle(id, nullptr); +} + +s32 PS4_SYSV_ABI sceSysmoduleLoadModule(OrbisSysModule id) { + LOG_INFO(Lib_SysModule, "called, id = {:#x}", id); + s32 result = validateModuleId(id); + if (result < ORBIS_OK) { + return result; + } + + // Only locks for internal loadModule call. + { + std::scoped_lock lk{g_mutex}; + result = loadModule(id, 0, nullptr, nullptr); + } + + if (result == ORBIS_KERNEL_ERROR_ESTART) { + s32 sdk_ver = 0; + result = Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver); + if (sdk_ver < Common::ElfInfo::FW_115 || result != ORBIS_OK) { + return ORBIS_KERNEL_ERROR_EINVAL; + } else { + return ORBIS_KERNEL_ERROR_ESTART; + } + } + + // The real library has some weird workaround for CUSA01478 and CUSA01495 here. + // Unless this is proven necessary, I don't plan to handle this. + return result; +} + +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleByNameInternal() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleInternal(OrbisSysModuleInternal id) { + LOG_INFO(Lib_SysModule, "called, id = {:#x}", id); + s32 result = validateModuleId(id); + if (result < ORBIS_OK) { + return result; + } + + // This specific module ID is loaded unlocked. + if (id == 0x80000039) { + return loadModule(id, 0, nullptr, nullptr); + } + std::scoped_lock lk{g_mutex}; + return loadModule(id, 0, nullptr, nullptr); +} + +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleInternalWithArg(OrbisSysModuleInternal id, s32 argc, + const void* argv, u64 unk, s32* res_out) { + LOG_INFO(Lib_SysModule, "called, id = {:#x}", id); + s32 result = validateModuleId(id); + if (result < ORBIS_OK) { + return result; + } + + if (unk != 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return loadModule(id, argc, argv, res_out); +} + +s32 PS4_SYSV_ABI sceSysmoduleMapLibcForLibkernel() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmodulePreloadModuleForLibkernel() { + LOG_DEBUG(Lib_SysModule, "called"); + return preloadModulesForLibkernel(); +} + +s32 PS4_SYSV_ABI sceSysmoduleUnloadModule(OrbisSysModule id) { + LOG_ERROR(Lib_SysModule, "(STUBBED) called, id = {:#x}", id); + if (id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + std::scoped_lock lk{g_mutex}; + return unloadModule(id, 0, nullptr, nullptr, false); +} + +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleByNameInternal() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleInternal() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleInternalWithArg() { + LOG_ERROR(Lib_SysModule, "(STUBBED) called"); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("D8cuU4d72xM", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleGetModuleHandleInternal); + LIB_FUNCTION("4fU5yvOkVG4", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleGetModuleInfoForUnwind); + LIB_FUNCTION("ctfO7dQ7geg", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleIsCalledFromSysModule); + LIB_FUNCTION("no6T3EfiS3E", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleIsCameraPreloaded); + LIB_FUNCTION("fMP5NHUOaMk", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleIsLoaded); + LIB_FUNCTION("ynFKQ5bfGks", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleIsLoadedInternal); + LIB_FUNCTION("g8cM39EUZ6o", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleLoadModule); + LIB_FUNCTION("CU8m+Qs+HN4", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleLoadModuleByNameInternal); + LIB_FUNCTION("39iV5E1HoCk", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleLoadModuleInternal); + LIB_FUNCTION("hHrGoGoNf+s", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleLoadModuleInternalWithArg); + LIB_FUNCTION("lZ6RvVl0vo0", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleMapLibcForLibkernel); + LIB_FUNCTION("DOO+zuW1lrE", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmodulePreloadModuleForLibkernel); + LIB_FUNCTION("eR2bZFAAU0Q", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleUnloadModule); + LIB_FUNCTION("vpTHmA6Knvg", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleUnloadModuleByNameInternal); + LIB_FUNCTION("vXZhrtJxkGc", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleUnloadModuleInternal); + LIB_FUNCTION("aKa6YfBKZs4", "libSceSysmodule", 1, "libSceSysmodule", + sceSysmoduleUnloadModuleInternalWithArg); +}; + +} // namespace Libraries::SysModule diff --git a/src/core/libraries/sysmodule/sysmodule.h b/src/core/libraries/sysmodule/sysmodule.h new file mode 100644 index 000000000..17ac3188f --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/kernel/process.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::SysModule { + +using OrbisSysModule = u16; +using OrbisSysModuleInternal = u32; + +s32 PS4_SYSV_ABI sceSysmoduleGetModuleHandleInternal(OrbisSysModuleInternal id, s32* handle); +s32 PS4_SYSV_ABI sceSysmoduleGetModuleInfoForUnwind(VAddr addr, s32 flags, + Kernel::OrbisModuleInfoForUnwind* info); +s32 PS4_SYSV_ABI sceSysmoduleIsCalledFromSysModule(); +s32 PS4_SYSV_ABI sceSysmoduleIsCameraPreloaded(); +s32 PS4_SYSV_ABI sceSysmoduleIsLoaded(OrbisSysModule id); +s32 PS4_SYSV_ABI sceSysmoduleIsLoadedInternal(OrbisSysModuleInternal id); +s32 PS4_SYSV_ABI sceSysmoduleLoadModule(OrbisSysModule id); +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleByNameInternal(); +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleInternal(OrbisSysModuleInternal id); +s32 PS4_SYSV_ABI sceSysmoduleLoadModuleInternalWithArg(OrbisSysModuleInternal id, s32 argc, + const void* argv, u64 unk, s32* res_out); +s32 PS4_SYSV_ABI sceSysmoduleMapLibcForLibkernel(); +s32 PS4_SYSV_ABI sceSysmodulePreloadModuleForLibkernel(); +s32 PS4_SYSV_ABI sceSysmoduleUnloadModule(OrbisSysModule id); +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleByNameInternal(); +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleInternal(); +s32 PS4_SYSV_ABI sceSysmoduleUnloadModuleInternalWithArg(); + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::SysModule diff --git a/src/core/libraries/sysmodule/sysmodule_error.h b/src/core/libraries/sysmodule/sysmodule_error.h new file mode 100644 index 000000000..aee14b9df --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule_error.h @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +constexpr s32 ORBIS_SYSMODULE_INVALID_ID = 0x805A1000; +constexpr s32 ORBIS_SYSMODULE_NOT_LOADED = 0x805A1001; +constexpr s32 ORBIS_SYSMODULE_LOCK_FAILED = 0x805A10FF; \ No newline at end of file diff --git a/src/core/libraries/sysmodule/sysmodule_internal.cpp b/src/core/libraries/sysmodule/sysmodule_internal.cpp new file mode 100644 index 000000000..56e130289 --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule_internal.cpp @@ -0,0 +1,444 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "common/elf_info.h" +#include "common/logging/log.h" +#include "core/emulator_settings.h" +#include "core/file_sys/fs.h" +#include "core/libraries/disc_map/disc_map.h" +#include "core/libraries/font/font.h" +#include "core/libraries/font/fontft.h" +#include "core/libraries/jpeg/jpegenc.h" +#include "core/libraries/kernel/kernel.h" +#include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/libc_internal/libc_internal.h" +#include "core/libraries/libpng/pngenc.h" +#include "core/libraries/libs.h" +#include "core/libraries/ngs2/ngs2.h" +#include "core/libraries/rtc/rtc.h" +#include "core/libraries/sysmodule/sysmodule_error.h" +#include "core/libraries/sysmodule/sysmodule_internal.h" +#include "core/libraries/sysmodule/sysmodule_table.h" +#include "core/linker.h" +#include "emulator.h" + +namespace Libraries::SysModule { + +s32 getModuleHandle(s32 id, s32* handle) { + if (id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + for (OrbisSysmoduleModuleInternal mod : g_modules_array) { + if (mod.id != id) { + continue; + } + if (mod.is_loaded < 1) { + return ORBIS_SYSMODULE_NOT_LOADED; + } + if (handle != nullptr) { + *handle = mod.handle; + } + return ORBIS_OK; + } + return ORBIS_SYSMODULE_INVALID_ID; +} + +bool shouldHideName(const char* module_name) { + for (u64 i = 0; i < g_num_modules; i++) { + OrbisSysmoduleModuleInternal mod = g_modules_array[i]; + if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsGame) == 0) { + continue; + } + u64 name_length = std::strlen(mod.name); + char name_copy[0x100]; + std::strncpy(name_copy, mod.name, sizeof(name_copy)); + // Module table stores names without extensions, so check with .prx appended to the name. + std::strncpy(&name_copy[name_length], ".prx", 4); + s32 result = std::strncmp(module_name, name_copy, sizeof(name_copy)); + if (result == 0) { + return true; + } + + // libSceFios2 and libc are checked as both sprx or prx modules. + if (i == 3) { + result = std::strncmp(module_name, "libSceFios2.sprx", sizeof(name_copy)); + } else if (i == 4) { + result = std::strncmp(module_name, "libc.sprx", sizeof(name_copy)); + } + + if (result == 0) { + return true; + } + } + return false; +} + +bool isDebugModule(s32 id) { + for (OrbisSysmoduleModuleInternal mod : g_modules_array) { + if (mod.id == id && (mod.flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0) { + return true; + } + } + return false; +} + +bool validateModuleId(s32 id) { + if ((id & 0x7fffffff) == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + s32 sdk_ver = 0; + ASSERT_MSG(!Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver), + "Failed to retrieve compiled SDK version"); + + // libSceGameCustomDialog isn't loadable on SDK >= 7.50 + if (id == 0xb8 && sdk_ver >= Common::ElfInfo::FW_75) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // libSceNpSnsFacebookDialog isn't loadable on SDK >= 7.00 + if (id == 0xb0 && sdk_ver >= Common::ElfInfo::FW_70) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // libSceJson isn't loadable on SDK >= 3.00 + if (id == 0x80 && sdk_ver >= Common::ElfInfo::FW_30) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // Cannot load debug modules on retail hardware. + if (isDebugModule(id) && !EmulatorSettings.IsDevKit()) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + return ORBIS_OK; +} + +s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { + auto* mnt = Common::Singleton::Instance(); + auto* linker = Common::Singleton::Instance(); + auto* game_info = Common::Singleton::Instance(); + + // If the module is already loaded, increment is_loaded and return ORBIS_OK. + OrbisSysmoduleModuleInternal& mod = g_modules_array[index]; + if (mod.is_loaded > 0) { + mod.is_loaded++; + return ORBIS_OK; + } + + s32 start_result = 0; + // Most of the logic the actual module has here is to get the correct location of this module. + // Since we only care about a small subset of LLEs, we can simplify this logic. + if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsGame) != 0) { + std::string guest_path = std::string("/app0/sce_module/").append(mod.name); + guest_path.append(".prx"); + const auto& host_path = mnt->GetHostPath(guest_path); + + // For convenience, load through linker directly instead of loading through libkernel calls. + s32 result = linker->LoadAndStartModule(host_path, argc, argv, &start_result); + // If the module is missing, the library prints a very helpful message for developers. + // We'll just log an error. + if (result < 0) { + LOG_ERROR(Lib_SysModule, "Failed to load game library {}", guest_path); + return result; + } else { + // On success, the library validates module params and the module SDK version. + // We don't store the information this uses, so skip the proper checks. + mod.handle = result; + mod.is_loaded++; + } + } else { + // This is not a game library. We'll need to perform some checks, + // but we don't need to perform the path resolution logic the actual library has. + std::string mod_name = std::string(mod.name); + + // libSceGnmDriver case + if (index == 0xd && EmulatorSettings.IsDevKit()) { + // There are some other checks involved here that I am not familiar with. + // Since we're not exactly running libSceGnmDriver LLE, this shouldn't matter too much. + mod_name.append("_padebug"); + } + + // libSceSsl2 case + if (index == 0x27 && false /*needs legacy ssl*/) { + // Replaces module name with libSceSsl (index 0x15) + mod_name.clear(); + mod_name.append(g_modules_array[0x15].name); + } + + // libSceVrTracker case + if (index == 0xb3 && EmulatorSettings.IsDevKit()) { + mod_name.append("_debug"); + } + + if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsNeo) == 0 && + (mod.flags & OrbisSysmoduleModuleInternalFlags::IsNeoMode) != 0 && + Kernel::sceKernelIsNeoMode() == 1) { + // PS4 Pro running in enhanced mode + mod_name.append("ForNeoMode"); + } else if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsNeo) != 0 && + EmulatorSettings.IsNeo()) { + // PS4 Pro running in base mode + mod_name.append("ForNeo"); + } + + // Append .sprx extension. + mod_name.append(".sprx"); + + // Now we need to check if the requested library is allowed to LLE. + // First, we allow all modules from game-specific sys_modules + const auto& sys_module_path = EmulatorSettings.GetSysModulesDir(); + const auto& game_specific_module_path = + sys_module_path / game_info->GameSerial() / mod_name; + if (std::filesystem::exists(game_specific_module_path)) { + // The requested module is present in the game-specific sys_modules, load it. + LOG_INFO(Loader, "Loading {} from game serial file {}", mod_name, + game_info->GameSerial()); + s32 handle = + linker->LoadAndStartModule(game_specific_module_path, argc, argv, &start_result); + ASSERT_MSG(handle >= 0, "Failed to load module {}", mod_name); + mod.handle = handle; + mod.is_loaded++; + if (res_out != nullptr) { + *res_out = start_result; + } + return ORBIS_OK; + } + + // We need to check a few things here. + // First, check if this is a module we allow LLE for. + static s32 stub_handle = 100; + constexpr auto ModulesToLoad = std::to_array( + {{"libSceNgs2.sprx", &Libraries::Ngs2::RegisterLib}, + {"libSceUlt.sprx", nullptr}, + {"libSceRtc.sprx", &Libraries::Rtc::RegisterLib}, + {"libSceJpegDec.sprx", nullptr}, + {"libSceJpegEnc.sprx", &Libraries::JpegEnc::RegisterLib}, + {"libScePngEnc.sprx", &Libraries::PngEnc::RegisterLib}, + {"libSceJson.sprx", nullptr}, + {"libSceJson2.sprx", nullptr}, + {"libSceLibcInternal.sprx", &Libraries::LibcInternal::RegisterLib}, + {"libSceCesCs.sprx", nullptr}, + {"libSceAudiodec.sprx", nullptr}, + {"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont}, + {"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt}, + {"libSceFreeTypeOt.sprx", nullptr}}); + + // Iterate through the allowed array + const auto it = std::ranges::find_if( + ModulesToLoad, [&](Core::SysModules module) { return mod_name == module.module_name; }); + if (it == ModulesToLoad.end()) { + // Not an allowed LLE, stub success without warning. + mod.is_loaded++; + // Some internal checks rely on a handle, stub a valid one. + mod.handle = stub_handle++; + if (res_out != nullptr) { + *res_out = ORBIS_OK; + } + return ORBIS_OK; + } + + // Allowed module, check if it exists + const auto& module_path = sys_module_path / mod_name; + if (std::filesystem::exists(module_path)) { + LOG_INFO(Loader, "Loading {}", mod_name); + s32 handle = linker->LoadAndStartModule(module_path, argc, argv, &start_result); + ASSERT_MSG(handle >= 0, "Failed to load module {}", mod_name); + mod.handle = handle; + } else { + // Allowed LLE that isn't present, log message + auto& [name, init_func] = *it; + if (init_func) { + LOG_INFO(Loader, "Can't Load {} switching to HLE", mod_name); + init_func(&linker->GetHLESymbols()); + + // When loading HLEs, we need to relocate imports + // This ensures later module loads can see our HLE functions. + linker->RelocateAllImports(); + } else { + LOG_INFO(Loader, "No HLE available for {} module", mod_name); + } + mod.handle = stub_handle++; + } + + // Mark module as loaded. + mod.is_loaded++; + } + + // Only successful loads will reach here + if (res_out != nullptr) { + *res_out = start_result; + } + + return ORBIS_OK; +} + +s32 loadModule(s32 id, s32 argc, const void* argv, s32* res_out) { + // Retrieve the module to load from the table + OrbisSysmoduleModuleInternal requested_module{}; + for (OrbisSysmoduleModuleInternal mod : g_modules_array) { + if (mod.id == id) { + requested_module = mod; + break; + } + } + if (requested_module.id != id || requested_module.id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // Every module has a pointer to an array of indexes to modules that need loading. + if (requested_module.to_load == nullptr) { + // Seems like ORBIS_SYSMODULE_LOCK_FAILED is a generic internal error. + return ORBIS_SYSMODULE_LOCK_FAILED; + } + + LOG_INFO(Lib_SysModule, "Loading {}", requested_module.name); + + // Loop through every module that requires loading, in reverse order + for (s64 i = requested_module.num_to_load - 1; i >= 0; i--) { + // Modules flagged as debug modules only load for devkits + u32 mod_index = requested_module.to_load[i]; + if ((!EmulatorSettings.IsDevKit() && + g_modules_array[mod_index].flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0) { + continue; + } + + // Arguments and result should only be applied to the requested module + // Dependencies don't receive these values. + s32 result = 0; + if (i != 0) { + result = loadModuleInternal(mod_index, 0, nullptr, nullptr); + } else { + result = loadModuleInternal(mod_index, argc, argv, res_out); + } + + // If loading any module fails, abort there. + if (result != ORBIS_OK) { + return result; + } + } + return ORBIS_OK; +} + +s32 unloadModule(s32 id, s32 argc, const void* argv, s32* res_out, bool is_internal) { + OrbisSysmoduleModuleInternal mod{}; + for (s32 i = 0; i < g_modules_array.size(); i++) { + mod = g_modules_array[i]; + if (mod.id != id) { + continue; + } + + // Skips checking libSceDiscMap + if (i == 0x22) { + continue; + } + + // If the module is loaded once, and is part of the second preload list, + // then return OK and do nothing. + for (s32 index : g_preload_list_2) { + if (index == i && mod.is_loaded == 1) { + return ORBIS_OK; + } + } + + // Found the correct module. + break; + } + + // If we failed to locate the module, return invalid id. + if (mod.id != id || mod.id == 0) { + return ORBIS_SYSMODULE_INVALID_ID; + } + + // If the module has no dependencies, then return an internal error. + if (mod.num_to_load == 0 || mod.to_load == nullptr) { + return ORBIS_SYSMODULE_LOCK_FAILED; + } + + // Unload the module and it's dependencies + for (s64 i = 0; i < mod.num_to_load; i++) { + OrbisSysmoduleModuleInternal dep_mod = g_modules_array[mod.to_load[i]]; + // If this is a debug module and we're not emulating a devkit, skip it. + if ((dep_mod.flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0 && + !EmulatorSettings.IsDevKit()) { + continue; + } + + // If the module to unload is marked as unloaded, then return not loaded + if (dep_mod.is_loaded == 0) { + return ORBIS_SYSMODULE_NOT_LOADED; + } + + // By this point, all necessary checks are performed, decrement the load count. + dep_mod.is_loaded--; + + // Normally, this is where the real library would actually unload the module, + // through a call to sceKernelStopUnloadModule. + // As we don't implement module unloading, this behavior is skipped. + + // Stub success during requested module unload. + if (i == 0 && res_out != nullptr) { + *res_out = ORBIS_OK; + } + } + return ORBIS_OK; +} + +s32 preloadModulesForLibkernel() { + // For now, default to loading g_preload_list_3. + // As far as I can tell, g_preload_list_1 seems to be some sort of list with libs + // that games don't typically use, and g_preload_list_2 is just a reorganized version of 3. + s32 sdk_ver = 0; + ASSERT_MSG(Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver) == 0, + "Failed to get compiled SDK version"); + for (u32 module_index : g_preload_list_3) { + // As per usual, these are arrays of indexes for g_modules_array + // libSceDbg, libScePerf, libSceMat, and libSceRazorCpu_debug. + // These are skipped unless this console is a devkit. + if ((module_index == 0x12 || module_index == 0x1e || module_index == 0x24 || + module_index == 0x26) && + !EmulatorSettings.IsDevKit()) { + continue; + } + + // libSceDiscMap case, skipped on newer SDK versions. + if (module_index == 0x22 && sdk_ver >= Common::ElfInfo::FW_20) { + continue; + } + + // libSceDbgAssist is skipped on non-testkit consoles. + // For now, stub check to non-devkit. + if (module_index == 0x23 && !EmulatorSettings.IsDevKit()) { + continue; + } + + // libSceRazorCpu, skipped for old non-devkit consoles. + if (module_index == 0x25 && sdk_ver < Common::ElfInfo::FW_45 && + !EmulatorSettings.IsDevKit()) { + continue; + } + + // libSceHttp2, skipped for SDK versions below 7.00. + if (module_index == 0x28 && sdk_ver < Common::ElfInfo::FW_70) { + continue; + } + + // libSceNpWebApi2 and libSceNpGameIntent, skipped for SDK versions below 7.50 + if ((module_index == 0x29 || module_index == 0x2a) && sdk_ver < Common::ElfInfo::FW_75) { + continue; + } + + // Load the actual module + s32 result = loadModuleInternal(module_index, 0, nullptr, nullptr); + if (result != ORBIS_OK) { + // On real hardware, module preloading must succeed or the game will abort. + // To enable users to test homebrew easier, we'll log a critical error instead. + LOG_CRITICAL(Lib_SysModule, "Failed to preload {}, expect crashes", + g_modules_array[module_index].name); + } + } + return ORBIS_OK; +} + +} // namespace Libraries::SysModule diff --git a/src/core/libraries/sysmodule/sysmodule_internal.h b/src/core/libraries/sysmodule/sysmodule_internal.h new file mode 100644 index 000000000..8f88f85ea --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule_internal.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/kernel/process.h" + +namespace Libraries::SysModule { + +s32 getModuleHandle(s32 id, s32* handle); +bool shouldHideName(const char* module_name); +bool isDebugModule(s32 id); +bool validateModuleId(s32 id); +s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out); +s32 loadModule(s32 id, s32 argc, const void* argv, s32* res_out); +s32 unloadModule(s32 id, s32 argc, const void* argv, s32* res_out, bool is_internal); +s32 preloadModulesForLibkernel(); + +} // namespace Libraries::SysModule \ No newline at end of file diff --git a/src/core/libraries/sysmodule/sysmodule_table.h b/src/core/libraries/sysmodule/sysmodule_table.h new file mode 100644 index 000000000..bd27a8aae --- /dev/null +++ b/src/core/libraries/sysmodule/sysmodule_table.h @@ -0,0 +1,684 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Libraries::SysModule { + +/** + * libSceSysmodule hardcodes an array of valuable data about loading each PS4 module. + * This header stores the contents of this array, as dumped from 12.52's libSceSysmodule, + * and altered to fit within my simplified internal module struct. + */ + +// This is an internal struct. Doesn't match the real one exactly. +struct OrbisSysmoduleModuleInternal { + u32 id; // User requested ID + s32 handle; // Handle of the module, once loaded + s32 is_loaded; // 0 by default, set to 1 once loaded. + s32 flags; // Miscellaneous details about the module + const char* name; // Name of the actual SPRX/PRX library + const u16* to_load; // Pointer to an array of modules to load + s32 num_to_load; // Number of indicies in the array of modules +}; + +// This enum contains helpful identifiers for some bits used in the flags of a module. +enum OrbisSysmoduleModuleInternalFlags : s32 { + IsCommon = 1, // Module is located in /system/common/lib + IsPriv = 2, // Module is located in /system/priv/lib + IsGame = 4, // Module is located in /app0/sce_module + IsDebug = 8, // Module should only be loaded on devkit/testkit consoles + IsNeo = 0x200, // Module should only be loaded on PS4 Pro consoles + IsNeoMode = 0x400, // Module should only be loaded for PS4 Pro running in enhanced mode + IsCommonEx = 0x1000, // Module is located in /system_ex/common_ex/lib + IsPrivEx = 0x2000, // Module is located in /system_ex/priv_ex/lib +}; + +// Array of module indexes to load in sceSysmodulePreloadModuleForLibkernel. +// The library has three versions of this array +u32 g_preload_list_1[36] = {0x24, 3, 4, 5, 6, 7, 8, 9, 0x25, 0xb, 0xc, 0xd, + 0xe, 0xf, 0x10, 0x11, 0x1f, 0x12, 0x13, 0x14, 0x27, 0x28, 0x16, 0x17, + 0x2a, 0x18, 0x29, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x26, 0x1e, 0x20, 0x21}; +u32 g_preload_list_2[38] = {1, 2, 0x24, 0x22, 3, 4, 5, 6, 7, 8, + 9, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x1f, 0x12, + 0x23, 0x13, 0x14, 0x27, 0x28, 0x16, 0x17, 0x2a, 0x18, 0x29, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x25, 0x26, 0x1e}; +u32 g_preload_list_3[38] = {1, 2, 0x24, 0x22, 3, 4, 5, 6, 7, 8, + 9, 0x25, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x1f, + 0x12, 0x23, 0x13, 0x14, 0x27, 0x28, 0x16, 0x17, 0x2a, 0x18, + 0x29, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x26, 0x1e}; + +// Arrays of modules to load for each module. +// The stored values are valid indices to modules in g_modules_array. +u16 g_libSceNet_modules[1] = {5}; +u16 g_libSceIpmi_modules[1] = {6}; +u16 g_libSceMbus_modules[2] = {7, 6}; +u16 g_libSceRegMgr_modules[1] = {8}; +u16 g_libSceRtc_modules[1] = {9}; +u16 g_libSceAvSetting_modules[3] = {11, 7, 6}; +u16 g_libSceVideoOut_modules[3] = {12, 11, 7}; +u16 g_libSceGnmDriver_modules[4] = {13, 12, 8, 37}; +u16 g_libSceAudioOut_modules[4] = {14, 11, 7, 6}; +u16 g_libSceAudioIn_modules[4] = {15, 14, 7, 6}; +u16 g_libSceAjm_modules[1] = {16}; +u16 g_libScePad_modules[2] = {17, 7}; +u16 g_libSceDbg_debug_modules[1] = {18}; +u16 g_libSceNetCtl_modules[2] = {19, 6}; +u16 g_libSceHttp_modules[5] = {20, 39, 9, 19, 5}; +u16 g_libSceSsl_modules[3] = {21, 9, 5}; +u16 g_libSceNpCommon_modules[8] = {22, 20, 39, 19, 9, 8, 6, 5}; +u16 g_libSceNpManager_modules[7] = {23, 22, 20, 39, 19, 9, 5}; +u16 g_libSceNpWebApi_modules[7] = {24, 23, 22, 20, 39, 9, 5}; +u16 g_libSceSaveData_modules[4] = {25, 27, 9, 6}; +u16 g_libSceSystemService_modules[3] = {26, 8, 6}; +u16 g_libSceUserService_modules[2] = {27, 6}; +u16 g_libSceCommonDialog_modules[1] = {28}; +u16 g_libSceSysUtil_modules[2] = {29, 8}; +u16 g_libScePerf_debug_modules[3] = {30, 38, 37}; +u16 g_libSceCamera_modules[2] = {31, 7}; +u16 g_libSceDiscMap_modules[1] = {34}; +u16 g_libSceDbgAssist_modules[1] = {35}; +u16 g_libSceMat_debug_modules[1] = {36}; +u16 g_libSceRazorCpu_modules[1] = {37}; +u16 g_libSceRazorCpu_debug_debug_modules[2] = {38, 37}; +u16 g_libSceSsl2_modules[3] = {39, 9, 5}; +u16 g_libSceHttp2_modules[13] = {40, 20, 39, 9, 19, 5, 39, 9, 5, 9, 19, 6, 5}; +u16 g_libSceNpWebApi2_modules[39] = {41, 23, 22, 20, 39, 19, 9, 5, 22, 20, 39, 19, 9, + 8, 6, 5, 40, 20, 39, 9, 19, 5, 39, 9, 5, 9, + 19, 6, 5, 20, 39, 9, 19, 5, 39, 9, 5, 9, 5}; +u16 g_libSceNpGameIntent_modules[10] = {42, 22, 20, 39, 19, 9, 8, 6, 5, 6}; +u16 g_libSceFiber_modules[5] = {49, 114, 30, 38, 37}; +u16 g_libSceUlt_modules[6] = {50, 49, 114, 30, 38, 37}; +u16 g_libSceNgs2_modules[2] = {51, 16}; +u16 g_libSceXml_modules[1] = {52}; +u16 g_libSceNpUtility_modules[5] = {53, 22, 20, 19, 5}; +u16 g_libSceVoice_modules[4] = {54, 16, 15, 14}; +u16 g_libSceNpMatching2_modules[7] = {55, 23, 22, 20, 39, 19, 5}; +u16 g_libSceNpScoreRanking_modules[3] = {56, 23, 22}; +u16 g_libSceRudp_modules[1] = {57}; +u16 g_libSceNpTus_modules[3] = {58, 23, 22}; +u16 g_libSceFace_modules[1] = {59}; +u16 g_libSceSmart_modules[1] = {60}; +u16 g_libSceJson_modules[1] = {61}; +u16 g_libSceGameLiveStreaming_modules[2] = {62, 6}; +u16 g_libSceCompanionUtil_modules[3] = {63, 7, 6}; +u16 g_libScePlayGo_modules[1] = {64}; +u16 g_libSceFont_modules[1] = {65}; +u16 g_libSceVideoRecording_modules[2] = {66, 82}; +u16 g_libSceAudiodec_modules[2] = {67, 16}; +u16 g_libSceJpegDec_modules[1] = {68}; +u16 g_libSceJpegEnc_modules[1] = {69}; +u16 g_libScePngDec_modules[1] = {70}; +u16 g_libScePngEnc_modules[1] = {71}; +u16 g_libSceVideodec_modules[3] = {72, 80, 161}; +u16 g_libSceMove_modules[1] = {73}; +u16 g_libScePadTracker_modules[2] = {75, 17}; +u16 g_libSceDepth_modules[2] = {76, 31}; +u16 g_libSceHand_modules[1] = {77}; +u16 g_libSceIme_modules[2] = {78, 6}; +u16 g_libSceImeDialog_modules[2] = {79, 6}; +u16 g_libSceVdecCore_modules[1] = {80}; +u16 g_libSceNpParty_modules[2] = {81, 6}; +u16 g_libSceAvcap_modules[2] = {82, 6}; +u16 g_libSceFontFt_modules[1] = {83}; +u16 g_libSceFreeTypeOt_modules[1] = {84}; +u16 g_libSceFreeTypeOl_modules[1] = {85}; +u16 g_libSceFreeTypeOptOl_modules[1] = {86}; +u16 g_libSceScreenShot_modules[3] = {87, 29, 6}; +u16 g_libSceNpAuth_modules[3] = {88, 22, 23}; +u16 g_libSceVoiceQos_modules[5] = {89, 54, 16, 15, 14}; +u16 g_libSceSysCore_modules[2] = {90, 6}; +u16 g_libSceM4aacEnc_modules[2] = {91, 16}; +u16 g_libSceAudiodecCpu_modules[1] = {92}; +u16 g_libSceCdlgUtilServer_modules[2] = {93, 26}; +u16 g_libSceSulpha_debug_modules[1] = {94}; +u16 g_libSceSaveDataDialog_modules[4] = {95, 9, 28, 26}; +u16 g_libSceInvitationDialog_modules[1] = {96}; +u16 g_libSceKeyboard_debug_modules[1] = {97}; +u16 g_libSceKeyboard_modules[1] = {98}; +u16 g_libSceMsgDialog_modules[1] = {99}; +u16 g_libSceAvPlayer_modules[1] = {100}; +u16 g_libSceContentExport_modules[1] = {101}; +u16 g_libSceVisionManager_modules[1] = {102}; +u16 g_libSceAc3Enc_modules[2] = {103, 16}; +u16 g_libSceAppInstUtil_modules[1] = {104}; +u16 g_libSceVencCore_modules[1] = {105}; +u16 g_libSceAudio3d_modules[1] = {106}; +u16 g_libSceNpCommerce_modules[1] = {107}; +u16 g_libSceHidControl_modules[1] = {108}; +u16 g_libSceMouse_modules[1] = {109}; +u16 g_libSceCompanionHttpd_modules[1] = {110}; +u16 g_libSceWebBrowserDialog_modules[1] = {111}; +u16 g_libSceErrorDialog_modules[1] = {112}; +u16 g_libSceNpTrophy_modules[1] = {113}; +u16 g_ulobjmgr_modules[1] = {114}; +u16 g_libSceVideoCoreInterface_modules[1] = {115}; +u16 g_libSceVideoCoreServerInterface_modules[1] = {116}; +u16 g_libSceNpSns_modules[1] = {117}; +u16 g_libSceNpSnsFacebookDialog_modules[2] = {118, 117}; +u16 g_libSceMoveTracker_modules[1] = {119}; +u16 g_libSceNpProfileDialog_modules[1] = {120}; +u16 g_libSceNpFriendListDialog_modules[1] = {121}; +u16 g_libSceAppContent_modules[1] = {122}; +u16 g_libSceMarlin_modules[1] = {123}; +u16 g_libSceDtsEnc_modules[2] = {124, 16}; +u16 g_libSceNpSignaling_modules[1] = {125}; +u16 g_libSceRemoteplay_modules[1] = {126}; +u16 g_libSceUsbd_modules[1] = {127}; +u16 g_libSceGameCustomDataDialog_modules[1] = {128}; +u16 g_libSceNpEulaDialog_modules[1] = {129}; +u16 g_libSceRandom_modules[1] = {130}; +u16 g_libSceDipsw_modules[1] = {131}; +u16 g_libSceS3DConversion_modules[1] = {132}; +u16 g_libSceOttvCapture_debug_modules[1] = {133}; +u16 g_libSceBgft_modules[1] = {134}; +u16 g_libSceAudiodecCpuDdp_modules[1] = {135}; +u16 g_libSceAudiodecCpuM4aac_modules[1] = {136}; +u16 g_libSceAudiodecCpuDts_modules[1] = {137}; +u16 g_libSceAudiodecCpuDtsHdLbr_modules[1] = {138}; +u16 g_libSceAudiodecCpuDtsHdMa_modules[1] = {139}; +u16 g_libSceAudiodecCpuLpcm_modules[1] = {140}; +u16 g_libSceBemp2sys_modules[1] = {141}; +u16 g_libSceBeisobmf_modules[1] = {142}; +u16 g_libScePlayReady_modules[1] = {143}; +u16 g_libSceVideoNativeExtEssential_modules[1] = {144}; +u16 g_libSceZlib_modules[1] = {145}; +u16 g_libSceIduUtil_modules[1] = {146}; +u16 g_libScePsm_modules[1] = {147}; +u16 g_libSceDtcpIp_modules[1] = {148}; +u16 g_libSceKbEmulate_modules[1] = {149}; +u16 g_libSceAppChecker_modules[1] = {150}; +u16 g_libSceNpGriefReport_modules[1] = {151}; +u16 g_libSceContentSearch_modules[1] = {152}; +u16 g_libSceShareUtility_modules[1] = {153}; +u16 g_libSceWeb_modules[6] = {154, 155, 147, 192, 27, 6}; +u16 g_libSceWebKit2_modules[30] = {155, 266, 90, 6, 8, 255, 192, 116, 266, 90, 6, 8, 12, 11, 7, + 17, 7, 26, 8, 6, 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceDeci4h_debug_modules[1] = {156}; +u16 g_libSceHeadTracker_modules[1] = {157}; +u16 g_libSceGameUpdate_modules[2] = {158, 6}; +u16 g_libSceAutoMounterClient_modules[2] = {159, 6}; +u16 g_libSceSystemGesture_modules[1] = {160}; +u16 g_libSceVdecSavc_modules[1] = {161}; +u16 g_libSceVdecSavc2_modules[1] = {162}; +u16 g_libSceVideodec2_modules[3] = {163, 80, 162}; +u16 g_libSceVdecwrap_modules[2] = {164, 80}; +u16 g_libSceVshctl_modules[1] = {165}; +u16 g_libSceAt9Enc_modules[1] = {166}; +u16 g_libSceConvertKeycode_modules[1] = {167}; +u16 g_libSceGpuException_modules[1] = {168}; +u16 g_libSceSharePlay_modules[1] = {169}; +u16 g_libSceAudiodReport_modules[1] = {170}; +u16 g_libSceSulphaDrv_modules[1] = {171}; +u16 g_libSceHmd_modules[1] = {172}; +u16 g_libSceUsbStorage_modules[2] = {173, 6}; +u16 g_libSceVdecShevc_modules[1] = {174}; +u16 g_libSceUsbStorageDialog_modules[1] = {175}; +u16 g_libSceFaceTracker_modules[2] = {176, 59}; +u16 g_libSceHandTracker_modules[1] = {177}; +u16 g_libSceNpSnsYouTubeDialog_modules[2] = {178, 117}; +u16 g_libSceVrTracker_modules[6] = {179, 6, 172, 31, 17, 73}; +u16 g_libSceProfileCacheExternal_modules[2] = {180, 6}; +u16 g_libSceBackupRestoreUtil_modules[1] = {181}; +u16 g_libSceMusicPlayerService_modules[2] = {182, 183}; +u16 g_libSceMusicCoreServerClientJsEx_modules[1] = {183}; +u16 g_libSceSpSysCallWrapper_modules[3] = {184, 19, 6}; +u16 g_libScePs2EmuMenuDialog_modules[1] = {185}; +u16 g_libSceNpSnsDailyMotionDialog_modules[1] = {186}; +u16 g_libSceAudiodecCpuHevag_modules[1] = {187}; +u16 g_libSceLoginDialog_modules[2] = {188, 6}; +u16 g_libSceLoginService_modules[2] = {189, 6}; +u16 g_libSceSigninDialog_modules[2] = {190, 6}; +u16 g_libSceVdecsw_modules[3] = {191, 80, 162}; +u16 g_libSceOrbisCompat_modules[24] = {192, 116, 266, 90, 6, 8, 12, 11, 7, 17, 7, 26, + 8, 6, 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceCoreIPC_modules[1] = {193}; +u16 g_libSceCustomMusicCore_modules[12] = {194, 29, 8, 27, 6, 14, 11, 7, 6, 11, 7, 6}; +u16 g_libSceJson2_modules[1] = {195}; +u16 g_libSceAudioLatencyEstimation_modules[1] = {196}; +u16 g_libSceWkFontConfig_modules[1] = {197}; +u16 g_libSceVorbisDec_modules[3] = {198, 67, 16}; +u16 g_libSceTtsCoreEnUs_modules[1] = {199}; +u16 g_libSceTtsCoreJp_modules[1] = {200}; +u16 g_libSceOpusCeltEnc_modules[2] = {201, 16}; +u16 g_libSceOpusCeltDec_modules[2] = {202, 16}; +u16 g_libSceLoginMgrServer_modules[1] = {203}; +u16 g_libSceHmdSetupDialog_modules[1] = {204}; +u16 g_libSceVideoOutSecondary_modules[6] = {205, 82, 6, 12, 11, 7}; +u16 g_libSceContentDelete_modules[1] = {206}; +u16 g_libSceImeBackend_modules[1] = {207}; +u16 g_libSceNetCtlApDialog_modules[1] = {208}; +u16 g_libSceGnmResourceRegistration_modules[1] = {209}; +u16 g_libScePlayGoDialog_modules[1] = {210}; +u16 g_libSceSocialScreen_modules[7] = {211, 205, 82, 6, 12, 11, 7}; +u16 g_libSceEditMp4_modules[1] = {212}; +u16 g_libScePsmKitSystem_modules[1] = {221}; +u16 g_libSceTextToSpeech_modules[1] = {222}; +u16 g_libSceNpToolkit_modules[1] = {223}; +u16 g_libSceCustomMusicService_modules[2] = {224, 183}; +u16 g_libSceClSysCallWrapper_modules[11] = {225, 20, 39, 9, 19, 5, 39, 9, 5, 67, 16}; +u16 g_libSceScm_modules[1] = {226}; +u16 g_libSceSystemLogger_modules[2] = {227, 6}; +u16 g_libSceBluetoothHid_modules[1] = {228}; +u16 g_libSceAvPlayerStreaming_modules[1] = {229}; +u16 g_libSceAudiodecCpuAlac_modules[1] = {230}; +u16 g_libSceVideoDecoderArbitration_modules[1] = {231}; +u16 g_libSceVrServiceDialog_modules[1] = {232}; +u16 g_libSceJobManager_modules[2] = {233, 114}; +u16 g_libSceAudiodecCpuFlac_modules[1] = {234}; +u16 g_libSceSrcUtl_modules[2] = {235, 16}; +u16 g_libSceS3da_modules[1] = {236}; +u16 g_libSceDseehx_modules[1] = {237}; +u16 g_libSceShareFactoryUtil_modules[1] = {238}; +u16 g_libSceDataTransfer_modules[1] = {239}; +u16 g_libSceSocialScreenDialog_modules[1] = {240}; +u16 g_libSceAbstractStorage_modules[1] = {241}; +u16 g_libSceImageUtil_modules[1] = {242}; +u16 g_libSceMetadataReaderWriter_modules[1] = {243}; +u16 g_libSceJpegParser_modules[1] = {244}; +u16 g_libSceGvMp4Parser_modules[1] = {245}; +u16 g_libScePngParser_modules[1] = {246}; +u16 g_libSceGifParser_modules[1] = {247}; +u16 g_libSceNpSnsDialog_modules[2] = {248, 117}; +u16 g_libSceAbstractLocal_modules[1] = {249}; +u16 g_libSceAbstractFacebook_modules[1] = {250}; +u16 g_libSceAbstractYoutube_modules[1] = {251}; +u16 g_libSceAbstractTwitter_modules[1] = {252}; +u16 g_libSceAbstractDailymotion_modules[1] = {253}; +u16 g_libSceNpToolkit2_modules[1] = {254}; +u16 g_libScePrecompiledShaders_modules[1] = {255}; +u16 g_libSceDiscId_modules[1] = {256}; +u16 g_libSceLibreSsl_modules[2] = {257, 130}; +u16 g_libSceFsInternalForVsh_modules[1] = {258}; +u16 g_libSceNpUniversalDataSystem_modules[1] = {259}; +u16 g_libSceDolbyVision_modules[1] = {260}; +u16 g_libSceOpusSilkEnc_modules[2] = {261, 16}; +u16 g_libSceOpusDec_modules[2] = {262, 16}; +u16 g_libSceWebKit2Secure_modules[34] = {263, 265, 26, 8, 6, 266, 90, 6, 8, 255, 192, 116, + 266, 90, 6, 8, 12, 11, 7, 17, 7, 26, 8, 6, + 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceJscCompiler_modules[1] = {264}; +u16 g_libSceJitBridge_modules[4] = {265, 26, 8, 6}; +u16 g_libScePigletv2VSH_modules[4] = {266, 90, 6, 8}; +u16 g_libSceJitBridge_common_ex_modules[4] = {267, 26, 8, 6}; +u16 g_libSceJscCompiler_common_ex_modules[1] = {268}; +u16 g_libSceOrbisCompat_common_ex_modules[24] = {269, 116, 266, 90, 6, 8, 12, 11, 7, 17, 7, 26, + 8, 6, 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceWeb_common_ex_modules[6] = {270, 271, 147, 269, 27, 6}; +u16 g_libSceWebKit2_common_ex_modules[30] = {271, 266, 90, 6, 8, 273, 269, 116, 266, 90, + 6, 8, 12, 11, 7, 17, 7, 26, 8, 6, + 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libSceWebKit2Secure_common_ex_modules[34] = { + 272, 267, 26, 8, 6, 266, 90, 6, 8, 273, 269, 116, 266, 90, 6, 8, 12, + 11, 7, 17, 7, 26, 8, 6, 257, 130, 39, 9, 5, 19, 6, 5, 8, 9}; +u16 g_libScePrecompiledShaders_common_ex_modules[1] = {273}; +u16 g_libSceGic_modules[1] = {274}; +u16 g_libSceRnpsAppMgr_modules[1] = {275}; +u16 g_libSceAsyncStorageInternal_modules[1] = {276}; +u16 g_libSceHttpCache_modules[1] = {277}; +u16 g_libScePlayReady2_modules[1] = {278}; +u16 g_libSceHdrScopes_debug_modules[1] = {279}; +u16 g_libSceNKWeb_modules[1] = {280}; +u16 g_libSceNKWebKit_modules[2] = {281, 282}; +u16 g_libSceNKWebKitRequirements_modules[1] = {282}; +u16 g_libSceVnaInternal_modules[1] = {283}; +u16 g_libSceVnaWebsocket_modules[1] = {284}; +u16 g_libSceCesCs_modules[1] = {285}; +u16 g_libSceComposite_modules[1] = {286}; +u16 g_libSceCompositeExt_modules[1] = {287}; +u16 g_libSceHubAppUtil_modules[1] = {288}; +u16 g_libScePosixForWebKit_modules[1] = {289}; +u16 g_libSceNpPartner001_modules[1] = {290}; +u16 g_libSceNpSessionSignaling_modules[75] = { + 291, 41, 23, 22, 20, 39, 19, 9, 5, 22, 20, 39, 19, 9, 8, 6, 5, 40, 20, 39, 9, 19, 5, 39, 9, + 5, 9, 19, 6, 5, 20, 39, 9, 19, 5, 39, 9, 5, 9, 5, 22, 20, 39, 19, 9, 8, 6, 5, 23, 22, + 20, 39, 19, 9, 5, 40, 20, 39, 9, 19, 5, 39, 9, 5, 9, 19, 6, 5, 39, 9, 5, 19, 6, 5, 9}; +u16 g_libScePlayerInvitationDialog_modules[1] = {292}; +u16 g_libSceNpCppWebApi_modules[42] = {293, 195, 41, 23, 22, 20, 39, 19, 9, 5, 22, 20, 39, 19, + 9, 8, 6, 5, 40, 20, 39, 9, 19, 5, 39, 9, 5, 9, + 19, 6, 5, 20, 39, 9, 19, 5, 39, 9, 5, 9, 5, 9}; +u16 g_libSceNpEntitlementAccess_modules[1] = {294}; +u16 g_libSceNpRemotePlaySessionSignaling_modules[76] = { + 295, 291, 41, 23, 22, 20, 39, 19, 9, 5, 22, 20, 39, 19, 9, 8, 6, 5, 40, + 20, 39, 9, 19, 5, 39, 9, 5, 9, 19, 6, 5, 20, 39, 9, 19, 5, 39, 9, + 5, 9, 5, 22, 20, 39, 19, 9, 8, 6, 5, 23, 22, 20, 39, 19, 9, 5, 40, + 20, 39, 9, 19, 5, 39, 9, 5, 9, 19, 6, 5, 39, 9, 5, 19, 6, 5, 9}; +u16 g_libSceLibreSsl3_modules[2] = {296, 130}; +u16 g_libcurl_modules[2] = {297, 289}; +u16 g_libicu_modules[2] = {298, 289}; +u16 g_libcairo_modules[9] = {299, 300, 301, 302, 303, 289, 298, 289, 289}; +u16 g_libfontconfig_modules[1] = {300}; +u16 g_libfreetype_modules[1] = {301}; +u16 g_libharfbuzz_modules[1] = {302}; +u16 g_libpng16_modules[2] = {303, 289}; +u16 g_libSceFontGs_modules[1] = {304}; +u16 g_libSceGLSlimClientVSH_modules[1] = {305}; +u16 g_libSceGLSlimServerVSH_modules[1] = {306}; +u16 g_libSceFontGsm_modules[1] = {307}; +u16 g_libSceNpPartnerSubscription_modules[1] = {308}; +u16 g_libSceNpAuthAuthorizedAppDialog_modules[1] = {309}; + +// This is the actual array of modules. +constexpr u64 g_num_modules = 310; +std::array g_modules_array = std::to_array< + OrbisSysmoduleModuleInternal>( + {{0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 1, "libkernel", nullptr, 0}, + {0x0, -1, 0, 1, "libSceLibcInternal", nullptr, 0}, + {0x0, -1, 0, 4, "libSceFios2", nullptr, 0}, + {0x0, -1, 0, 4, "libc", nullptr, 0}, + {0x8000001c, -1, 0, 1, "libSceNet", g_libSceNet_modules, 1}, + {0x8000001d, -1, 0, 1, "libSceIpmi", g_libSceIpmi_modules, 1}, + {0x8000001e, -1, 0, 1, "libSceMbus", g_libSceMbus_modules, 2}, + {0x8000001f, -1, 0, 1, "libSceRegMgr", g_libSceRegMgr_modules, 1}, + {0x80000020, -1, 0, 1, "libSceRtc", g_libSceRtc_modules, 1}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x80000021, -1, 0, 1, "libSceAvSetting", g_libSceAvSetting_modules, 3}, + {0x80000022, -1, 0, 1, "libSceVideoOut", g_libSceVideoOut_modules, 3}, + {0x80000052, -1, 0, 1025, "libSceGnmDriver", g_libSceGnmDriver_modules, 4}, + {0x80000001, -1, 0, 1, "libSceAudioOut", g_libSceAudioOut_modules, 4}, + {0x80000002, -1, 0, 1, "libSceAudioIn", g_libSceAudioIn_modules, 4}, + {0x80000023, -1, 0, 1, "libSceAjm", g_libSceAjm_modules, 1}, + {0x80000024, -1, 0, 1, "libScePad", g_libScePad_modules, 2}, + {0x80000025, -1, 0, 9, "libSceDbg", g_libSceDbg_debug_modules, 1}, + {0x80000009, -1, 0, 1, "libSceNetCtl", g_libSceNetCtl_modules, 2}, + {0x8000000a, -1, 0, 1, "libSceHttp", g_libSceHttp_modules, 5}, + {0x0, -1, 0, 1, "libSceSsl", g_libSceSsl_modules, 3}, + {0x8000000c, -1, 0, 1, "libSceNpCommon", g_libSceNpCommon_modules, 8}, + {0x8000000d, -1, 0, 1, "libSceNpManager", g_libSceNpManager_modules, 7}, + {0x8000000e, -1, 0, 1, "libSceNpWebApi", g_libSceNpWebApi_modules, 7}, + {0x8000000f, -1, 0, 1, "libSceSaveData", g_libSceSaveData_modules, 4}, + {0x80000010, -1, 0, 1, "libSceSystemService", g_libSceSystemService_modules, 3}, + {0x80000011, -1, 0, 1, "libSceUserService", g_libSceUserService_modules, 2}, + {0x80000018, -1, 0, 1, "libSceCommonDialog", g_libSceCommonDialog_modules, 1}, + {0x80000026, -1, 0, 1, "libSceSysUtil", g_libSceSysUtil_modules, 2}, + {0x80000019, -1, 0, 9, "libScePerf", g_libScePerf_debug_modules, 3}, + {0x8000001a, -1, 0, 1, "libSceCamera", g_libSceCamera_modules, 2}, + {0x0, -1, 0, 1, "libSceWebKit2ForVideoService", nullptr, 0}, + {0x0, -1, 0, 1, "libSceOrbisCompatForVideoService", nullptr, 0}, + {0xd7, -1, 0, 1, "libSceDiscMap", g_libSceDiscMap_modules, 1}, + {0x8000003d, -1, 0, 129, "libSceDbgAssist", g_libSceDbgAssist_modules, 1}, + {0x80000048, -1, 0, 9, "libSceMat", g_libSceMat_debug_modules, 1}, + {0x0, -1, 0, 1, "libSceRazorCpu", g_libSceRazorCpu_modules, 1}, + {0x80000075, -1, 0, 9, "libSceRazorCpu_debug", g_libSceRazorCpu_debug_debug_modules, 2}, + {0x8000000b, -1, 0, 1, "libSceSsl2", g_libSceSsl2_modules, 3}, + {0x8000008c, -1, 0, 1, "libSceHttp2", g_libSceHttp2_modules, 13}, + {0x8000008f, -1, 0, 1, "libSceNpWebApi2", g_libSceNpWebApi2_modules, 39}, + {0x8000008d, -1, 0, 1, "libSceNpGameIntent", g_libSceNpGameIntent_modules, 10}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x6, -1, 0, 1, "libSceFiber", g_libSceFiber_modules, 5}, + {0x7, -1, 0, 1, "libSceUlt", g_libSceUlt_modules, 6}, + {0xb, -1, 0, 1, "libSceNgs2", g_libSceNgs2_modules, 2}, + {0x17, -1, 0, 1, "libSceXml", g_libSceXml_modules, 1}, + {0x19, -1, 0, 1, "libSceNpUtility", g_libSceNpUtility_modules, 5}, + {0x1a, -1, 0, 1, "libSceVoice", g_libSceVoice_modules, 4}, + {0x1c, -1, 0, 1, "libSceNpMatching2", g_libSceNpMatching2_modules, 7}, + {0x1e, -1, 0, 1, "libSceNpScoreRanking", g_libSceNpScoreRanking_modules, 3}, + {0x21, -1, 0, 1, "libSceRudp", g_libSceRudp_modules, 1}, + {0x2c, -1, 0, 1, "libSceNpTus", g_libSceNpTus_modules, 3}, + {0x38, -1, 0, 4, "libSceFace", g_libSceFace_modules, 1}, + {0x39, -1, 0, 4, "libSceSmart", g_libSceSmart_modules, 1}, + {0x80, -1, 0, 1, "libSceJson", g_libSceJson_modules, 1}, + {0x81, -1, 0, 1, "libSceGameLiveStreaming", g_libSceGameLiveStreaming_modules, 2}, + {0x82, -1, 0, 1, "libSceCompanionUtil", g_libSceCompanionUtil_modules, 3}, + {0x83, -1, 0, 1, "libScePlayGo", g_libScePlayGo_modules, 1}, + {0x84, -1, 0, 1, "libSceFont", g_libSceFont_modules, 1}, + {0x85, -1, 0, 1, "libSceVideoRecording", g_libSceVideoRecording_modules, 2}, + {0x88, -1, 0, 1, "libSceAudiodec", g_libSceAudiodec_modules, 2}, + {0x8a, -1, 0, 1, "libSceJpegDec", g_libSceJpegDec_modules, 1}, + {0x8b, -1, 0, 1, "libSceJpegEnc", g_libSceJpegEnc_modules, 1}, + {0x8c, -1, 0, 1, "libScePngDec", g_libScePngDec_modules, 1}, + {0x8d, -1, 0, 1, "libScePngEnc", g_libScePngEnc_modules, 1}, + {0x8e, -1, 0, 2049, "libSceVideodec", g_libSceVideodec_modules, 3}, + {0x8f, -1, 0, 1, "libSceMove", g_libSceMove_modules, 1}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x91, -1, 0, 1, "libScePadTracker", g_libScePadTracker_modules, 2}, + {0x92, -1, 0, 1, "libSceDepth", g_libSceDepth_modules, 2}, + {0x93, -1, 0, 4, "libSceHand", g_libSceHand_modules, 1}, + {0x95, -1, 0, 1, "libSceIme", g_libSceIme_modules, 2}, + {0x96, -1, 0, 1, "libSceImeDialog", g_libSceImeDialog_modules, 2}, + {0x80000015, -1, 0, 1, "libSceVdecCore", g_libSceVdecCore_modules, 1}, + {0x97, -1, 0, 1, "libSceNpParty", g_libSceNpParty_modules, 2}, + {0x80000003, -1, 0, 1, "libSceAvcap", g_libSceAvcap_modules, 2}, + {0x98, -1, 0, 1, "libSceFontFt", g_libSceFontFt_modules, 1}, + {0x99, -1, 0, 1, "libSceFreeTypeOt", g_libSceFreeTypeOt_modules, 1}, + {0x9a, -1, 0, 1, "libSceFreeTypeOl", g_libSceFreeTypeOl_modules, 1}, + {0x9b, -1, 0, 1, "libSceFreeTypeOptOl", g_libSceFreeTypeOptOl_modules, 1}, + {0x9c, -1, 0, 1, "libSceScreenShot", g_libSceScreenShot_modules, 3}, + {0x9d, -1, 0, 1, "libSceNpAuth", g_libSceNpAuth_modules, 3}, + {0x1b, -1, 0, 1, "libSceVoiceQos", g_libSceVoiceQos_modules, 5}, + {0x80000004, -1, 0, 1, "libSceSysCore", g_libSceSysCore_modules, 2}, + {0xbc, -1, 0, 1, "libSceM4aacEnc", g_libSceM4aacEnc_modules, 2}, + {0xbd, -1, 0, 1, "libSceAudiodecCpu", g_libSceAudiodecCpu_modules, 1}, + {0x80000007, -1, 0, 1, "libSceCdlgUtilServer", g_libSceCdlgUtilServer_modules, 2}, + {0x9f, -1, 0, 9, "libSceSulpha", g_libSceSulpha_debug_modules, 1}, + {0xa0, -1, 0, 1, "libSceSaveDataDialog", g_libSceSaveDataDialog_modules, 4}, + {0xa2, -1, 0, 1, "libSceInvitationDialog", g_libSceInvitationDialog_modules, 1}, + {0xa3, -1, 0, 2057, "libSceKeyboard", g_libSceKeyboard_debug_modules, 1}, + {0x106, -1, 0, 2049, "libSceKeyboard", g_libSceKeyboard_modules, 1}, + {0xa4, -1, 0, 1, "libSceMsgDialog", g_libSceMsgDialog_modules, 1}, + {0xa5, -1, 0, 1, "libSceAvPlayer", g_libSceAvPlayer_modules, 1}, + {0xa6, -1, 0, 1, "libSceContentExport", g_libSceContentExport_modules, 1}, + {0x80000012, -1, 0, 2, "libSceVisionManager", g_libSceVisionManager_modules, 1}, + {0x80000013, -1, 0, 2, "libSceAc3Enc", g_libSceAc3Enc_modules, 2}, + {0x80000014, -1, 0, 1, "libSceAppInstUtil", g_libSceAppInstUtil_modules, 1}, + {0x80000016, -1, 0, 514, "libSceVencCore", g_libSceVencCore_modules, 1}, + {0xa7, -1, 0, 1, "libSceAudio3d", g_libSceAudio3d_modules, 1}, + {0xa8, -1, 0, 1, "libSceNpCommerce", g_libSceNpCommerce_modules, 1}, + {0x80000017, -1, 0, 1, "libSceHidControl", g_libSceHidControl_modules, 1}, + {0xa9, -1, 0, 1, "libSceMouse", g_libSceMouse_modules, 1}, + {0xaa, -1, 0, 1, "libSceCompanionHttpd", g_libSceCompanionHttpd_modules, 1}, + {0xab, -1, 0, 1, "libSceWebBrowserDialog", g_libSceWebBrowserDialog_modules, 1}, + {0xac, -1, 0, 1, "libSceErrorDialog", g_libSceErrorDialog_modules, 1}, + {0xad, -1, 0, 1, "libSceNpTrophy", g_libSceNpTrophy_modules, 1}, + {0x0, -1, 0, 1, "ulobjmgr", g_ulobjmgr_modules, 1}, + {0xae, -1, 0, 1, "libSceVideoCoreInterface", g_libSceVideoCoreInterface_modules, 1}, + {0xaf, -1, 0, 1, "libSceVideoCoreServerInterface", g_libSceVideoCoreServerInterface_modules, + 1}, + {0x8000001b, -1, 0, 1, "libSceNpSns", g_libSceNpSns_modules, 1}, + {0xb0, -1, 0, 1, "libSceNpSnsFacebookDialog", g_libSceNpSnsFacebookDialog_modules, 2}, + {0xb1, -1, 0, 1, "libSceMoveTracker", g_libSceMoveTracker_modules, 1}, + {0xb2, -1, 0, 1, "libSceNpProfileDialog", g_libSceNpProfileDialog_modules, 1}, + {0xb3, -1, 0, 1, "libSceNpFriendListDialog", g_libSceNpFriendListDialog_modules, 1}, + {0xb4, -1, 0, 1, "libSceAppContent", g_libSceAppContent_modules, 1}, + {0x80000027, -1, 0, 2, "libSceMarlin", g_libSceMarlin_modules, 1}, + {0x80000028, -1, 0, 2, "libSceDtsEnc", g_libSceDtsEnc_modules, 2}, + {0xb5, -1, 0, 1, "libSceNpSignaling", g_libSceNpSignaling_modules, 1}, + {0xb6, -1, 0, 1, "libSceRemoteplay", g_libSceRemoteplay_modules, 1}, + {0xb7, -1, 0, 1, "libSceUsbd", g_libSceUsbd_modules, 1}, + {0xb8, -1, 0, 1, "libSceGameCustomDataDialog", g_libSceGameCustomDataDialog_modules, 1}, + {0xb9, -1, 0, 1, "libSceNpEulaDialog", g_libSceNpEulaDialog_modules, 1}, + {0xba, -1, 0, 1, "libSceRandom", g_libSceRandom_modules, 1}, + {0x80000029, -1, 0, 2, "libSceDipsw", g_libSceDipsw_modules, 1}, + {0x86, -1, 0, 4, "libSceS3DConversion", g_libSceS3DConversion_modules, 1}, + {0x8000003e, -1, 0, 9, "libSceOttvCapture", g_libSceOttvCapture_debug_modules, 1}, + {0x8000002a, -1, 0, 1, "libSceBgft", g_libSceBgft_modules, 1}, + {0xbe, -1, 0, 1, "libSceAudiodecCpuDdp", g_libSceAudiodecCpuDdp_modules, 1}, + {0xc0, -1, 0, 1, "libSceAudiodecCpuM4aac", g_libSceAudiodecCpuM4aac_modules, 1}, + {0x8000002b, -1, 0, 2, "libSceAudiodecCpuDts", g_libSceAudiodecCpuDts_modules, 1}, + {0xc9, -1, 0, 1, "libSceAudiodecCpuDtsHdLbr", g_libSceAudiodecCpuDtsHdLbr_modules, 1}, + {0x8000002d, -1, 0, 2, "libSceAudiodecCpuDtsHdMa", g_libSceAudiodecCpuDtsHdMa_modules, 1}, + {0x8000002e, -1, 0, 2, "libSceAudiodecCpuLpcm", g_libSceAudiodecCpuLpcm_modules, 1}, + {0xc1, -1, 0, 1, "libSceBemp2sys", g_libSceBemp2sys_modules, 1}, + {0xc2, -1, 0, 1, "libSceBeisobmf", g_libSceBeisobmf_modules, 1}, + {0xc3, -1, 0, 1, "libScePlayReady", g_libScePlayReady_modules, 1}, + {0xc4, -1, 0, 1, "libSceVideoNativeExtEssential", g_libSceVideoNativeExtEssential_modules, 1}, + {0xc5, -1, 0, 1, "libSceZlib", g_libSceZlib_modules, 1}, + {0x8000002f, -1, 0, 1, "libSceIduUtil", g_libSceIduUtil_modules, 1}, + {0x80000030, -1, 0, 1, "libScePsm", g_libScePsm_modules, 1}, + {0xc6, -1, 0, 1, "libSceDtcpIp", g_libSceDtcpIp_modules, 1}, + {0x80000031, -1, 0, 1, "libSceKbEmulate", g_libSceKbEmulate_modules, 1}, + {0x80000032, -1, 0, 2, "libSceAppChecker", g_libSceAppChecker_modules, 1}, + {0x80000033, -1, 0, 1, "libSceNpGriefReport", g_libSceNpGriefReport_modules, 1}, + {0xc7, -1, 0, 1, "libSceContentSearch", g_libSceContentSearch_modules, 1}, + {0xc8, -1, 0, 1, "libSceShareUtility", g_libSceShareUtility_modules, 1}, + {0x80000034, -1, 0, 1, "libSceWeb", g_libSceWeb_modules, 6}, + {0x8000006a, -1, 0, 1, "libSceWebKit2", g_libSceWebKit2_modules, 30}, + {0xca, -1, 0, 9, "libSceDeci4h", g_libSceDeci4h_debug_modules, 1}, + {0xcb, -1, 0, 4, "libSceHeadTracker", g_libSceHeadTracker_modules, 1}, + {0xcc, -1, 0, 1, "libSceGameUpdate", g_libSceGameUpdate_modules, 2}, + {0xcd, -1, 0, 1, "libSceAutoMounterClient", g_libSceAutoMounterClient_modules, 2}, + {0xce, -1, 0, 1, "libSceSystemGesture", g_libSceSystemGesture_modules, 1}, + {0x80000035, -1, 0, 1, "libSceVdecSavc", g_libSceVdecSavc_modules, 1}, + {0x80000036, -1, 0, 1, "libSceVdecSavc2", g_libSceVdecSavc2_modules, 1}, + {0xcf, -1, 0, 2049, "libSceVideodec2", g_libSceVideodec2_modules, 3}, + {0xd0, -1, 0, 1, "libSceVdecwrap", g_libSceVdecwrap_modules, 2}, + {0x80000037, -1, 0, 1, "libSceVshctl", g_libSceVshctl_modules, 1}, + {0xd1, -1, 0, 1, "libSceAt9Enc", g_libSceAt9Enc_modules, 1}, + {0xd2, -1, 0, 1, "libSceConvertKeycode", g_libSceConvertKeycode_modules, 1}, + {0x80000039, -1, 0, 1, "libSceGpuException", g_libSceGpuException_modules, 1}, + {0xd3, -1, 0, 1, "libSceSharePlay", g_libSceSharePlay_modules, 1}, + {0x8000003a, -1, 0, 2, "libSceAudiodReport", g_libSceAudiodReport_modules, 1}, + {0x8000003b, -1, 0, 2, "libSceSulphaDrv", g_libSceSulphaDrv_modules, 1}, + {0xd4, -1, 0, 1, "libSceHmd", g_libSceHmd_modules, 1}, + {0xd5, -1, 0, 1, "libSceUsbStorage", g_libSceUsbStorage_modules, 2}, + {0x8000003c, -1, 0, 1, "libSceVdecShevc", g_libSceVdecShevc_modules, 1}, + {0xd6, -1, 0, 1, "libSceUsbStorageDialog", g_libSceUsbStorageDialog_modules, 1}, + {0xd8, -1, 0, 4, "libSceFaceTracker", g_libSceFaceTracker_modules, 2}, + {0xd9, -1, 0, 4, "libSceHandTracker", g_libSceHandTracker_modules, 1}, + {0xda, -1, 0, 1, "libSceNpSnsYouTubeDialog", g_libSceNpSnsYouTubeDialog_modules, 2}, + {0xed, -1, 0, 1, "libSceVrTracker", g_libSceVrTracker_modules, 6}, + {0xdc, -1, 0, 1, "libSceProfileCacheExternal", g_libSceProfileCacheExternal_modules, 2}, + {0x8000003f, -1, 0, 1, "libSceBackupRestoreUtil", g_libSceBackupRestoreUtil_modules, 1}, + {0xdd, -1, 0, 1, "libSceMusicPlayerService", g_libSceMusicPlayerService_modules, 2}, + {0x0, -1, 0, 1, "libSceMusicCoreServerClientJsEx", g_libSceMusicCoreServerClientJsEx_modules, + 1}, + {0xde, -1, 0, 1, "libSceSpSysCallWrapper", g_libSceSpSysCallWrapper_modules, 3}, + {0xdf, -1, 0, 1, "libScePs2EmuMenuDialog", g_libScePs2EmuMenuDialog_modules, 1}, + {0xe0, -1, 0, 1, "libSceNpSnsDailyMotionDialog", g_libSceNpSnsDailyMotionDialog_modules, 1}, + {0xe1, -1, 0, 1, "libSceAudiodecCpuHevag", g_libSceAudiodecCpuHevag_modules, 1}, + {0xe2, -1, 0, 1, "libSceLoginDialog", g_libSceLoginDialog_modules, 2}, + {0xe3, -1, 0, 1, "libSceLoginService", g_libSceLoginService_modules, 2}, + {0xe4, -1, 0, 1, "libSceSigninDialog", g_libSceSigninDialog_modules, 2}, + {0xe5, -1, 0, 1, "libSceVdecsw", g_libSceVdecsw_modules, 3}, + {0x8000006d, -1, 0, 1, "libSceOrbisCompat", g_libSceOrbisCompat_modules, 24}, + {0x0, -1, 0, 1, "libSceCoreIPC", g_libSceCoreIPC_modules, 1}, + {0xe6, -1, 0, 1, "libSceCustomMusicCore", g_libSceCustomMusicCore_modules, 12}, + {0xe7, -1, 0, 1, "libSceJson2", g_libSceJson2_modules, 1}, + {0xe8, -1, 0, 4, "libSceAudioLatencyEstimation", g_libSceAudioLatencyEstimation_modules, 1}, + {0xe9, -1, 0, 1, "libSceWkFontConfig", g_libSceWkFontConfig_modules, 1}, + {0xea, -1, 0, 2, "libSceVorbisDec", g_libSceVorbisDec_modules, 3}, + {0x80000041, -1, 0, 1, "libSceTtsCoreEnUs", g_libSceTtsCoreEnUs_modules, 1}, + {0x80000042, -1, 0, 1, "libSceTtsCoreJp", g_libSceTtsCoreJp_modules, 1}, + {0x80000043, -1, 0, 1, "libSceOpusCeltEnc", g_libSceOpusCeltEnc_modules, 2}, + {0x80000044, -1, 0, 1, "libSceOpusCeltDec", g_libSceOpusCeltDec_modules, 2}, + {0x80000045, -1, 0, 2, "libSceLoginMgrServer", g_libSceLoginMgrServer_modules, 1}, + {0xeb, -1, 0, 1, "libSceHmdSetupDialog", g_libSceHmdSetupDialog_modules, 1}, + {0x80000046, -1, 0, 1, "libSceVideoOutSecondary", g_libSceVideoOutSecondary_modules, 6}, + {0xee, -1, 0, 1, "libSceContentDelete", g_libSceContentDelete_modules, 1}, + {0xef, -1, 0, 1, "libSceImeBackend", g_libSceImeBackend_modules, 1}, + {0xf0, -1, 0, 1, "libSceNetCtlApDialog", g_libSceNetCtlApDialog_modules, 1}, + {0x80000047, -1, 0, 1, "libSceGnmResourceRegistration", + g_libSceGnmResourceRegistration_modules, 1}, + {0xf1, -1, 0, 1, "libScePlayGoDialog", g_libScePlayGoDialog_modules, 1}, + {0xf2, -1, 0, 1, "libSceSocialScreen", g_libSceSocialScreen_modules, 7}, + {0xf3, -1, 0, 1, "libSceEditMp4", g_libSceEditMp4_modules, 1}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0x0, -1, 0, 0, nullptr, nullptr, 0}, + {0xf5, -1, 0, 1, "libScePsmKitSystem", g_libScePsmKitSystem_modules, 1}, + {0xf6, -1, 0, 1, "libSceTextToSpeech", g_libSceTextToSpeech_modules, 1}, + {0xf7, -1, 0, 2052, "libSceNpToolkit", g_libSceNpToolkit_modules, 1}, + {0xf8, -1, 0, 1, "libSceCustomMusicService", g_libSceCustomMusicService_modules, 2}, + {0xf9, -1, 0, 1, "libSceClSysCallWrapper", g_libSceClSysCallWrapper_modules, 11}, + {0x80000049, -1, 0, 1, "libSceScm", g_libSceScm_modules, 1}, + {0xfa, -1, 0, 1, "libSceSystemLogger", g_libSceSystemLogger_modules, 2}, + {0xfb, -1, 0, 1, "libSceBluetoothHid", g_libSceBluetoothHid_modules, 1}, + {0x80000050, -1, 0, 1, "libSceAvPlayerStreaming", g_libSceAvPlayerStreaming_modules, 1}, + {0x80000051, -1, 0, 2, "libSceAudiodecCpuAlac", g_libSceAudiodecCpuAlac_modules, 1}, + {0xfc, -1, 0, 1, "libSceVideoDecoderArbitration", g_libSceVideoDecoderArbitration_modules, 1}, + {0xfd, -1, 0, 1, "libSceVrServiceDialog", g_libSceVrServiceDialog_modules, 1}, + {0xfe, -1, 0, 4, "libSceJobManager", g_libSceJobManager_modules, 2}, + {0x80000053, -1, 0, 2, "libSceAudiodecCpuFlac", g_libSceAudiodecCpuFlac_modules, 1}, + {0x103, -1, 0, 1, "libSceSrcUtl", g_libSceSrcUtl_modules, 2}, + {0x80000055, -1, 0, 2, "libSceS3da", g_libSceS3da_modules, 1}, + {0x80000056, -1, 0, 2, "libSceDseehx", g_libSceDseehx_modules, 1}, + {0xff, -1, 0, 1, "libSceShareFactoryUtil", g_libSceShareFactoryUtil_modules, 1}, + {0x80000057, -1, 0, 1, "libSceDataTransfer", g_libSceDataTransfer_modules, 1}, + {0x100, -1, 0, 1, "libSceSocialScreenDialog", g_libSceSocialScreenDialog_modules, 1}, + {0x80000058, -1, 0, 1, "libSceAbstractStorage", g_libSceAbstractStorage_modules, 1}, + {0x80000059, -1, 0, 1, "libSceImageUtil", g_libSceImageUtil_modules, 1}, + {0x8000005a, -1, 0, 1, "libSceMetadataReaderWriter", g_libSceMetadataReaderWriter_modules, 1}, + {0x8000005b, -1, 0, 1, "libSceJpegParser", g_libSceJpegParser_modules, 1}, + {0x8000005c, -1, 0, 1, "libSceGvMp4Parser", g_libSceGvMp4Parser_modules, 1}, + {0x8000005d, -1, 0, 1, "libScePngParser", g_libScePngParser_modules, 1}, + {0x8000005e, -1, 0, 1, "libSceGifParser", g_libSceGifParser_modules, 1}, + {0x101, -1, 0, 1, "libSceNpSnsDialog", g_libSceNpSnsDialog_modules, 2}, + {0x8000005f, -1, 0, 1, "libSceAbstractLocal", g_libSceAbstractLocal_modules, 1}, + {0x80000060, -1, 0, 1, "libSceAbstractFacebook", g_libSceAbstractFacebook_modules, 1}, + {0x80000061, -1, 0, 1, "libSceAbstractYoutube", g_libSceAbstractYoutube_modules, 1}, + {0x80000062, -1, 0, 1, "libSceAbstractTwitter", g_libSceAbstractTwitter_modules, 1}, + {0x80000063, -1, 0, 1, "libSceAbstractDailymotion", g_libSceAbstractDailymotion_modules, 1}, + {0x102, -1, 0, 2052, "libSceNpToolkit2", g_libSceNpToolkit2_modules, 1}, + {0x80000064, -1, 0, 1, "libScePrecompiledShaders", g_libScePrecompiledShaders_modules, 1}, + {0x104, -1, 0, 1, "libSceDiscId", g_libSceDiscId_modules, 1}, + {0x80000065, -1, 0, 1, "libSceLibreSsl", g_libSceLibreSsl_modules, 2}, + {0x80000066, -1, 0, 2, "libSceFsInternalForVsh", g_libSceFsInternalForVsh_modules, 1}, + {0x105, -1, 0, 1, "libSceNpUniversalDataSystem", g_libSceNpUniversalDataSystem_modules, 1}, + {0x80000067, -1, 0, 1, "libSceDolbyVision", g_libSceDolbyVision_modules, 1}, + {0x80000068, -1, 0, 1, "libSceOpusSilkEnc", g_libSceOpusSilkEnc_modules, 2}, + {0x80000069, -1, 0, 1, "libSceOpusDec", g_libSceOpusDec_modules, 2}, + {0x8000006b, -1, 0, 1, "libSceWebKit2Secure", g_libSceWebKit2Secure_modules, 34}, + {0x8000006c, -1, 0, 1, "libSceJscCompiler", g_libSceJscCompiler_modules, 1}, + {0x8000006e, -1, 0, 1, "libSceJitBridge", g_libSceJitBridge_modules, 4}, + {0x0, -1, 0, 1, "libScePigletv2VSH", g_libScePigletv2VSH_modules, 4}, + {0x8000006f, -1, 0, 4096, "libSceJitBridge", g_libSceJitBridge_common_ex_modules, 4}, + {0x80000070, -1, 0, 4096, "libSceJscCompiler", g_libSceJscCompiler_common_ex_modules, 1}, + {0x80000071, -1, 0, 4096, "libSceOrbisCompat", g_libSceOrbisCompat_common_ex_modules, 24}, + {0x80000072, -1, 0, 4096, "libSceWeb", g_libSceWeb_common_ex_modules, 6}, + {0x80000073, -1, 0, 4096, "libSceWebKit2", g_libSceWebKit2_common_ex_modules, 30}, + {0x80000074, -1, 0, 4096, "libSceWebKit2Secure", g_libSceWebKit2Secure_common_ex_modules, 34}, + {0x0, -1, 0, 4096, "libScePrecompiledShaders", g_libScePrecompiledShaders_common_ex_modules, + 1}, + {0x107, -1, 0, 1, "libSceGic", g_libSceGic_modules, 1}, + {0x80000076, -1, 0, 1, "libSceRnpsAppMgr", g_libSceRnpsAppMgr_modules, 1}, + {0x80000077, -1, 0, 1, "libSceAsyncStorageInternal", g_libSceAsyncStorageInternal_modules, 1}, + {0x80000078, -1, 0, 1, "libSceHttpCache", g_libSceHttpCache_modules, 1}, + {0x108, -1, 0, 1, "libScePlayReady2", g_libScePlayReady2_modules, 1}, + {0x109, -1, 0, 9, "libSceHdrScopes", g_libSceHdrScopes_debug_modules, 1}, + {0x80000079, -1, 0, 1, "libSceNKWeb", g_libSceNKWeb_modules, 1}, + {0x8000007a, -1, 0, 1, "libSceNKWebKit", g_libSceNKWebKit_modules, 2}, + {0x0, -1, 0, 1, "libSceNKWebKitRequirements", g_libSceNKWebKitRequirements_modules, 1}, + {0x8000007c, -1, 0, 1, "libSceVnaInternal", g_libSceVnaInternal_modules, 1}, + {0x8000007d, -1, 0, 1, "libSceVnaWebsocket", g_libSceVnaWebsocket_modules, 1}, + {0x10c, -1, 0, 1, "libSceCesCs", g_libSceCesCs_modules, 1}, + {0x8000008a, -1, 0, 2, "libSceComposite", g_libSceComposite_modules, 1}, + {0x8000008b, -1, 0, 1, "libSceCompositeExt", g_libSceCompositeExt_modules, 1}, + {0x116, -1, 0, 1, "libSceHubAppUtil", g_libSceHubAppUtil_modules, 1}, + {0x80000098, -1, 0, 1, "libScePosixForWebKit", g_libScePosixForWebKit_modules, 1}, + {0x11a, -1, 0, 1, "libSceNpPartner001", g_libSceNpPartner001_modules, 1}, + {0x112, -1, 0, 1, "libSceNpSessionSignaling", g_libSceNpSessionSignaling_modules, 75}, + {0x10d, -1, 0, 1, "libScePlayerInvitationDialog", g_libScePlayerInvitationDialog_modules, 1}, + {0x115, -1, 0, 4, "libSceNpCppWebApi", g_libSceNpCppWebApi_modules, 42}, + {0x113, -1, 0, 1, "libSceNpEntitlementAccess", g_libSceNpEntitlementAccess_modules, 1}, + {0x8000009a, -1, 0, 2, "libSceNpRemotePlaySessionSignaling", + g_libSceNpRemotePlaySessionSignaling_modules, 76}, + {0x800000b8, -1, 0, 1, "libSceLibreSsl3", g_libSceLibreSsl3_modules, 2}, + {0x800000b1, -1, 0, 1, "libcurl", g_libcurl_modules, 2}, + {0x800000aa, -1, 0, 1, "libicu", g_libicu_modules, 2}, + {0x800000ac, -1, 0, 1, "libcairo", g_libcairo_modules, 9}, + {0x0, -1, 0, 1, "libfontconfig", g_libfontconfig_modules, 1}, + {0x0, -1, 0, 1, "libfreetype", g_libfreetype_modules, 1}, + {0x0, -1, 0, 1, "libharfbuzz", g_libharfbuzz_modules, 1}, + {0x800000ab, -1, 0, 1, "libpng16", g_libpng16_modules, 2}, + {0x12f, -1, 0, 1, "libSceFontGs", g_libSceFontGs_modules, 1}, + {0x800000c0, -1, 0, 1, "libSceGLSlimClientVSH", g_libSceGLSlimClientVSH_modules, 1}, + {0x800000c1, -1, 0, 1, "libSceGLSlimServerVSH", g_libSceGLSlimServerVSH_modules, 1}, + {0x135, -1, 0, 4, "libSceFontGsm", g_libSceFontGsm_modules, 1}, + {0x138, -1, 0, 1, "libSceNpPartnerSubscription", g_libSceNpPartnerSubscription_modules, 1}, + {0x139, -1, 0, 1, "libSceNpAuthAuthorizedAppDialog", g_libSceNpAuthAuthorizedAppDialog_modules, + 1}}); + +} // namespace Libraries::SysModule diff --git a/src/core/libraries/system/sysmodule.cpp b/src/core/libraries/system/sysmodule.cpp deleted file mode 100644 index 50d030065..000000000 --- a/src/core/libraries/system/sysmodule.cpp +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#define MAGIC_ENUM_RANGE_MIN 0 -#define MAGIC_ENUM_RANGE_MAX 300 -#include - -#include "common/logging/log.h" -#include "core/libraries/error_codes.h" -#include "core/libraries/kernel/process.h" -#include "core/libraries/libs.h" -#include "core/libraries/system/sysmodule.h" -#include "core/libraries/system/system_error.h" - -namespace Libraries::SysModule { - -int PS4_SYSV_ABI sceSysmoduleGetModuleHandleInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceSysmoduleGetModuleInfoForUnwind(VAddr addr, s32 flags, - Kernel::OrbisModuleInfoForUnwind* info) { - LOG_TRACE(Lib_SysModule, "sceSysmoduleGetModuleInfoForUnwind(addr=0x{:X}, flags=0x{:X})", addr, - flags); - - s32 res = Kernel::sceKernelGetModuleInfoForUnwind(addr, flags, info); - if (res != 0) { - return res; - } - - static constexpr std::array modules_to_hide = { - "libc.prx", - "libc.sprx", - "libSceAudioLatencyEstimation.prx", - "libSceFace.prx", - "libSceFaceTracker.prx", - "libSceFios2.prx", - "libSceFios2.sprx", - "libSceFontGsm.prx", - "libSceHand.prx", - "libSceHandTracker.prx", - "libSceHeadTracker.prx", - "libSceJobManager.prx", - "libSceNpCppWebApi.prx", - "libSceNpToolkit.prx", - "libSceNpToolkit2.prx", - "libSceS3DConversion.prx", - "libSceSmart.prx", - }; - - const std::string_view module_name = info->name.data(); - if (std::ranges::find(modules_to_hide, module_name) != modules_to_hide.end()) { - std::ranges::fill(info->name, '\0'); - } - return res; -} - -int PS4_SYSV_ABI sceSysmoduleIsCalledFromSysModule() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleIsCameraPreloaded() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleIsLoaded(OrbisSysModule id) { - LOG_ERROR(Lib_SysModule, "(DUMMY) called module = {}", magic_enum::enum_name(id)); - if (static_cast(id) == 0) { - LOG_ERROR(Lib_SysModule, "Invalid sysmodule ID: {:#x}", static_cast(id)); - return ORBIS_SYSMODULE_INVALID_ID; - } - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleIsLoadedInternal(OrbisSysModuleInternal id) { - LOG_ERROR(Lib_SysModule, "(DUMMY) called module = {:#x}", static_cast(id)); - if ((static_cast(id) & 0x7FFFFFFF) == 0) { - LOG_ERROR(Lib_SysModule, "Invalid internal sysmodule ID: {:#x}", static_cast(id)); - return ORBIS_SYSMODULE_INVALID_ID; - } - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleLoadModule(OrbisSysModule id) { - LOG_ERROR(Lib_SysModule, "(DUMMY) called module = {}", magic_enum::enum_name(id)); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleLoadModuleByNameInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleLoadModuleInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleLoadModuleInternalWithArg() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleMapLibcForLibkernel() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmodulePreloadModuleForLibkernel() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleUnloadModule() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleUnloadModuleByNameInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleUnloadModuleInternal() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceSysmoduleUnloadModuleInternalWithArg() { - LOG_ERROR(Lib_SysModule, "(STUBBED) called"); - return ORBIS_OK; -} - -void RegisterLib(Core::Loader::SymbolsResolver* sym) { - LIB_FUNCTION("D8cuU4d72xM", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleGetModuleHandleInternal); - LIB_FUNCTION("4fU5yvOkVG4", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleGetModuleInfoForUnwind); - LIB_FUNCTION("ctfO7dQ7geg", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleIsCalledFromSysModule); - LIB_FUNCTION("no6T3EfiS3E", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleIsCameraPreloaded); - LIB_FUNCTION("fMP5NHUOaMk", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleIsLoaded); - LIB_FUNCTION("ynFKQ5bfGks", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleIsLoadedInternal); - LIB_FUNCTION("g8cM39EUZ6o", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleLoadModule); - LIB_FUNCTION("CU8m+Qs+HN4", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleLoadModuleByNameInternal); - LIB_FUNCTION("39iV5E1HoCk", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleLoadModuleInternal); - LIB_FUNCTION("hHrGoGoNf+s", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleLoadModuleInternalWithArg); - LIB_FUNCTION("lZ6RvVl0vo0", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleMapLibcForLibkernel); - LIB_FUNCTION("DOO+zuW1lrE", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmodulePreloadModuleForLibkernel); - LIB_FUNCTION("eR2bZFAAU0Q", "libSceSysmodule", 1, "libSceSysmodule", sceSysmoduleUnloadModule); - LIB_FUNCTION("vpTHmA6Knvg", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleUnloadModuleByNameInternal); - LIB_FUNCTION("vXZhrtJxkGc", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleUnloadModuleInternal); - LIB_FUNCTION("aKa6YfBKZs4", "libSceSysmodule", 1, "libSceSysmodule", - sceSysmoduleUnloadModuleInternalWithArg); -}; - -} // namespace Libraries::SysModule diff --git a/src/core/libraries/system/sysmodule.h b/src/core/libraries/system/sysmodule.h deleted file mode 100644 index 3f1328e4b..000000000 --- a/src/core/libraries/system/sysmodule.h +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "common/types.h" -#include "core/libraries/kernel/process.h" - -namespace Core::Loader { -class SymbolsResolver; -} - -namespace Libraries::SysModule { - -enum class OrbisSysModule : u16 { - ORBIS_SYSMODULE_INVALID = 0x0000, - ORBIS_SYSMODULE_FIBER = 0x0006, // libSceFiber.sprx - ORBIS_SYSMODULE_ULT = 0x0007, // libSceUlt.sprx - ORBIS_SYSMODULE_NGS2 = 0x000B, // libSceNgs2.sprx - ORBIS_SYSMODULE_XML = 0x0017, // libSceXml.sprx - ORBIS_SYSMODULE_NP_UTILITY = 0x0019, // libSceNpUtility.sprx - ORBIS_SYSMODULE_VOICE = 0x001A, // libSceVoice.sprx - ORBIS_SYSMODULE_VOICEQOS = 0x001B, // libSceVoiceQos.sprx - ORBIS_SYSMODULE_NP_MATCHING2 = 0x001C, // libSceNpMatching2.sprx - ORBIS_SYSMODULE_NP_SCORE_RANKING = 0x001E, // libSceNpScoreRanking.sprx - ORBIS_SYSMODULE_RUDP = 0x0021, // libSceRudp.sprx - ORBIS_SYSMODULE_NP_TUS = 0x002C, // libSceNpTus.sprx - ORBIS_SYSMODULE_FACE = 0x0038, // libSceFace.sprx - ORBIS_SYSMODULE_SMART = 0x0039, // libSceSmart.sprx - ORBIS_SYSMODULE_JSON = 0x0080, // libSceJson.sprx - ORBIS_SYSMODULE_GAME_LIVE_STREAMING = 0x0081, // libSceGameLiveStreaming.sprx - ORBIS_SYSMODULE_COMPANION_UTIL = 0x0082, // libSceCompanionUtil.sprx - ORBIS_SYSMODULE_PLAYGO = 0x0083, // libScePlayGo.sprx - ORBIS_SYSMODULE_FONT = 0x0084, // libSceFont.sprx - ORBIS_SYSMODULE_VIDEO_RECORDING = 0x0085, // libSceVideoRecording.sprx - ORBIS_SYSMODULE_S3DCONVERSION = 0x0086, // libSceS3DConversion - ORBIS_SYSMODULE_AUDIODEC = 0x0088, // libSceAudiodec.sprx - ORBIS_SYSMODULE_JPEG_DEC = 0x008A, // libSceJpegDec.sprx - ORBIS_SYSMODULE_JPEG_ENC = 0x008B, // libSceJpegEnc.sprx - ORBIS_SYSMODULE_PNG_DEC = 0x008C, // libScePngDec.sprx - ORBIS_SYSMODULE_PNG_ENC = 0x008D, // libScePngEnc.sprx - ORBIS_SYSMODULE_VIDEODEC = 0x008E, // libSceVideodec.sprx - ORBIS_SYSMODULE_MOVE = 0x008F, // libSceMove.sprx - ORBIS_SYSMODULE_PAD_TRACKER = 0x0091, // libScePadTracker.sprx - ORBIS_SYSMODULE_DEPTH = 0x0092, // libSceDepth.sprx - ORBIS_SYSMODULE_HAND = 0x0093, // libSceHand.sprx - ORBIS_SYSMODULE_LIBIME = 0x0095, // libSceIme.sprx - ORBIS_SYSMODULE_IME_DIALOG = 0x0096, // libSceImeDialog.sprx - ORBIS_SYSMODULE_NP_PARTY = 0x0097, // libSceNpParty.sprx - ORBIS_SYSMODULE_FONT_FT = 0x0098, // libSceFontFt.sprx - ORBIS_SYSMODULE_FREETYPE_OT = 0x0099, // libSceFreeTypeOt.sprx - ORBIS_SYSMODULE_FREETYPE_OL = 0x009A, // libSceFreeTypeOl.sprx - ORBIS_SYSMODULE_FREETYPE_OPT_OL = 0x009B, // libSceFreeTypeOptOl.sprx - ORBIS_SYSMODULE_SCREEN_SHOT = 0x009C, // libSceScreenShot.sprx - ORBIS_SYSMODULE_NP_AUTH = 0x009D, // libSceNpAuth.sprx - ORBIS_SYSMODULE_SULPHA = 0x009F, - ORBIS_SYSMODULE_SAVE_DATA_DIALOG = 0x00A0, // libSceSaveDataDialog.sprx - ORBIS_SYSMODULE_INVITATION_DIALOG = 0x00A2, // libSceInvitationDialog.sprx - ORBIS_SYSMODULE_DEBUG_KEYBOARD = 0x00A3, - ORBIS_SYSMODULE_MESSAGE_DIALOG = 0x00A4, // libSceMsgDialog.sprx - ORBIS_SYSMODULE_AV_PLAYER = 0x00A5, // libSceAvPlayer.sprx - ORBIS_SYSMODULE_CONTENT_EXPORT = 0x00A6, // libSceContentExport.sprx - ORBIS_SYSMODULE_AUDIO_3D = 0x00A7, // libSceAudio3d.sprx - ORBIS_SYSMODULE_NP_COMMERCE = 0x00A8, // libSceNpCommerce.sprx - ORBIS_SYSMODULE_MOUSE = 0x00A9, // libSceMouse.sprx - ORBIS_SYSMODULE_COMPANION_HTTPD = 0x00AA, // libSceCompanionHttpd.sprx - ORBIS_SYSMODULE_WEB_BROWSER_DIALOG = 0x00AB, // libSceWebBrowserDialog.sprx - ORBIS_SYSMODULE_ERROR_DIALOG = 0x00AC, // libSceErrorDialog.sprx - ORBIS_SYSMODULE_NP_TROPHY = 0x00AD, // libSceNpTrophy.sprx - ORBIS_SYSMODULE_VIDEO_CORE_IF = 0x00AE, // libSceVideoCoreInterface.sprx - ORBIS_SYSMODULE_VIDEO_CORE_SERVER_IF = 0x00AF, // libSceVideoCoreServerInterface.sprx - ORBIS_SYSMODULE_NP_SNS_FACEBOOK = 0x00B0, // libSceNpSnsFacebookDialog.sprx - ORBIS_SYSMODULE_MOVE_TRACKER = 0x00B1, // libSceMoveTracker.sprx - ORBIS_SYSMODULE_NP_PROFILE_DIALOG = 0x00B2, // libSceNpProfileDialog.sprx - ORBIS_SYSMODULE_NP_FRIEND_LIST_DIALOG = 0x00B3, // libSceNpFriendListDialog.sprx - ORBIS_SYSMODULE_APP_CONTENT = 0x00B4, // libSceAppContent.sprx - ORBIS_SYSMODULE_NP_SIGNALING = 0x00B5, // libSceNpSignaling.sprx - ORBIS_SYSMODULE_REMOTE_PLAY = 0x00B6, // libSceRemoteplay.sprx - ORBIS_SYSMODULE_USBD = 0x00B7, // libSceUsbd.sprx - ORBIS_SYSMODULE_GAME_CUSTOM_DATA_DIALOG = 0x00B8, // libSceGameCustomDataDialog.sprx - ORBIS_SYSMODULE_NP_EULA_DIALOG = 0x00B9, // libSceNpEulaDialog.sprx - ORBIS_SYSMODULE_RANDOM = 0x00BA, // libSceRandom.sprx - ORBIS_SYSMODULE_RESERVED2 = 0x00BB, - ORBIS_SYSMODULE_M4AAC_ENC = 0x00BC, // libSceM4aacEnc.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU = 0x00BD, // libSceAudiodecCpu.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU_DDP = 0x00BE, // libSceAudiodecCpuDdp.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU_M4AAC = 0x00C0, // libSceAudiodecCpuM4aac.sprx - ORBIS_SYSMODULE_BEMP2_SYS = 0x00C1, // libSceBemp2sys.sprx - ORBIS_SYSMODULE_BEISOBMF = 0x00C2, // libSceBeisobmf.sprx - ORBIS_SYSMODULE_PLAY_READY = 0x00C3, // libScePlayReady.sprx - ORBIS_SYSMODULE_VIDEO_NATIVE_EXT_ESSENTIAL = 0x00C4, // libSceVideoNativeExtEssential.sprx - ORBIS_SYSMODULE_ZLIB = 0x00C5, // libSceZlib.sprx - ORBIS_SYSMODULE_DTCP_IP = 0x00C6, // libSceDtcpIp.sprx - ORBIS_SYSMODULE_CONTENT_SEARCH = 0x00C7, // libSceContentSearch.sprx - ORBIS_SYSMODULE_SHARE_UTILITY = 0x00C8, // libSceShareUtility.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU_DTS_HD_LBR = 0x00C9, // libSceAudiodecCpuDtsHdLbr.sprx - ORBIS_SYSMODULE_DECI4H = 0x00CA, - ORBIS_SYSMODULE_HEAD_TRACKER = 0x00CB, // libSceHeadTracker.sprx - ORBIS_SYSMODULE_GAME_UPDATE = 0x00CC, // libSceGameUpdate.sprx - ORBIS_SYSMODULE_AUTO_MOUNTER_CLIENT = 0x00CD, // libSceAutoMounterClient.sprx - ORBIS_SYSMODULE_SYSTEM_GESTURE = 0x00CE, // libSceSystemGesture.sprx - ORBIS_SYSMODULE_VIDEODEC2 = 0x00CF, // libSceVideodec2.sprx - ORBIS_SYSMODULE_VDECWRAP = 0x00D0, // libSceVdecwrap.sprx - ORBIS_SYSMODULE_AT9_ENC = 0x00D1, // libSceAt9Enc.sprx - ORBIS_SYSMODULE_CONVERT_KEYCODE = 0x00D2, // libSceConvertKeycode.sprx - ORBIS_SYSMODULE_SHARE_PLAY = 0x00D3, // libSceSharePlay.sprx - ORBIS_SYSMODULE_HMD = 0x00D4, // libSceHmd.sprx - ORBIS_SYSMODULE_USB_STORAGE = 0x00D5, // libSceUsbStorage.sprx - ORBIS_SYSMODULE_USB_STORAGE_DIALOG = 0x00D6, // libSceUsbStorageDialog.sprx - ORBIS_SYSMODULE_DISC_MAP = 0x00D7, // libSceDiscMap.sprx - ORBIS_SYSMODULE_FACE_TRACKER = 0x00D8, // libSceFaceTracker.sprx - ORBIS_SYSMODULE_HAND_TRACKER = 0x00D9, // libSceHandTracker.sprx - ORBIS_SYSMODULE_NP_SNS_YOUTUBE_DIALOG = 0x00DA, // libSceNpSnsYouTubeDialog.sprx - ORBIS_SYSMODULE_PROFILE_CACHE_EXTERNAL = 0x00DC, // libSceProfileCacheExternal.sprx - ORBIS_SYSMODULE_MUSIC_PLAYER_SERVICE = 0x00DD, // libSceMusicPlayerService.sprx - ORBIS_SYSMODULE_SP_SYS_CALL_WRAPPER = 0x00DE, // libSceSpSysCallWrapper.sprx - ORBIS_SYSMODULE_PS2_EMU_MENU_DIALOG = 0x00DF, // libScePs2EmuMenuDialog.sprx - ORBIS_SYSMODULE_NP_SNS_DAILYMOTION_DIALOG = 0x00E0, // libSceNpSnsDailyMotionDialog.sprx - ORBIS_SYSMODULE_AUDIODEC_CPU_HEVAG = 0x00E1, // libSceAudiodecCpuHevag.sprx - ORBIS_SYSMODULE_LOGIN_DIALOG = 0x00E2, // libSceLoginDialog.sprx - ORBIS_SYSMODULE_LOGIN_SERVICE = 0x00E3, // libSceLoginService.sprx - ORBIS_SYSMODULE_SIGNIN_DIALOG = 0x00E4, // libSceSigninDialog.sprx - ORBIS_SYSMODULE_VDECSW = 0x00E5, // libSceVdecsw.sprx - ORBIS_SYSMODULE_CUSTOM_MUSIC_CORE = 0x00E6, // libSceCustomMusicCore.sprx - ORBIS_SYSMODULE_JSON2 = 0x00E7, // libSceJson2.sprx - ORBIS_SYSMODULE_AUDIO_LATENCY_ESTIMATION = 0x00E8, // libSceAudioLatencyEstimation.sprx - ORBIS_SYSMODULE_WK_FONT_CONFIG = 0x00E9, // libSceWkFontConfig.sprx - ORBIS_SYSMODULE_VORBIS_DEC = 0x00EA, // libSceVorbisDec.sprx - ORBIS_SYSMODULE_HMD_SETUP_DIALOG = 0x00EB, // libSceHmdSetupDialog.sprx - ORBIS_SYSMODULE_RESERVED28 = 0x00EC, - ORBIS_SYSMODULE_VR_TRACKER = 0x00ED, // libSceVrTracker.sprx - ORBIS_SYSMODULE_CONTENT_DELETE = 0x00EE, // libSceContentDelete.sprx - ORBIS_SYSMODULE_IME_BACKEND = 0x00EF, // libSceImeBackend.sprx - ORBIS_SYSMODULE_NET_CTL_AP_DIALOG = 0x00F0, // libSceNetCtlApDialog.sprx - ORBIS_SYSMODULE_PLAYGO_DIALOG = 0x00F1, // libScePlayGoDialog.sprx - ORBIS_SYSMODULE_SOCIAL_SCREEN = 0x00F2, // libSceSocialScreen.sprx - ORBIS_SYSMODULE_EDIT_MP4 = 0x00F3, // libSceEditMp4.sprx - ORBIS_SYSMODULE_PSM_KIT_SYSTEM = 0x00F5, // libScePsmKitSystem.sprx - ORBIS_SYSMODULE_TEXT_TO_SPEECH = 0x00F6, // libSceTextToSpeech.sprx - ORBIS_SYSMODULE_NP_TOOLKIT = 0x00F7, // libSceNpToolkit.sprx - ORBIS_SYSMODULE_CUSTOM_MUSIC_SERVICE = 0x00F8, // libSceCustomMusicService.sprx - ORBIS_SYSMODULE_CL_SYS_CALL_WRAPPER = 0x00F9, // libSceClSysCallWrapper.sprx - ORBIS_SYSMODULE_SYSTEM_LOGGER = 0x00FA, // libSceSystemLogger.sprx - ORBIS_SYSMODULE_BLUETOOTH_HID = 0x00FB, // libSceBluetoothHid.sprx - ORBIS_SYSMODULE_VIDEO_DECODER_ARBITRATION = 0x00FC, // libSceVideoDecoderArbitration.sprx - ORBIS_SYSMODULE_VR_SERVICE_DIALOG = 0x00FD, // libSceVrServiceDialog.sprx - ORBIS_SYSMODULE_JOB_MANAGER = 0x00FE, // libSceJobManager.sprx - ORBIS_SYSMODULE_SHARE_FACTORY_UTIL = 0x00FF, // libSceShareFactoryUtil.sprx - ORBIS_SYSMODULE_SOCIAL_SCREEN_DIALOG = 0x0100, // libSceSocialScreenDialog.sprx - ORBIS_SYSMODULE_NP_SNS_DIALOG = 0x0101, // libSceNpSnsDialog.sprx - ORBIS_SYSMODULE_NP_TOOLKIT2 = 0x0102, // libSceNpToolkit2.sprx - ORBIS_SYSMODULE_SRC_UTL = 0x0103, // libSceSrcUtl.sprx - ORBIS_SYSMODULE_DISC_ID = 0x0104, // libSceDiscId.sprx - ORBIS_SYSMODULE_NP_UNIVERSAL_DATA_SYSTEM = 0x0105, // libSceNpUniversalDataSystem.sprx - ORBIS_SYSMODULE_KEYBOARD = 0x0106, // libSceKeyboard.sprx - ORBIS_SYSMODULE_GIC = 0x0107, // libSceGic.sprx - ORBIS_SYSMODULE_PLAY_READY2 = 0x0108, // libScePlayReady2.sprx - ORBIS_SYSMODULE_CES_CS = 0x010c, // libSceCesCs.sprx - ORBIS_SYSMODULE_PLAYER_INVITATION_DIALOG = 0x010d, // libScePlayerInvitationDialog.sprx - ORBIS_SYSMODULE_NP_SESSION_SIGNALING = 0x0112, // libSceNpSessionSignaling.sprx - ORBIS_SYSMODULE_NP_ENTITLEMENT_ACCESS = 0x0113, // libSceNpEntitlementAccess.sprx - ORBIS_SYSMODULE_NP_CPP_WEB_API = 0x0115, // libSceNpCppWebApi.sprx - ORBIS_SYSMODULE_HUB_APP_UTIL = 0x0116, // libSceHubAppUtil.sprx - ORBIS_SYSMODULE_NP_PARTNER001 = 0x011a, // libSceNpPartner001.sprx - ORBIS_SYSMODULE_FONT_GS = 0x012f, // libSceFontGs.sprx - ORBIS_SYSMODULE_FONT_GSM = 0x0135, // libSceFontGsm.sprx - ORBIS_SYSMODULE_NP_PARTNER_SUBSCRIPTION = 0x0138, // libSceNpPartnerSubscription.sprx - ORBIS_SYSMODULE_NP_AUTH_AUTHORIZED_APP_DIALOG = 0x0139, // libSceNpAuthAuthorizedAppDialog.sprx -}; - -enum class OrbisSysModuleInternal : u32 { - ORBIS_SYSMODULE_INTERNAL_RAZOR_CPU = 0x80000019, // libSceRazorCpu.sprx -}; - -int PS4_SYSV_ABI sceSysmoduleGetModuleHandleInternal(); -s32 PS4_SYSV_ABI sceSysmoduleGetModuleInfoForUnwind(VAddr addr, s32 flags, - Kernel::OrbisModuleInfoForUnwind* info); -int PS4_SYSV_ABI sceSysmoduleIsCalledFromSysModule(); -int PS4_SYSV_ABI sceSysmoduleIsCameraPreloaded(); -int PS4_SYSV_ABI sceSysmoduleIsLoaded(OrbisSysModule id); -int PS4_SYSV_ABI sceSysmoduleIsLoadedInternal(OrbisSysModuleInternal id); -int PS4_SYSV_ABI sceSysmoduleLoadModule(OrbisSysModule id); -int PS4_SYSV_ABI sceSysmoduleLoadModuleByNameInternal(); -int PS4_SYSV_ABI sceSysmoduleLoadModuleInternal(); -int PS4_SYSV_ABI sceSysmoduleLoadModuleInternalWithArg(); -int PS4_SYSV_ABI sceSysmoduleMapLibcForLibkernel(); -int PS4_SYSV_ABI sceSysmodulePreloadModuleForLibkernel(); -int PS4_SYSV_ABI sceSysmoduleUnloadModule(); -int PS4_SYSV_ABI sceSysmoduleUnloadModuleByNameInternal(); -int PS4_SYSV_ABI sceSysmoduleUnloadModuleInternal(); -int PS4_SYSV_ABI sceSysmoduleUnloadModuleInternalWithArg(); - -void RegisterLib(Core::Loader::SymbolsResolver* sym); -} // namespace Libraries::SysModule diff --git a/src/core/libraries/system/system_error.h b/src/core/libraries/system/system_error.h deleted file mode 100644 index 615e4cd5f..000000000 --- a/src/core/libraries/system/system_error.h +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -constexpr int ORBIS_SYSMODULE_INVALID_ID = 0x805A1000; -constexpr int ORBIS_SYSMODULE_NOT_LOADED = 0x805A1001; -constexpr int ORBIS_SYSMODULE_LOCK_FAILED = 0x805A10FF; \ No newline at end of file diff --git a/src/core/libraries/system/systemservice.cpp b/src/core/libraries/system/systemservice.cpp index ce5542fc8..0e40c723f 100644 --- a/src/core/libraries/system/systemservice.cpp +++ b/src/core/libraries/system/systemservice.cpp @@ -1,10 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" -#include "common/logging/log.h" #include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/file_sys/fs.h" #include "core/libraries/libs.h" #include "core/libraries/system/systemservice.h" @@ -18,7 +17,7 @@ std::queue g_event_queue; std::mutex g_event_queue_mutex; bool IsSplashVisible() { - return Config::showSplash() && g_splash_status; + return EmulatorSettings.IsShowSplash() && g_splash_status; } int PS4_SYSV_ABI sceAppMessagingClearEventFlag() { @@ -1918,7 +1917,7 @@ s32 PS4_SYSV_ABI sceSystemServiceParamGetInt(OrbisSystemServiceParamId param_id, } switch (param_id) { case OrbisSystemServiceParamId::Lang: - *value = Config::GetLanguage(); + *value = EmulatorSettings.GetConsoleLanguage(); break; case OrbisSystemServiceParamId::DateFormat: *value = u32(OrbisSystemParamDateFormat::FmtDDMMYYYY); diff --git a/src/core/libraries/system/userservice.cpp b/src/core/libraries/system/userservice.cpp index 508b1d7e5..029868eb4 100644 --- a/src/core/libraries/system/userservice.cpp +++ b/src/core/libraries/system/userservice.cpp @@ -1,12 +1,19 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" +#include + #include "common/logging/log.h" +#include +#include +#include "common/singleton.h" +#include "core/emulator_settings.h" #include "core/libraries/libs.h" #include "core/libraries/system/userservice.h" #include "core/libraries/system/userservice_error.h" +#include "core/tls.h" +#include "input/controller.h" namespace Libraries::UserService { @@ -105,15 +112,23 @@ int PS4_SYSV_ABI sceUserServiceGetDiscPlayerFlag() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceUserServiceGetEvent(OrbisUserServiceEvent* event) { - LOG_TRACE(Lib_UserService, "(DUMMY) called"); - // fake a loggin event - static bool logged_in = false; +std::queue user_service_event_queue = {}; - if (!logged_in) { - logged_in = true; - event->event = OrbisUserServiceEventType::Login; - event->userId = 1; +void AddUserServiceEvent(const OrbisUserServiceEvent e) { + LOG_DEBUG(Lib_UserService, "Event added to queue: {} {}", (u8)e.event, e.userId); + user_service_event_queue.push(e); +} + +s32 PS4_SYSV_ABI sceUserServiceGetEvent(OrbisUserServiceEvent* event) { + LOG_TRACE(Lib_UserService, "called"); + + if (!user_service_event_queue.empty()) { + OrbisUserServiceEvent& temp = user_service_event_queue.front(); + event->event = temp.event; + event->userId = temp.userId; + user_service_event_queue.pop(); + LOG_INFO(Lib_UserService, "Event processed by the game: {} {}", (u8)temp.event, + temp.userId); return ORBIS_OK; } @@ -496,8 +511,7 @@ s32 PS4_SYSV_ABI sceUserServiceGetInitialUser(int* user_id) { LOG_ERROR(Lib_UserService, "user_id is null"); return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT; } - // select first user (TODO add more) - *user_id = 1; + *user_id = UserManagement.GetDefaultUser().user_id; return ORBIS_OK; } @@ -567,20 +581,29 @@ int PS4_SYSV_ABI sceUserServiceGetLoginFlag() { } s32 PS4_SYSV_ABI sceUserServiceGetLoginUserIdList(OrbisUserServiceLoginUserIdList* userIdList) { - LOG_DEBUG(Lib_UserService, "called"); if (userIdList == nullptr) { - LOG_ERROR(Lib_UserService, "user_id is null"); + LOG_ERROR(Lib_UserService, "userIdList is null"); return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT; } - // TODO only first user, do the others as well - userIdList->user_id[0] = 1; - userIdList->user_id[1] = ORBIS_USER_SERVICE_USER_ID_INVALID; - userIdList->user_id[2] = ORBIS_USER_SERVICE_USER_ID_INVALID; - userIdList->user_id[3] = ORBIS_USER_SERVICE_USER_ID_INVALID; + // Initialize all slots to invalid (-1) + for (int i = 0; i < ORBIS_USER_SERVICE_MAX_LOGIN_USERS; i++) { + userIdList->user_id[i] = ORBIS_USER_SERVICE_USER_ID_INVALID; + } + + auto& user_manager = UserManagement; + + auto logged_in_users = user_manager.GetLoggedInUsers(); + + for (int i = 0; i < ORBIS_USER_SERVICE_MAX_LOGIN_USERS; i++) { + s32 id = + logged_in_users[i] ? logged_in_users[i]->user_id : ORBIS_USER_SERVICE_USER_ID_INVALID; + userIdList->user_id[i] = id; + LOG_DEBUG(Lib_UserService, "Slot {}: User ID {} (port {})", i, id, + logged_in_users[i] ? logged_in_users[i]->player_index : -1); + } return ORBIS_OK; } - int PS4_SYSV_ABI sceUserServiceGetMicLevel() { LOG_ERROR(Lib_UserService, "(STUBBED) called"); return ORBIS_OK; @@ -1048,7 +1071,7 @@ s32 PS4_SYSV_ABI sceUserServiceGetUserColor(int user_id, OrbisUserServiceUserCol LOG_ERROR(Lib_UserService, "color is null"); return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT; } - *color = OrbisUserServiceUserColor::Blue; + *color = (OrbisUserServiceUserColor)UserManagement.GetUserByID(user_id)->user_color; return ORBIS_OK; } @@ -1068,12 +1091,18 @@ int PS4_SYSV_ABI sceUserServiceGetUserGroupNum() { } s32 PS4_SYSV_ABI sceUserServiceGetUserName(int user_id, char* user_name, std::size_t size) { - LOG_DEBUG(Lib_UserService, "called user_id = {} ,size = {} ", user_id, size); + LOG_DEBUG(Lib_UserService, "called user_id = {}, size = {} ", user_id, size); if (user_name == nullptr) { LOG_ERROR(Lib_UserService, "user_name is null"); return ORBIS_USER_SERVICE_ERROR_INVALID_ARGUMENT; } - std::string name = Config::getUserName(); + std::string name = "shadPS4"; + auto const* u = UserManagement.GetUserByID(user_id); + if (u != nullptr) { + name = u->user_name; + } else { + LOG_ERROR(Lib_UserService, "No user found"); + } if (size < name.length()) { LOG_ERROR(Lib_UserService, "buffer is too short"); return ORBIS_USER_SERVICE_ERROR_BUFFER_TOO_SHORT; diff --git a/src/core/libraries/system/userservice.h b/src/core/libraries/system/userservice.h index 30920e002..799bf89ba 100644 --- a/src/core/libraries/system/userservice.h +++ b/src/core/libraries/system/userservice.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later // reference : // https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/master/include/orbis/_types/user.h @@ -57,6 +57,8 @@ struct OrbisUserServiceEvent { OrbisUserServiceUserId userId; }; +void AddUserServiceEvent(const OrbisUserServiceEvent e); + int PS4_SYSV_ABI sceUserServiceInitializeForShellCore(); int PS4_SYSV_ABI sceUserServiceTerminateForShellCore(); int PS4_SYSV_ABI sceUserServiceDestroyUser(); diff --git a/src/core/libraries/system_gesture/system_gesture.cpp b/src/core/libraries/system_gesture/system_gesture.cpp index 304a11612..a6455e05e 100644 --- a/src/core/libraries/system_gesture/system_gesture.cpp +++ b/src/core/libraries/system_gesture/system_gesture.cpp @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/logging/log.h" #include "common/singleton.h" #include "core/libraries/libs.h" diff --git a/src/core/libraries/usbd/usbd.cpp b/src/core/libraries/usbd/usbd.cpp index 0708c3dd7..52d6aec66 100644 --- a/src/core/libraries/usbd/usbd.cpp +++ b/src/core/libraries/usbd/usbd.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" @@ -9,7 +9,7 @@ #include #include -#include "common/config.h" +#include "core/emulator_settings.h" namespace Libraries::Usbd { @@ -457,14 +457,14 @@ int PS4_SYSV_ABI Func_D56B43060720B1E0() { } void RegisterLib(Core::Loader::SymbolsResolver* sym) { - switch (Config::getUsbDeviceBackend()) { - case Config::SkylandersPortal: + switch (EmulatorSettings.GetUsbDeviceBackend()) { + case UsbBackendType::SkylandersPortal: usb_backend = std::make_shared(); break; - case Config::InfinityBase: + case UsbBackendType::InfinityBase: usb_backend = std::make_shared(); break; - case Config::DimensionsToypad: + case UsbBackendType::DimensionsToypad: usb_backend = std::make_shared(); break; default: diff --git a/src/core/libraries/videoout/driver.cpp b/src/core/libraries/videoout/driver.cpp index bebbf9602..9db70569b 100644 --- a/src/core/libraries/videoout/driver.cpp +++ b/src/core/libraries/videoout/driver.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/thread.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/time.h" #include "core/libraries/videoout/driver.h" #include "core/libraries/videoout/videoout_error.h" @@ -268,7 +268,8 @@ void VideoOutDriver::SubmitFlipInternal(VideoOutPort* port, s32 index, s64 flip_ } void VideoOutDriver::PresentThread(std::stop_token token) { - const std::chrono::nanoseconds vblank_period(1000000000 / Config::vblankFreq()); + const std::chrono::nanoseconds vblank_period(1000000000 / + EmulatorSettings.GetVblankFrequency()); Common::SetCurrentThreadName("shadPS4:PresentThread"); Common::SetCurrentThreadRealtime(vblank_period); diff --git a/src/core/libraries/videoout/video_out.cpp b/src/core/libraries/videoout/video_out.cpp index 1b8a6b59d..7714eb2b5 100644 --- a/src/core/libraries/videoout/video_out.cpp +++ b/src/core/libraries/videoout/video_out.cpp @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/libs.h" #include "core/libraries/system/userservice.h" #include "core/libraries/videoout/driver.h" @@ -455,8 +455,8 @@ s32 PS4_SYSV_ABI sceVideoOutSetWindowModeMargins(s32 handle, s32 top, s32 bottom } void RegisterLib(Core::Loader::SymbolsResolver* sym) { - driver = std::make_unique(Config::getInternalScreenWidth(), - Config::getInternalScreenHeight()); + driver = std::make_unique(EmulatorSettings.GetInternalScreenWidth(), + EmulatorSettings.GetInternalScreenHeight()); LIB_FUNCTION("SbU3dwp80lQ", "libSceVideoOut", 1, "libSceVideoOut", sceVideoOutGetFlipStatus); LIB_FUNCTION("U46NwOiJpys", "libSceVideoOut", 1, "libSceVideoOut", sceVideoOutSubmitFlip); diff --git a/src/core/linker.cpp b/src/core/linker.cpp index 0b80ecacc..889f3a298 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -4,7 +4,6 @@ #include "common/alignment.h" #include "common/arch.h" #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/logging/log.h" #include "common/path_util.h" @@ -13,14 +12,20 @@ #include "core/aerolib/aerolib.h" #include "core/aerolib/stubs.h" #include "core/devtools/widget/module_list.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/threads.h" +#include "core/libraries/sysmodule/sysmodule.h" #include "core/linker.h" #include "core/memory.h" #include "core/tls.h" #include "ipc/ipc.h" +#ifndef _WIN32 +#include +#endif + namespace Core { static PS4_SYSV_ABI void ProgramExitFunc() { @@ -56,7 +61,7 @@ Linker::Linker() : memory{Memory::Instance()} {} Linker::~Linker() = default; void Linker::Execute(const std::vector& args) { - if (Config::debugDump()) { + if (EmulatorSettings.IsDebugDump()) { DebugDump(); } @@ -106,11 +111,17 @@ void Linker::Execute(const std::vector& args) { main_thread.Run([this, module, &args](std::stop_token) { Common::SetCurrentThreadName("Game:Main"); +#ifndef _WIN32 // Clear any existing signal mask for game threads. + sigset_t emptyset; + sigemptyset(&emptyset); + pthread_sigmask(SIG_SETMASK, &emptyset, nullptr); +#endif if (auto& ipc = IPC::Instance()) { ipc.WaitForStart(); } - LoadSharedLibraries(); + // Have libSceSysmodule preload our libraries. + Libraries::SysModule::sceSysmodulePreloadModuleForLibkernel(); // Simulate libSceGnmDriver initialization, which maps a chunk of direct memory. // Some games fail without accurately emulating this behavior. @@ -350,8 +361,10 @@ bool Linker::Resolve(const std::string& name, Loader::SymbolType sym_type, Modul return_info->virtual_address = AeroLib::GetStub(sr.name.c_str()); return_info->name = "Unknown !!!"; } - LOG_ERROR(Core_Linker, "Linker: Stub resolved {} as {} (lib: {}, mod: {})", sr.name, - return_info->name, library->name, module->name); + if (library->name != "libc" && library->name != "libSceFios2") { + LOG_WARNING(Core_Linker, "Linker: Stub resolved {} as {} (lib: {}, mod: {})", sr.name, + return_info->name, library->name, module->name); + } return false; } diff --git a/src/core/linker.h b/src/core/linker.h index 8ffcd9d45..895901f08 100644 --- a/src/core/linker.h +++ b/src/core/linker.h @@ -54,17 +54,16 @@ struct EntryParams { }; struct HeapAPI { - PS4_SYSV_ABI void* (*heap_malloc)(size_t); + PS4_SYSV_ABI void* (*heap_malloc)(u64); PS4_SYSV_ABI void (*heap_free)(void*); - PS4_SYSV_ABI void* (*heap_calloc)(size_t, size_t); - PS4_SYSV_ABI void* (*heap_realloc)(void*, size_t); - PS4_SYSV_ABI void* (*heap_memalign)(size_t, size_t); - PS4_SYSV_ABI int (*heap_posix_memalign)(void**, size_t, size_t); - // NOTE: Fields below may be inaccurate - PS4_SYSV_ABI int (*heap_reallocalign)(void); - PS4_SYSV_ABI void (*heap_malloc_stats)(void); - PS4_SYSV_ABI int (*heap_malloc_stats_fast)(void); - PS4_SYSV_ABI size_t (*heap_malloc_usable_size)(void*); + PS4_SYSV_ABI void* (*heap_calloc)(u64, u64); + PS4_SYSV_ABI void* (*heap_realloc)(void*, u64); + PS4_SYSV_ABI void* (*heap_memalign)(u64, u64); + PS4_SYSV_ABI s32 (*heap_posix_memalign)(void**, u64, u64); + PS4_SYSV_ABI s32 (*heap_reallocalign)(void*, u64, u64); + PS4_SYSV_ABI s32 (*heap_malloc_stats)(void*); + PS4_SYSV_ABI s32 (*heap_malloc_stats_fast)(void*); + PS4_SYSV_ABI u64 (*heap_malloc_usable_size)(void*); }; using AppHeapAPI = HeapAPI*; @@ -125,11 +124,10 @@ public: } } - void LoadSharedLibraries() { + void RelocateAllImports() { + std::scoped_lock lk{mutex}; for (auto& module : m_modules) { - if (module->IsSharedLib()) { - module->Start(0, nullptr, nullptr); - } + Relocate(module.get()); } } diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 9d26142ce..a340c3643 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -3,9 +3,9 @@ #include "common/alignment.h" #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/elf_info.h" +#include "core/emulator_settings.h" #include "core/file_sys/fs.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/orbis_error.h" @@ -37,11 +37,11 @@ void MemoryManager::SetupMemoryRegions(u64 flexible_size, bool use_extended_mem1 bool use_extended_mem2) { const bool is_neo = ::Libraries::Kernel::sceKernelIsNeoMode(); auto total_size = is_neo ? ORBIS_KERNEL_TOTAL_MEM_PRO : ORBIS_KERNEL_TOTAL_MEM; - if (Config::isDevKitConsole()) { + if (EmulatorSettings.IsDevKit()) { total_size = is_neo ? ORBIS_KERNEL_TOTAL_MEM_DEV_PRO : ORBIS_KERNEL_TOTAL_MEM_DEV; } - s32 extra_dmem = Config::getExtraDmemInMbytes(); - if (Config::getExtraDmemInMbytes() != 0) { + s32 extra_dmem = EmulatorSettings.GetExtraDmemInMBytes(); + if (extra_dmem != 0) { LOG_WARNING(Kernel_Vmm, "extraDmemInMbytes is {} MB! Old Direct Size: {:#x} -> New Direct Size: {:#x}", extra_dmem, total_size, total_size + extra_dmem * 1_MB); @@ -1223,13 +1223,16 @@ s32 MemoryManager::SetDirectMemoryType(VAddr addr, u64 size, s32 memory_type) { // Increment phys_handle phys_handle++; } - - // Check if VMA can be merged with adjacent areas after physical area modifications. - vma_handle = MergeAdjacent(vma_map, vma_handle); } current_addr += size_in_vma; remaining_size -= size_in_vma; - vma_handle++; + + // Check if VMA can be merged with adjacent areas after modifications. + vma_handle = MergeAdjacent(vma_map, vma_handle); + if (vma_handle->second.base + vma_handle->second.size <= current_addr) { + // If we're now in the next VMA, then go to the next handle. + vma_handle++; + } } return ORBIS_OK; @@ -1262,10 +1265,15 @@ void MemoryManager::NameVirtualRange(VAddr virtual_addr, u64 size, std::string_v vma.name = name; } } - it = MergeAdjacent(vma_map, it); remaining_size -= size_in_vma; current_addr += size_in_vma; - it++; + + // Check if VMA can be merged with adjacent areas after modifications. + it = MergeAdjacent(vma_map, it); + if (it->second.base + it->second.size <= current_addr) { + // If we're now in the next VMA, then go to the next handle. + it++; + } } } diff --git a/src/core/module.h b/src/core/module.h index 778344e33..8dde0f467 100644 --- a/src/core/module.h +++ b/src/core/module.h @@ -1,12 +1,12 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include -#include "common/config.h" #include "common/types.h" +#include "core/emulator_settings.h" #include "core/loader/elf.h" #include "core/loader/symbols_resolver.h" @@ -166,7 +166,7 @@ public: } bool IsSystemLib() { - auto system_path = Config::getSysModulesPath(); + auto system_path = EmulatorSettings.GetSysModulesDir(); if (file.string().starts_with(system_path.string().c_str())) { return true; } diff --git a/src/core/signals.cpp b/src/core/signals.cpp index 70b431d39..f9b45bab7 100644 --- a/src/core/signals.cpp +++ b/src/core/signals.cpp @@ -21,7 +21,7 @@ #ifndef _WIN32 namespace Libraries::Kernel { void SigactionHandler(int native_signum, siginfo_t* inf, ucontext_t* raw_context); -extern std::array Handlers; +extern std::array Handlers; } // namespace Libraries::Kernel #endif @@ -86,7 +86,7 @@ void SignalHandler(int sig, siginfo_t* info, void* raw_context) { if (!signals->DispatchAccessViolation(raw_context, info->si_addr)) { // If the guest has installed a custom signal handler, and the access violation didn't // come from HLE memory tracking, pass the signal on - if (Libraries::Kernel::Handlers[sig]) { + if (Libraries::Kernel::Handlers[Libraries::Kernel::NativeToOrbisSignal(sig)]) { Libraries::Kernel::SigactionHandler(sig, info, reinterpret_cast(raw_context)); return; @@ -99,7 +99,7 @@ void SignalHandler(int sig, siginfo_t* info, void* raw_context) { } case SIGILL: if (!signals->DispatchIllegalInstruction(raw_context)) { - if (Libraries::Kernel::Handlers[sig]) { + if (Libraries::Kernel::Handlers[Libraries::Kernel::NativeToOrbisSignal(sig)]) { Libraries::Kernel::SigactionHandler(sig, info, reinterpret_cast(raw_context)); return; diff --git a/src/core/signals.h b/src/core/signals.h index 90801debb..011383693 100644 --- a/src/core/signals.h +++ b/src/core/signals.h @@ -10,10 +10,8 @@ #ifdef _WIN32 #define SIGSLEEP -1 -#elif defined(__APPLE__) -#define SIGSLEEP SIGVTALRM #else -#define SIGSLEEP SIGRTMAX +#define SIGSLEEP SIGVTALRM #endif namespace Core { diff --git a/src/core/user_manager.cpp b/src/core/user_manager.cpp new file mode 100644 index 000000000..73e881e8e --- /dev/null +++ b/src/core/user_manager.cpp @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include "emulator_settings.h" +#include "libraries/system/userservice.h" +#include "user_manager.h" +#include "user_settings.h" + +bool UserManager::AddUser(const User& user) { + for (const auto& u : m_users.user) { + if (u.user_id == user.user_id) + return false; // already exists + } + + m_users.user.push_back(user); + + // Create user home directory and subfolders + const auto user_dir = EmulatorSettings.GetHomeDir() / std::to_string(user.user_id); + + std::error_code ec; + if (!std::filesystem::exists(user_dir)) { + std::filesystem::create_directory(user_dir, ec); + std::filesystem::create_directory(user_dir / "savedata", ec); + std::filesystem::create_directory(user_dir / "trophy", ec); + std::filesystem::create_directory(user_dir / "inputs", ec); + } + + Save(); + return true; +} + +bool UserManager::RemoveUser(s32 user_id) { + auto it = std::remove_if(m_users.user.begin(), m_users.user.end(), + [user_id](const User& u) { return u.user_id == user_id; }); + if (it == m_users.user.end()) + return false; // not found + + const auto user_dir = EmulatorSettings.GetHomeDir() / std::to_string(user_id); + + if (std::filesystem::exists(user_dir)) { + std::error_code ec; + std::filesystem::remove_all(user_dir, ec); + } + + m_users.user.erase(it, m_users.user.end()); + Save(); + return true; +} + +bool UserManager::RenameUser(s32 user_id, const std::string& new_name) { + // Find user in the internal list + for (auto& user : m_users.user) { + if (user.user_id == user_id) { + if (user.user_name == new_name) + return true; // no change + + user.user_name = new_name; + return true; + } + } + Save(); + return false; +} + +User* UserManager::GetUserByID(s32 user_id) { + for (auto& u : m_users.user) { + if (u.user_id == user_id) + return &u; + } + return nullptr; +} + +User* UserManager::GetUserByPlayerIndex(s32 index) { + for (auto& u : m_users.user) { + if (u.player_index == index) + return &u; + } + return nullptr; +} + +const std::vector& UserManager::GetAllUsers() const { + return m_users.user; +} + +Users UserManager::CreateDefaultUsers() { + Users default_users; + default_users.user = { + { + .user_id = 1000, + .user_name = "shadPS4", + .user_color = 1, + .player_index = 1, + }, + { + .user_id = 1001, + .user_name = "shadPS4-2", + .user_color = 2, + .player_index = 2, + }, + { + .user_id = 1002, + .user_name = "shadPS4-3", + .user_color = 3, + .player_index = 3, + }, + { + .user_id = 1003, + .user_name = "shadPS4-4", + .user_color = 4, + .player_index = 4, + }, + }; + + for (auto& u : default_users.user) { + const auto user_dir = EmulatorSettings.GetHomeDir() / std::to_string(u.user_id); + + if (!std::filesystem::exists(user_dir)) { + std::filesystem::create_directory(user_dir); + std::filesystem::create_directory(user_dir / "savedata"); + std::filesystem::create_directory(user_dir / "trophy"); + std::filesystem::create_directory(user_dir / "inputs"); + } + } + + return default_users; +} + +bool UserManager::SetDefaultUser(u32 user_id) { + auto it = std::find_if(m_users.user.begin(), m_users.user.end(), + [user_id](const User& u) { return u.user_id == user_id; }); + if (it == m_users.user.end()) + return false; + + SetControllerPort(user_id, 1); // Set default user to port 1 + return Save(); +} + +User UserManager::GetDefaultUser() { + return *GetUserByPlayerIndex(1); +} + +void UserManager::SetControllerPort(u32 user_id, int port) { + for (auto& u : m_users.user) { + if (u.user_id != user_id && u.player_index == port) + u.player_index = -1; + if (u.user_id == user_id) + u.player_index = port; + } + Save(); +} +// Returns a list of users that have valid home directories +std::vector UserManager::GetValidUsers() const { + std::vector result; + result.reserve(m_users.user.size()); + + const auto home_dir = EmulatorSettings.GetHomeDir(); + + for (const auto& user : m_users.user) { + const auto user_dir = home_dir / std::to_string(user.user_id); + if (std::filesystem::exists(user_dir)) { + result.push_back(user); + } + } + + return result; +} + +LoggedInUsers UserManager::GetLoggedInUsers() const { + return logged_in_users; +} + +using namespace Libraries::UserService; + +void UserManager::LoginUser(User* u, s32 player_index) { + if (!u) { + return; + } + u->logged_in = true; + // u->player_index = player_index; + AddUserServiceEvent({OrbisUserServiceEventType::Login, u->user_id}); + logged_in_users[player_index - 1] = u; +} + +void UserManager::LogoutUser(User* u) { + if (!u) { + return; + } + u->logged_in = false; + AddUserServiceEvent({OrbisUserServiceEventType::Logout, u->user_id}); + logged_in_users[u->player_index - 1] = {}; +} + +bool UserManager::Save() const { + return UserSettings.Save(); +} \ No newline at end of file diff --git a/src/core/user_manager.h b/src/core/user_manager.h new file mode 100644 index 000000000..9273ef0cb --- /dev/null +++ b/src/core/user_manager.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#include +#include +#include "common/types.h" + +struct User { + s32 user_id = -1; + std::string user_name = ""; + u32 user_color; + int player_index = 0; // 1-4 + + bool logged_in = false; +}; + +struct Users { + std::vector user{}; + std::string commit_hash{}; +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, user_id, user_color, user_name, player_index) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Users, user, commit_hash) + +using LoggedInUsers = std::array; + +class UserManager { +public: + UserManager() = default; + + bool AddUser(const User& user); + bool RemoveUser(s32 user_id); + bool RenameUser(s32 user_id, const std::string& new_name); + User* GetUserByID(s32 user_id); + User* GetUserByPlayerIndex(s32 index); + const std::vector& GetAllUsers() const; + Users CreateDefaultUsers(); + bool SetDefaultUser(u32 user_id); + User GetDefaultUser(); + void SetControllerPort(u32 user_id, int port); + std::vector GetValidUsers() const; + LoggedInUsers GetLoggedInUsers() const; + void LoginUser(User* u, s32 player_index); + void LogoutUser(User* u); + + Users& GetUsers() { + return m_users; + } + const Users& GetUsers() const { + return m_users; + } + + bool Save() const; + +private: + Users m_users; + LoggedInUsers logged_in_users{}; +}; diff --git a/src/core/user_settings.cpp b/src/core/user_settings.cpp new file mode 100644 index 000000000..cf569a68a --- /dev/null +++ b/src/core/user_settings.cpp @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "user_settings.h" + +using json = nlohmann::json; + +// Singleton storage +std::shared_ptr UserSettingsImpl::s_instance = nullptr; +std::mutex UserSettingsImpl::s_mutex; + +// Singleton +UserSettingsImpl::UserSettingsImpl() = default; + +UserSettingsImpl::~UserSettingsImpl() { + Save(); +} + +std::shared_ptr UserSettingsImpl::GetInstance() { + std::lock_guard lock(s_mutex); + if (!s_instance) + s_instance = std::make_shared(); + return s_instance; +} + +void UserSettingsImpl::SetInstance(std::shared_ptr instance) { + std::lock_guard lock(s_mutex); + s_instance = std::move(instance); +} + +bool UserSettingsImpl::Save() const { + const auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "users.json"; + try { + json j; + j["Users"] = m_userManager.GetUsers(); + j["Users"]["commit_hash"] = std::string(Common::g_scm_rev); + + std::ofstream out(path); + if (!out) { + LOG_ERROR(Config, "Failed to open user settings for writing: {}", path.string()); + return false; + } + out << std::setw(2) << j; + return !out.fail(); + } catch (const std::exception& e) { + LOG_ERROR(Config, "Error saving user settings: {}", e.what()); + return false; + } +} + +bool UserSettingsImpl::Load() { + const auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "users.json"; + try { + if (!std::filesystem::exists(path)) { + LOG_DEBUG(Config, "User settings file not found: {}", path.string()); + // Create default user if no file exists + if (m_userManager.GetUsers().user.empty()) { + m_userManager.GetUsers() = m_userManager.CreateDefaultUsers(); + } + Save(); // Save default users + return false; + } + + std::ifstream in(path); + if (!in) { + LOG_ERROR(Config, "Failed to open user settings: {}", path.string()); + return false; + } + + json j; + in >> j; + + // Create a default Users object + auto default_users = m_userManager.CreateDefaultUsers(); + + // Convert default_users to json for merging + json default_json; + default_json["Users"] = default_users; + + // Merge the loaded json with defaults (preserves existing data, adds missing fields) + if (j.contains("Users")) { + json current = default_json["Users"]; + current.update(j["Users"]); + m_userManager.GetUsers() = current.get(); + } else { + m_userManager.GetUsers() = default_users; + } + + if (m_userManager.GetUsers().commit_hash != Common::g_scm_rev) { + Save(); + } + + LOG_DEBUG(Config, "User settings loaded successfully"); + return true; + } catch (const std::exception& e) { + LOG_ERROR(Config, "Error loading user settings: {}", e.what()); + // Fall back to defaults + if (m_userManager.GetUsers().user.empty()) { + m_userManager.GetUsers() = m_userManager.CreateDefaultUsers(); + } + return false; + } +} \ No newline at end of file diff --git a/src/core/user_settings.h b/src/core/user_settings.h new file mode 100644 index 000000000..45cfa91dd --- /dev/null +++ b/src/core/user_settings.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/logging/log.h" +#include "common/types.h" +#include "core/user_manager.h" + +#define UserSettings (*UserSettingsImpl::GetInstance()) + +#define UserManagement UserSettings.GetUserManager() + +// ------------------------------- +// User settings +// ------------------------------- + +class UserSettingsImpl { +public: + UserSettingsImpl(); + ~UserSettingsImpl(); + + UserManager& GetUserManager() { + return m_userManager; + } + + bool Save() const; + bool Load(); + + static std::shared_ptr GetInstance(); + static void SetInstance(std::shared_ptr instance); + +private: + UserManager m_userManager; + + static std::shared_ptr s_instance; + static std::mutex s_mutex; +}; \ No newline at end of file diff --git a/src/emulator.cpp b/src/emulator.cpp index 18d3024dc..034d30ed8 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -10,11 +10,11 @@ #include #include -#include "common/config.h" #include "common/debug.h" #include "common/logging/backend.h" #include "common/logging/log.h" #include "common/thread.h" +#include "core/emulator_settings.h" #include "core/ipc/ipc.h" #ifdef ENABLE_DISCORD_RPC #include "common/discord_rpc_handler.h" @@ -28,24 +28,18 @@ #include "common/singleton.h" #include "core/debugger.h" #include "core/devtools/widget/module_list.h" +#include "core/emulator_settings.h" #include "core/emulator_state.h" #include "core/file_format/psf.h" #include "core/file_format/trp.h" #include "core/file_sys/fs.h" -#include "core/libraries/disc_map/disc_map.h" -#include "core/libraries/font/font.h" -#include "core/libraries/font/fontft.h" -#include "core/libraries/jpeg/jpegenc.h" #include "core/libraries/kernel/kernel.h" -#include "core/libraries/libc_internal/libc_internal.h" -#include "core/libraries/libpng/pngenc.h" #include "core/libraries/libs.h" -#include "core/libraries/ngs2/ngs2.h" #include "core/libraries/np/np_trophy.h" -#include "core/libraries/rtc/rtc.h" #include "core/libraries/save_data/save_backup.h" #include "core/linker.h" #include "core/memory.h" +#include "core/user_settings.h" #include "emulator.h" #include "video_core/cache_storage.h" #include "video_core/renderdoc.h" @@ -58,6 +52,7 @@ #include #include #endif +#include Frontend::WindowSDL* g_window = nullptr; @@ -204,19 +199,23 @@ void Emulator::Run(std::filesystem::path file, std::vector args, } game_info.game_folder = game_folder; - - Config::load(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".toml"), - true); - - if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / - (id + ".toml"))) { - EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(true); + std::filesystem::path npbindPath = game_folder / "sce_sys/npbind.dat"; + NPBindFile npbind; + if (!npbind.Load(npbindPath.string())) { + LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file"); } else { - EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(false); + auto npCommIds = npbind.GetNpCommIds(); + if (npCommIds.empty()) { + LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat"); + } else { + game_info.npCommIds = std::move(npCommIds); + } } + EmulatorSettings.Load(id); + // Initialize logging as soon as possible - if (!id.empty() && Config::getSeparateLogFilesEnabled()) { + if (!id.empty() && EmulatorSettings.IsSeparateLoggingEnabled()) { Common::Log::Initialize(id + ".log"); } else { Common::Log::Initialize(); @@ -234,32 +233,35 @@ void Emulator::Run(std::filesystem::path file, std::vector args, LOG_INFO(Loader, "Description {}", Common::g_scm_desc); LOG_INFO(Loader, "Remote {}", Common::g_scm_remote_url); - const bool has_game_config = std::filesystem::exists( - Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".toml")); - LOG_INFO(Config, "Game-specific config exists: {}", has_game_config); + LOG_INFO(Config, "Game-specific config used: {}", + EmulatorState::GetInstance()->IsGameSpecifigConfigUsed()); - LOG_INFO(Config, "General LogType: {}", Config::getLogType()); - LOG_INFO(Config, "General isIdenticalLogGrouped: {}", Config::groupIdenticalLogs()); - LOG_INFO(Config, "General isNeo: {}", Config::isNeoModeConsole()); - LOG_INFO(Config, "General isDevKit: {}", Config::isDevKitConsole()); - LOG_INFO(Config, "General isConnectedToNetwork: {}", Config::getIsConnectedToNetwork()); - LOG_INFO(Config, "General isPsnSignedIn: {}", Config::getPSNSignedIn()); - LOG_INFO(Config, "GPU isNullGpu: {}", Config::nullGpu()); - LOG_INFO(Config, "GPU readbacksMode: {}", Config::getReadbacksMode()); - LOG_INFO(Config, "GPU readbackLinearImages: {}", Config::readbackLinearImages()); - LOG_INFO(Config, "GPU directMemoryAccess: {}", Config::directMemoryAccess()); - LOG_INFO(Config, "GPU shouldDumpShaders: {}", Config::dumpShaders()); - LOG_INFO(Config, "GPU vblankFrequency: {}", Config::vblankFreq()); - LOG_INFO(Config, "GPU shouldCopyGPUBuffers: {}", Config::copyGPUCmdBuffers()); - LOG_INFO(Config, "Vulkan gpuId: {}", Config::getGpuId()); - LOG_INFO(Config, "Vulkan vkValidation: {}", Config::vkValidationEnabled()); - LOG_INFO(Config, "Vulkan vkValidationCore: {}", Config::vkValidationCoreEnabled()); - LOG_INFO(Config, "Vulkan vkValidationSync: {}", Config::vkValidationSyncEnabled()); - LOG_INFO(Config, "Vulkan vkValidationGpu: {}", Config::vkValidationGpuEnabled()); - LOG_INFO(Config, "Vulkan crashDiagnostics: {}", Config::getVkCrashDiagnosticEnabled()); - LOG_INFO(Config, "Vulkan hostMarkers: {}", Config::getVkHostMarkersEnabled()); - LOG_INFO(Config, "Vulkan guestMarkers: {}", Config::getVkGuestMarkersEnabled()); - LOG_INFO(Config, "Vulkan rdocEnable: {}", Config::isRdocEnabled()); + LOG_INFO(Config, "General LogType: {}", EmulatorSettings.GetLogType()); + LOG_INFO(Config, "General isIdenticalLogGrouped: {}", EmulatorSettings.IsIdenticalLogGrouped()); + LOG_INFO(Config, "General isNeo: {}", EmulatorSettings.IsNeo()); + LOG_INFO(Config, "General isDevKit: {}", EmulatorSettings.IsDevKit()); + LOG_INFO(Config, "General isConnectedToNetwork: {}", EmulatorSettings.IsConnectedToNetwork()); + LOG_INFO(Config, "General isPsnSignedIn: {}", EmulatorSettings.IsPSNSignedIn()); + LOG_INFO(Config, "GPU isNullGpu: {}", EmulatorSettings.IsNullGPU()); + LOG_INFO(Config, "GPU readbacksMode: {}", EmulatorSettings.GetReadbacksMode()); + LOG_INFO(Config, "GPU readbackLinearImages: {}", + EmulatorSettings.IsReadbackLinearImagesEnabled()); + LOG_INFO(Config, "GPU directMemoryAccess: {}", EmulatorSettings.IsDirectMemoryAccessEnabled()); + LOG_INFO(Config, "GPU shouldDumpShaders: {}", EmulatorSettings.IsDumpShaders()); + LOG_INFO(Config, "GPU vblankFrequency: {}", EmulatorSettings.GetVblankFrequency()); + LOG_INFO(Config, "GPU shouldCopyGPUBuffers: {}", EmulatorSettings.IsCopyGpuBuffers()); + LOG_INFO(Config, "Vulkan gpuId: {}", EmulatorSettings.GetGpuId()); + LOG_INFO(Config, "Vulkan vkValidation: {}", EmulatorSettings.IsVkValidationEnabled()); + LOG_INFO(Config, "Vulkan vkValidationCore: {}", EmulatorSettings.IsVkValidationCoreEnabled()); + LOG_INFO(Config, "Vulkan vkValidationSync: {}", EmulatorSettings.IsVkValidationSyncEnabled()); + LOG_INFO(Config, "Vulkan vkValidationGpu: {}", EmulatorSettings.IsVkValidationGpuEnabled()); + LOG_INFO(Config, "Vulkan crashDiagnostics: {}", EmulatorSettings.IsVkCrashDiagnosticEnabled()); + LOG_INFO(Config, "Vulkan hostMarkers: {}", EmulatorSettings.IsVkHostMarkersEnabled()); + LOG_INFO(Config, "Vulkan guestMarkers: {}", EmulatorSettings.IsVkGuestMarkersEnabled()); + LOG_INFO(Config, "Vulkan rdocEnable: {}", EmulatorSettings.IsRenderdocEnabled()); + LOG_INFO(Config, "Vulkan PipelineCacheEnabled: {}", EmulatorSettings.IsPipelineCacheEnabled()); + LOG_INFO(Config, "Vulkan PipelineCacheArchived: {}", + EmulatorSettings.IsPipelineCacheArchived()); hwinfo::Memory ram; hwinfo::OS os; @@ -295,7 +297,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, // Initialize components memory = Core::Memory::Instance(); - controller = Common::Singleton::Instance(); + controllers = Common::Singleton::Instance(); linker = Common::Singleton::Instance(); // Load renderdoc module @@ -304,15 +306,28 @@ void Emulator::Run(std::filesystem::path file, std::vector args, // Initialize patcher and trophies if (!id.empty()) { MemoryPatcher::g_game_serial = id; - Libraries::Np::NpTrophy::game_serial = id; - const auto trophyDir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / id / "TrophyFiles"; - if (!std::filesystem::exists(trophyDir)) { - TRP trp; - if (!trp.Extract(game_folder, id)) { - LOG_ERROR(Loader, "Couldn't extract trophies"); + int index = 0; + for (std::string npCommId : game_info.npCommIds) { + const auto trophyDir = + Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "trophy" / npCommId; + if (!std::filesystem::exists(trophyDir)) { + TRP trp; + if (!trp.Extract(game_folder, index, npCommId, trophyDir)) { + LOG_ERROR(Loader, "Couldn't extract trophies"); + } } + for (User user : UserSettings.GetUserManager().GetValidUsers()) { + auto const user_trophy_file = + Common::FS::GetUserPath(Common::FS::PathType::HomeDir) / + std::to_string(user.user_id) / "trophy" / (npCommId + ".xml"); + if (!std::filesystem::exists(user_trophy_file)) { + std::error_code discard; + std::filesystem::copy_file(trophyDir / "Xml" / "TROPCONF.XML", user_trophy_file, + discard); + } + } + index++; } } @@ -336,8 +351,9 @@ void Emulator::Run(std::filesystem::path file, std::vector args, Common::g_scm_branch, Common::g_scm_desc, game_title); } } - window = std::make_unique( - Config::getWindowWidth(), Config::getWindowHeight(), controller, window_title); + window = std::make_unique(EmulatorSettings.GetWindowWidth(), + EmulatorSettings.GetWindowHeight(), controllers, + window_title); g_window = window.get(); @@ -368,7 +384,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, VideoCore::SetOutputDir(mount_captures_dir, id); // Mount system fonts - const auto& fonts_dir = Config::getFontsPath(); + const auto& fonts_dir = EmulatorSettings.GetFontsDir(); if (!std::filesystem::exists(fonts_dir)) { std::filesystem::create_directory(fonts_dir); } @@ -405,20 +421,9 @@ void Emulator::Run(std::filesystem::path file, std::vector args, std::quick_exit(0); } - // check if we have system modules to load - LoadSystemModules(game_info.game_serial); - - // Load all prx from game's sce_module folder - mnt->IterateDirectory("/app0/sce_module", [this](const auto& path, const auto is_file) { - if (is_file) { - LOG_INFO(Loader, "Loading {}", fmt::UTF(path.u8string())); - linker->LoadModule(path); - } - }); - #ifdef ENABLE_DISCORD_RPC // Discord RPC - if (Config::getEnableDiscordRPC()) { + if (EmulatorSettings.IsDiscordRPCEnabled()) { auto* rpc = Common::Singleton::Instance(); if (rpc->getRPCEnabled() == false) { rpc->init(); @@ -556,54 +561,6 @@ void Emulator::Restart(std::filesystem::path eboot_path, std::quick_exit(0); } -void Emulator::LoadSystemModules(const std::string& game_serial) { - constexpr auto ModulesToLoad = std::to_array( - {{"libSceNgs2.sprx", &Libraries::Ngs2::RegisterLib}, - {"libSceUlt.sprx", nullptr}, - {"libSceRtc.sprx", &Libraries::Rtc::RegisterLib}, - {"libSceJpegDec.sprx", nullptr}, - {"libSceJpegEnc.sprx", &Libraries::JpegEnc::RegisterLib}, - {"libScePngEnc.sprx", &Libraries::PngEnc::RegisterLib}, - {"libSceJson.sprx", nullptr}, - {"libSceJson2.sprx", nullptr}, - {"libSceLibcInternal.sprx", &Libraries::LibcInternal::RegisterLib}, - {"libSceCesCs.sprx", nullptr}, - {"libSceAudiodec.sprx", nullptr}, - {"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont}, - {"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt}, - {"libSceFreeTypeOt.sprx", nullptr}}); - - std::vector found_modules; - const auto& sys_module_path = Config::getSysModulesPath(); - for (const auto& entry : std::filesystem::directory_iterator(sys_module_path)) { - found_modules.push_back(entry.path()); - } - for (const auto& [module_name, init_func] : ModulesToLoad) { - const auto it = std::ranges::find_if( - found_modules, [&](const auto& path) { return path.filename() == module_name; }); - if (it != found_modules.end()) { - LOG_INFO(Loader, "Loading {}", it->string()); - if (linker->LoadModule(*it) != -1) { - continue; - } - } - if (init_func) { - LOG_INFO(Loader, "Can't Load {} switching to HLE", module_name); - init_func(&linker->GetHLESymbols()); - } else { - LOG_INFO(Loader, "No HLE available for {} module", module_name); - } - } - if (!game_serial.empty() && std::filesystem::exists(sys_module_path / game_serial)) { - for (const auto& entry : - std::filesystem::directory_iterator(sys_module_path / game_serial)) { - LOG_INFO(Loader, "Loading {} from game serial file {}", entry.path().string(), - game_serial); - linker->LoadModule(entry.path()); - } - } -} - void Emulator::UpdatePlayTime(const std::string& serial) { const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); const auto filePath = (user_dir / "play_time.txt").string(); diff --git a/src/emulator.h b/src/emulator.h index f4dd32c20..d350ce16c 100644 --- a/src/emulator.h +++ b/src/emulator.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -43,7 +43,7 @@ private: void LoadSystemModules(const std::string& game_serial); Core::MemoryManager* memory; - Input::GameController* controller; + Input::GameControllers* controllers; Core::Linker* linker; std::unique_ptr window; std::chrono::steady_clock::time_point start_time; diff --git a/src/imgui/renderer/imgui_core.cpp b/src/imgui/renderer/imgui_core.cpp index 452dee013..b52a68d22 100644 --- a/src/imgui/renderer/imgui_core.cpp +++ b/src/imgui/renderer/imgui_core.cpp @@ -1,13 +1,13 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include -#include "common/config.h" #include "common/path_util.h" #include "core/debug_state.h" #include "core/devtools/layer.h" +#include "core/emulator_settings.h" #include "imgui/imgui_layer.h" #include "imgui_core.h" #include "imgui_impl_sdl3.h" @@ -219,7 +219,7 @@ void Render(const vk::CommandBuffer& cmdbuf, const vk::ImageView& image_view, return; } - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{ .pLabelName = "ImGui Render", }); @@ -244,7 +244,7 @@ void Render(const vk::CommandBuffer& cmdbuf, const vk::ImageView& image_view, cmdbuf.beginRendering(render_info); Vulkan::RenderDrawData(*draw_data, cmdbuf); cmdbuf.endRendering(); - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.endDebugUtilsLabelEXT(); } } diff --git a/src/imgui/renderer/imgui_impl_sdl3.cpp b/src/imgui/renderer/imgui_impl_sdl3.cpp index fbca90efb..679aeb8c0 100644 --- a/src/imgui/renderer/imgui_impl_sdl3.cpp +++ b/src/imgui/renderer/imgui_impl_sdl3.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later // Based on imgui_impl_sdl3.cpp from Dear ImGui repository #include -#include "common/config.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/memory.h" #include "imgui_impl_sdl3.h" #include "input/controller.h" @@ -396,7 +396,7 @@ bool ProcessEvent(const SDL_Event* event) { if (mouse_pos.x != bd->prev_mouse_pos.x || mouse_pos.y != bd->prev_mouse_pos.y) { bd->prev_mouse_pos.x = mouse_pos.x; bd->prev_mouse_pos.y = mouse_pos.y; - if (Config::getCursorState() == Config::HideCursorState::Idle) { + if (EmulatorSettings.GetCursorState() == HideCursorState::Idle) { bd->lastCursorMoveTime = bd->time; } } @@ -656,16 +656,16 @@ static void UpdateMouseCursor() { return; SdlData* bd = GetBackendData(); - s16 cursorState = Config::getCursorState(); + s16 cursorState = EmulatorSettings.GetCursorState(); ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor(); if (io.MouseDrawCursor || imgui_cursor == ImGuiMouseCursor_None || - cursorState == Config::HideCursorState::Always) { + cursorState == HideCursorState::Always) { // Hide OS mouse cursor if imgui is drawing it or if it wants no cursor SDL_HideCursor(); - } else if (cursorState == Config::HideCursorState::Idle && + } else if (cursorState == HideCursorState::Idle && bd->time - bd->lastCursorMoveTime >= - Config::getCursorHideTimeout() * SDL_GetPerformanceFrequency()) { + EmulatorSettings.GetCursorHideTimeout() * SDL_GetPerformanceFrequency()) { bool wasCursorVisible = SDL_CursorVisible(); SDL_HideCursor(); @@ -737,9 +737,8 @@ static void UpdateGamepads() { ImGuiIO& io = ImGui::GetIO(); SdlData* bd = GetBackendData(); - auto controller = Common::Singleton::Instance(); - auto engine = controller->GetEngine(); - SDL_Gamepad* SDLGamepad = engine->m_gamepad; + auto& controllers = *Common::Singleton::Instance(); + SDL_Gamepad* SDLGamepad = controllers[0]->m_sdl_gamepad; // Update list of gamepads to use if (bd->want_update_gamepads_list && bd->gamepad_mode != ImGui_ImplSDL3_GamepadMode_Manual) { if (SDLGamepad) { diff --git a/src/imgui/renderer/texture_manager.cpp b/src/imgui/renderer/texture_manager.cpp index 49f912a92..f8ac04352 100644 --- a/src/imgui/renderer/texture_manager.cpp +++ b/src/imgui/renderer/texture_manager.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 @@ -6,11 +6,11 @@ #include #include "common/assert.h" -#include "common/config.h" #include "common/io_file.h" #include "common/polyfill_thread.h" #include "common/stb.h" #include "common/thread.h" +#include "core/emulator_settings.h" #include "imgui_impl_vulkan.h" #include "texture_manager.h" @@ -152,7 +152,7 @@ void WorkerLoop() { g_job_list.pop_front(); g_job_list_mtx.unlock(); - if (Config::getVkCrashDiagnosticEnabled()) { + if (EmulatorSettings.IsVkCrashDiagnosticEnabled()) { // FIXME: Crash diagnostic hangs when building the command buffer here continue; } diff --git a/src/input/controller.cpp b/src/input/controller.cpp index 3606ad5d2..c1ba584e3 100644 --- a/src/input/controller.cpp +++ b/src/input/controller.cpp @@ -1,15 +1,20 @@ // SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include #include -#include "common/config.h" +#include +#include #include "common/logging/log.h" +#include "controller.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/time.h" #include "core/libraries/pad/pad.h" +#include "core/libraries/system/userservice.h" +#include "core/user_settings.h" #include "input/controller.h" -static std::string SelectedGamepad = ""; - namespace Input { using Libraries::Pad::OrbisPadButtonDataOffset; @@ -22,7 +27,15 @@ void State::OnButton(OrbisPadButtonDataOffset button, bool isPressed) { } } -void State::OnAxis(Axis axis, int value) { +void State::OnAxis(Axis axis, int value, bool smooth) { + auto const i = std::to_underlying(axis); + // forcibly finish the previous smoothing task by jumping to the end + axes[i] = axis_smoothing_end_values[i]; + + axis_smoothing_start_times[i] = time; + axis_smoothing_start_values[i] = axes[i]; + axis_smoothing_end_values[i] = value; + axis_smoothing_flags[i] = smooth; const auto toggle = [&](const auto button) { if (value > 0) { buttonsState |= button; @@ -40,7 +53,6 @@ void State::OnAxis(Axis axis, int value) { default: break; } - axes[static_cast(axis)] = value; } void State::OnTouchpad(int touchIndex, bool isDown, float x, float y) { @@ -61,6 +73,22 @@ void State::OnAccel(const float accel[3]) { acceleration.z = accel[2]; } +void State::UpdateAxisSmoothing() { + for (int i = 0; i < std::to_underlying(Axis::AxisMax); i++) { + // if it's not to be smoothed or close enough, just jump to the end + if (!axis_smoothing_flags[i] || std::abs(axes[i] - axis_smoothing_end_values[i]) < 16) { + if (axes[i] != axis_smoothing_end_values[i]) { + axes[i] = axis_smoothing_end_values[i]; + } + continue; + } + auto now = Libraries::Kernel::sceKernelGetProcessTime(); + f32 t = + std::clamp((now - axis_smoothing_start_times[i]) / f32{axis_smoothing_time}, 0.f, 1.f); + axes[i] = s32(axis_smoothing_start_values[i] * (1 - t) + axis_smoothing_end_values[i] * t); + } +} + GameController::GameController() : m_states_queue(64) {} void GameController::ReadState(State* state, bool* isConnected, int* connectedCount) { @@ -88,31 +116,82 @@ int GameController::ReadStates(State* states, int states_num, bool* isConnected, return ret_num; } -void GameController::Button(int id, OrbisPadButtonDataOffset button, bool is_pressed) { +void GameController::Button(OrbisPadButtonDataOffset button, bool is_pressed) { m_state.OnButton(button, is_pressed); PushState(); } -void GameController::Axis(int id, Input::Axis axis, int value) { - m_state.OnAxis(axis, value); +void GameController::Axis(Input::Axis axis, int value, bool smooth) { + m_state.OnAxis(axis, value, smooth); PushState(); } -void GameController::Gyro(int id, const float gyro[3]) { - m_state.OnGyro(gyro); +void GameController::Gyro(int id) { + m_state.OnGyro(gyro_buf); PushState(); } -void GameController::Acceleration(int id, const float acceleration[3]) { - m_state.OnAccel(acceleration); +void GameController::Acceleration(int id) { + m_state.OnAccel(accel_buf); PushState(); } -void GameController::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, - Libraries::Pad::OrbisFVector3& angularVelocity, - float deltaTime, - Libraries::Pad::OrbisFQuaternion& lastOrientation, - Libraries::Pad::OrbisFQuaternion& orientation) { +void GameController::UpdateGyro(const float gyro[3]) { + std::scoped_lock l(m_states_queue_mutex); + std::memcpy(gyro_buf, gyro, sizeof(gyro_buf)); +} + +void GameController::UpdateAcceleration(const float acceleration[3]) { + std::scoped_lock l(m_states_queue_mutex); + std::memcpy(accel_buf, acceleration, sizeof(accel_buf)); +} + +void GameController::UpdateAxisSmoothing() { + m_state.UpdateAxisSmoothing(); +} + +void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) { + colour = {r, g, b}; + if (m_sdl_gamepad != nullptr) { + SDL_SetGamepadLED(m_sdl_gamepad, r, g, b); + } +} + +void GameController::PollLightColour() { + if (m_sdl_gamepad != nullptr) { + SDL_SetGamepadLED(m_sdl_gamepad, colour.r, colour.g, colour.b); + } +} + +bool GameController::SetVibration(u8 smallMotor, u8 largeMotor) { + if (m_sdl_gamepad != nullptr) { + return SDL_RumbleGamepad(m_sdl_gamepad, (smallMotor / 255.0f) * 0xFFFF, + (largeMotor / 255.0f) * 0xFFFF, -1); + } + return true; +} + +void GameController::SetTouchpadState(int touchIndex, bool touchDown, float x, float y) { + if (touchIndex < 2) { + m_state.OnTouchpad(touchIndex, touchDown, x, y); + PushState(); + } +} + +std::array, 4> GameControllers::controller_override_colors{ + std::nullopt, std::nullopt, std::nullopt, std::nullopt}; + +void GameControllers::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, + Libraries::Pad::OrbisFVector3& angularVelocity, + float deltaTime, + Libraries::Pad::OrbisFQuaternion& lastOrientation, + Libraries::Pad::OrbisFQuaternion& orientation) { + // avoid wildly off values coming from elapsed time between two samples + // being too high, such as on the first time the controller is polled + if (deltaTime > 1.0f) { + orientation = lastOrientation; + return; + } Libraries::Pad::OrbisFQuaternion q = lastOrientation; Libraries::Pad::OrbisFQuaternion ω = {angularVelocity.x, angularVelocity.y, angularVelocity.z, 0.0f}; @@ -143,27 +222,100 @@ void GameController::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceler orientation.y, orientation.z, orientation.w); } -void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) { - if (!m_engine) { - return; - } - m_engine->SetLightBarRGB(r, g, b); -} +bool is_first_check = true; -void GameController::SetVibration(u8 smallMotor, u8 largeMotor) { - if (!m_engine) { - return; - } - m_engine->SetVibration(smallMotor, largeMotor); -} +void GameControllers::TryOpenSDLControllers() { + using namespace Libraries::UserService; + int controller_count; + s32 move_count = 0; + SDL_JoystickID* new_joysticks = SDL_GetGamepads(&controller_count); + LOG_INFO(Input, "{} controllers are currently connected", controller_count); -void GameController::SetTouchpadState(int touchIndex, bool touchDown, float x, float y) { - if (touchIndex < 2) { - m_state.OnTouchpad(touchIndex, touchDown, x, y); - PushState(); - } -} + std::unordered_set assigned_ids; + std::array slot_taken{false, false, false, false}; + for (int i = 0; i < 4; i++) { + SDL_Gamepad* pad = controllers[i]->m_sdl_gamepad; + if (pad) { + SDL_JoystickID id = SDL_GetGamepadID(pad); + bool still_connected = false; + ControllerType type = ControllerType::Standard; + for (int j = 0; j < controller_count; j++) { + if (new_joysticks[j] == id) { + still_connected = true; + assigned_ids.insert(id); + slot_taken[i] = true; + break; + } + } + if (!still_connected) { + auto u = UserManagement.GetUserByID(controllers[i]->user_id); + UserManagement.LogoutUser(u); + SDL_CloseGamepad(pad); + controllers[i]->m_sdl_gamepad = nullptr; + controllers[i]->user_id = -1; + slot_taken[i] = false; + } + } + } + + for (int j = 0; j < controller_count; j++) { + SDL_JoystickID id = new_joysticks[j]; + if (assigned_ids.contains(id)) + continue; + + SDL_Gamepad* pad = SDL_OpenGamepad(id); + if (!pad) { + continue; + } + + for (int i = 0; i < 4; i++) { + if (!slot_taken[i]) { + auto u = UserManagement.GetUserByPlayerIndex(i + 1); + if (!u) { + LOG_INFO(Input, "User {} not found", i + 1); + continue; // for now, if you don't specify who Player N is in the config, + // Player N won't be registered at all + } + auto* c = controllers[i]; + c->m_sdl_gamepad = pad; + LOG_INFO(Input, "Gamepad registered for slot {}! Handle: {}", i, + SDL_GetGamepadID(pad)); + c->user_id = u->user_id; + slot_taken[i] = true; + UserManagement.LoginUser(u, i + 1); + if (EmulatorSettings.IsMotionControlsEnabled()) { + if (SDL_SetGamepadSensorEnabled(c->m_sdl_gamepad, SDL_SENSOR_GYRO, true)) { + c->gyro_poll_rate = + SDL_GetGamepadSensorDataRate(c->m_sdl_gamepad, SDL_SENSOR_GYRO); + LOG_INFO(Input, "Gyro initialized, poll rate: {}", c->gyro_poll_rate); + } else { + LOG_ERROR(Input, "Failed to initialize gyro controls for gamepad {}", + c->user_id); + } + if (SDL_SetGamepadSensorEnabled(c->m_sdl_gamepad, SDL_SENSOR_ACCEL, true)) { + c->accel_poll_rate = + SDL_GetGamepadSensorDataRate(c->m_sdl_gamepad, SDL_SENSOR_ACCEL); + LOG_INFO(Input, "Accel initialized, poll rate: {}", c->accel_poll_rate); + } else { + LOG_ERROR(Input, "Failed to initialize accel controls for gamepad {}", + c->user_id); + } + } + break; + } + } + } + if (is_first_check) [[unlikely]] { + is_first_check = false; + if (controller_count - move_count == 0) { + auto u = UserManagement.GetUserByPlayerIndex(1); + controllers[0]->user_id = u->user_id; + UserManagement.LoginUser(u, 1); + } + } + SDL_free(new_joysticks); +} u8 GameController::GetTouchCount() { return m_touch_count; } @@ -215,73 +367,37 @@ void GameController::SetLastUpdate(std::chrono::steady_clock::time_point lastUpd m_last_update = lastUpdate; } -void GameController::SetEngine(std::unique_ptr engine) { - m_engine = std::move(engine); - if (m_engine) { - m_engine->Init(); - } -} - -Engine* GameController::GetEngine() { - return m_engine.get(); -} - void GameController::PushState() { std::lock_guard lg(m_states_queue_mutex); m_state.time = Libraries::Kernel::sceKernelGetProcessTime(); m_states_queue.Push(m_state); } -u32 GameController::Poll() { - if (m_connected) { - PushState(); - } - return 33; -} - -} // namespace Input - -namespace GamepadSelect { - -int GetDefaultGamepad(SDL_JoystickID* gamepadIDs, int gamepadCount) { - char GUIDbuf[33]; - if (Config::getDefaultControllerID() != "") { - for (int i = 0; i < gamepadCount; i++) { - SDL_GUIDToString(SDL_GetGamepadGUIDForID(gamepadIDs[i]), GUIDbuf, 33); - std::string currentGUID = std::string(GUIDbuf); - if (currentGUID == Config::getDefaultControllerID()) { - return i; - } - } - } - return -1; -} - -int GetIndexfromGUID(SDL_JoystickID* gamepadIDs, int gamepadCount, std::string GUID) { - char GUIDbuf[33]; - for (int i = 0; i < gamepadCount; i++) { - SDL_GUIDToString(SDL_GetGamepadGUIDForID(gamepadIDs[i]), GUIDbuf, 33); - std::string currentGUID = std::string(GUIDbuf); - if (currentGUID == GUID) { +u8 GameControllers::GetGamepadIndexFromJoystickId(SDL_JoystickID id) { + auto g = SDL_GetGamepadFromID(id); + ASSERT(g != nullptr); + for (int i = 0; i < 5; i++) { + if (controllers[i]->m_sdl_gamepad == g) { return i; } } + // LOG_TRACE(Input, "Gamepad index: {}", index); return -1; } -std::string GetGUIDString(SDL_JoystickID* gamepadIDs, int index) { - char GUIDbuf[33]; - SDL_GUIDToString(SDL_GetGamepadGUIDForID(gamepadIDs[index]), GUIDbuf, 33); - std::string GUID = std::string(GUIDbuf); - return GUID; +std::optional GameControllers::GetControllerIndexFromUserID(s32 user_id) { + auto const u = UserManagement.GetUserByID(user_id); + if (!u) { + return std::nullopt; + } + return u->player_index - 1; } -std::string GetSelectedGamepad() { - return SelectedGamepad; +std::optional GameControllers::GetControllerIndexFromControllerID(s32 controller_id) { + if (controller_id < 1 || controller_id > 5) { + return std::nullopt; + } + return controller_id - 1; } -void SetSelectedGamepad(std::string GUID) { - SelectedGamepad = GUID; -} - -} // namespace GamepadSelect +} // namespace Input diff --git a/src/input/controller.h b/src/input/controller.h index 6c13fdf99..1c711c488 100644 --- a/src/input/controller.h +++ b/src/input/controller.h @@ -3,18 +3,25 @@ #pragma once -#include -#include #include +#include #include #include - +#include "SDL3/SDL_joystick.h" +#include "common/assert.h" #include "common/types.h" #include "core/libraries/pad/pad.h" +#include "core/libraries/system/userservice.h" + +struct SDL_Gamepad; namespace Input { +enum class ControllerType { + Standard, +}; + enum class Axis { LeftX = 0, LeftY = 1, @@ -33,37 +40,41 @@ struct TouchpadEntry { u16 y{}; }; -class State { +struct Colour { + u8 r, g, b; +}; + +struct State { +private: + template + using AxisArray = std::array; + static constexpr AxisArray axis_defaults{128, 128, 128, 128, 0, 0}; + static constexpr u64 axis_smoothing_time{33000}; + AxisArray axis_smoothing_flags{true}; + AxisArray axis_smoothing_start_times{0}; + AxisArray axis_smoothing_start_values{axis_defaults}; + AxisArray axis_smoothing_end_values{axis_defaults}; + public: void OnButton(Libraries::Pad::OrbisPadButtonDataOffset, bool); - void OnAxis(Axis, int); + void OnAxis(Axis, int, bool smooth = true); void OnTouchpad(int touchIndex, bool isDown, float x, float y); void OnGyro(const float[3]); void OnAccel(const float[3]); + void UpdateAxisSmoothing(); Libraries::Pad::OrbisPadButtonDataOffset buttonsState{}; u64 time = 0; - int axes[static_cast(Axis::AxisMax)] = {128, 128, 128, 128, 0, 0}; + AxisArray axes{axis_defaults}; TouchpadEntry touchpad[2] = {{false, 0, 0}, {false, 0, 0}}; - Libraries::Pad::OrbisFVector3 acceleration = {0.0f, 0.0f, 0.0f}; + Libraries::Pad::OrbisFVector3 acceleration = {0.0f, -9.81f, 0.0f}; Libraries::Pad::OrbisFVector3 angularVelocity = {0.0f, 0.0f, 0.0f}; Libraries::Pad::OrbisFQuaternion orientation = {0.0f, 0.0f, 0.0f, 1.0f}; }; -class Engine { -public: - virtual ~Engine() = default; - virtual void Init() = 0; - virtual void SetLightBarRGB(u8 r, u8 g, u8 b) = 0; - virtual void SetVibration(u8 smallMotor, u8 largeMotor) = 0; - virtual State ReadState() = 0; - virtual float GetAccelPollRate() const = 0; - virtual float GetGyroPollRate() const = 0; - SDL_Gamepad* m_gamepad; -}; - inline int GetAxis(int min, int max, int value) { - return std::clamp((255 * (value - min)) / (max - min), 0, 255); + int v = (255 * (value - min)) / (max - min); + return (v < 0 ? 0 : (v > 255 ? 255 : v)); } template @@ -98,6 +109,8 @@ private: }; class GameController { + friend class GameControllers; + public: GameController(); virtual ~GameController() = default; @@ -105,16 +118,17 @@ public: void ReadState(State* state, bool* isConnected, int* connectedCount); int ReadStates(State* states, int states_num, bool* isConnected, int* connectedCount); - void Button(int id, Libraries::Pad::OrbisPadButtonDataOffset button, bool isPressed); - void Axis(int id, Input::Axis axis, int value); - void Gyro(int id, const float gyro[3]); - void Acceleration(int id, const float acceleration[3]); + void Button(Libraries::Pad::OrbisPadButtonDataOffset button, bool isPressed); + void Axis(Input::Axis axis, int value, bool smooth = true); + void Gyro(int id); + void Acceleration(int id); + void UpdateGyro(const float gyro[3]); + void UpdateAcceleration(const float acceleration[3]); + void UpdateAxisSmoothing(); void SetLightBarRGB(u8 r, u8 g, u8 b); - void SetVibration(u8 smallMotor, u8 largeMotor); + void PollLightColour(); + bool SetVibration(u8 smallMotor, u8 largeMotor); void SetTouchpadState(int touchIndex, bool touchDown, float x, float y); - void SetEngine(std::unique_ptr); - Engine* GetEngine(); - u32 Poll(); u8 GetTouchCount(); void SetTouchCount(u8 touchCount); @@ -129,11 +143,12 @@ public: Libraries::Pad::OrbisFQuaternion GetLastOrientation(); std::chrono::steady_clock::time_point GetLastUpdate(); void SetLastUpdate(std::chrono::steady_clock::time_point lastUpdate); - static void CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, - Libraries::Pad::OrbisFVector3& angularVelocity, - float deltaTime, - Libraries::Pad::OrbisFQuaternion& lastOrientation, - Libraries::Pad::OrbisFQuaternion& orientation); + + float gyro_poll_rate; + float accel_poll_rate; + float gyro_buf[3] = {0.0f, 0.0f, 0.0f}, accel_buf[3] = {0.0f, 9.81f, 0.0f}; + s32 user_id = Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID; + SDL_Gamepad* m_sdl_gamepad = nullptr; private: void PushState(); @@ -146,22 +161,46 @@ private: bool m_was_secondary_reset = false; std::chrono::steady_clock::time_point m_last_update = {}; Libraries::Pad::OrbisFQuaternion m_orientation = {0.0f, 0.0f, 0.0f, 1.0f}; + Colour colour; State m_state; std::mutex m_states_queue_mutex; RingBufferQueue m_states_queue; +}; - std::unique_ptr m_engine = nullptr; +class GameControllers { + std::array controllers; + + static std::array, 4> controller_override_colors; + +public: + GameControllers() + : controllers({new GameController(), new GameController(), new GameController(), + new GameController(), new GameController()}) {}; + virtual ~GameControllers() = default; + GameController* operator[](const size_t& i) const { + if (i > 4) { + UNREACHABLE_MSG("Index {} is out of bounds for GameControllers!", i); + } + return controllers[i]; + } + void TryOpenSDLControllers(); + u8 GetGamepadIndexFromJoystickId(SDL_JoystickID id); + static std::optional GetControllerIndexFromUserID(s32 user_id); + static std::optional GetControllerIndexFromControllerID(s32 controller_id); + + static void CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, + Libraries::Pad::OrbisFVector3& angularVelocity, + float deltaTime, + Libraries::Pad::OrbisFQuaternion& lastOrientation, + Libraries::Pad::OrbisFQuaternion& orientation); + static void SetControllerCustomColor(s32 i, u8 r, u8 g, u8 b) { + controller_override_colors[i] = {r, g, b}; + } + static std::optional GetControllerCustomColor(s32 i) { + return controller_override_colors[i]; + } }; } // namespace Input - -namespace GamepadSelect { - -int GetIndexfromGUID(SDL_JoystickID* gamepadIDs, int gamepadCount, std::string GUID); -std::string GetGUIDString(SDL_JoystickID* gamepadIDs, int index); -std::string GetSelectedGamepad(); -void SetSelectedGamepad(std::string GUID); - -} // namespace GamepadSelect diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index e44693fbf..cf258235d 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -18,11 +18,12 @@ #include "SDL3/SDL_events.h" #include "SDL3/SDL_timer.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/io_file.h" #include "common/path_util.h" +#include "common/singleton.h" #include "core/devtools/layer.h" +#include "core/emulator_settings.h" #include "core/emulator_state.h" #include "input/controller.h" #include "input/input_mouse.h" @@ -42,78 +43,185 @@ What structs are needed? InputBinding(key1, key2, key3) ControllerOutput(button, axis) - we only need a const array of these, and one of the attr-s is always 0 BindingConnection(inputBinding (member), controllerOutput (ref to the array element)) - -Things to always test before pushing like a dumbass: -Button outputs -Axis outputs -Input hierarchy -Multi key inputs -Mouse to joystick -Key toggle -Joystick halfmode - -Don't be an idiot and test only the changed part expecting everything else to not be broken */ +constexpr std::string_view GetDefaultGlobalConfig() { + return R"(# Anything put here will be loaded for all games, +# alongside the game's config or default.ini depending on your preference. +)"; +} + +constexpr std::string_view GetDefaultInputConfig() { + return R"(#Feeling lost? Check out the Help section! + +# Keyboard bindings + +triangle = kp8 +circle = kp6 +cross = kp2 +square = kp4 +# Alternatives for users without a keypad +triangle = c +circle = b +cross = n +square = v + +l1 = q +r1 = u +l2 = e +r2 = o +l3 = x +r3 = m + +options = enter +touchpad_center = space + +pad_up = up +pad_down = down +pad_left = left +pad_right = right + +axis_left_x_minus = a +axis_left_x_plus = d +axis_left_y_minus = w +axis_left_y_plus = s + +axis_right_x_minus = j +axis_right_x_plus = l +axis_right_y_minus = i +axis_right_y_plus = k + +# Controller bindings + +triangle = triangle +cross = cross +square = square +circle = circle + +l1 = l1 +l2 = l2 +l3 = l3 +r1 = r1 +r2 = r2 +r3 = r3 + +options = options +touchpad_center = back + +pad_up = pad_up +pad_down = pad_down +pad_left = pad_left +pad_right = pad_right + +axis_left_x = axis_left_x +axis_left_y = axis_left_y +axis_right_x = axis_right_x +axis_right_y = axis_right_y + +# Range of deadzones: 1 (almost none) to 127 (max) +analog_deadzone = leftjoystick, 2, 127 +analog_deadzone = rightjoystick, 2, 127 + +override_controller_color = false, 0, 0, 255 +)"; +} +std::filesystem::path GetInputConfigFile(const std::string& game_id) { + // Read configuration file of the game, and if it doesn't exist, generate it from default + // If that doesn't exist either, generate that from getDefaultConfig() and try again + // If even the folder is missing, we start with that. + + const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "input_config"; + const auto config_file = config_dir / (game_id + ".ini"); + const auto default_config_file = config_dir / "default.ini"; + + // Ensure the config directory exists + if (!std::filesystem::exists(config_dir)) { + std::filesystem::create_directories(config_dir); + } + + // Check if the default config exists + if (!std::filesystem::exists(default_config_file)) { + // If the default config is also missing, create it from getDefaultConfig() + const auto default_config = GetDefaultInputConfig(); + std::ofstream default_config_stream(default_config_file); + if (default_config_stream) { + default_config_stream << default_config; + } + } + + // if empty, we only need to execute the function up until this point + if (game_id.empty()) { + return default_config_file; + } + + // Create global config if it doesn't exist yet + if (game_id == "global" && !std::filesystem::exists(config_file)) { + if (!std::filesystem::exists(config_file)) { + const auto global_config = GetDefaultGlobalConfig(); + std::ofstream global_config_stream(config_file); + if (global_config_stream) { + global_config_stream << global_config; + } + } + } + if (game_id == "global") { + std::map default_bindings_to_add = { + {"hotkey_renderdoc_capture", "f12"}, + {"hotkey_fullscreen", "f11"}, + {"hotkey_show_fps", "f10"}, + {"hotkey_pause", "f9"}, + {"hotkey_reload_inputs", "f8"}, + {"hotkey_toggle_mouse_to_joystick", "f7"}, + {"hotkey_toggle_mouse_to_gyro", "f6"}, + {"hotkey_add_virtual_user", "f5"}, + {"hotkey_remove_virtual_user", "f4"}, + {"hotkey_toggle_mouse_to_touchpad", "delete"}, + {"hotkey_quit", "lctrl, lshift, end"}, + {"hotkey_volume_up", "kpplus"}, + {"hotkey_volume_down", "kpminus"}, + }; + std::ifstream global_in(config_file); + std::string line; + while (std::getline(global_in, line)) { + line.erase(std::remove_if(line.begin(), line.end(), + [](unsigned char c) { return std::isspace(c); }), + line.end()); + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) { + continue; + } + std::string output_string = line.substr(0, equal_pos); + default_bindings_to_add.erase(output_string); + } + global_in.close(); + std::ofstream global_out(config_file, std::ios::app); + for (auto const& b : default_bindings_to_add) { + global_out << b.first << " = " << b.second << "\n"; + } + } + + // If game-specific config doesn't exist, create it from the default config + if (!std::filesystem::exists(config_file)) { + std::filesystem::copy(default_config_file, config_file); + } + return config_file; +} + bool leftjoystick_halfmode = false, rightjoystick_halfmode = false; -std::pair leftjoystick_deadzone, rightjoystick_deadzone, lefttrigger_deadzone, - righttrigger_deadzone; +std::array, 4> leftjoystick_deadzone, rightjoystick_deadzone, + lefttrigger_deadzone, righttrigger_deadzone; std::list> pressed_keys; std::list toggled_keys; static std::vector connections; -auto output_array = std::array{ - // Important: these have to be the first, or else they will update in the wrong order - ControllerOutput(LEFTJOYSTICK_HALFMODE), - ControllerOutput(RIGHTJOYSTICK_HALFMODE), - ControllerOutput(KEY_TOGGLE), - ControllerOutput(MOUSE_GYRO_ROLL_MODE), +GameControllers ControllerOutput::controllers = + *Common::Singleton::Instance(); - // Button mappings - ControllerOutput(SDL_GAMEPAD_BUTTON_NORTH), // Triangle - ControllerOutput(SDL_GAMEPAD_BUTTON_EAST), // Circle - ControllerOutput(SDL_GAMEPAD_BUTTON_SOUTH), // Cross - ControllerOutput(SDL_GAMEPAD_BUTTON_WEST), // Square - ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), // L1 - ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_STICK), // L3 - ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), // R1 - ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_STICK), // R3 - ControllerOutput(SDL_GAMEPAD_BUTTON_START), // Options - ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT), // TouchPad - ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER), // TouchPad - ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT), // TouchPad - ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_UP), // Up - ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_DOWN), // Down - ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_LEFT), // Left - ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), // Right - - // Axis mappings - // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX, false), - // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY, false), - // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX, false), - // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY, false), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY), - - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER), - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER), - - ControllerOutput(HOTKEY_FULLSCREEN), - ControllerOutput(HOTKEY_PAUSE), - ControllerOutput(HOTKEY_SIMPLE_FPS), - ControllerOutput(HOTKEY_QUIT), - ControllerOutput(HOTKEY_RELOAD_INPUTS), - ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_JOYSTICK), - ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_GYRO), - ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD), - ControllerOutput(HOTKEY_RENDERDOC), - ControllerOutput(HOTKEY_VOLUME_UP), - ControllerOutput(HOTKEY_VOLUME_DOWN), - - ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID), +std::array output_arrays = { + ControllerAllOutputs(0), ControllerAllOutputs(1), ControllerAllOutputs(2), + ControllerAllOutputs(3), ControllerAllOutputs(4), ControllerAllOutputs(5), + ControllerAllOutputs(6), ControllerAllOutputs(7), ControllerAllOutputs(8), }; void ControllerOutput::LinkJoystickAxes() { @@ -157,6 +265,8 @@ static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) { return OPBDO::TouchPad; case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: return OPBDO::L1; + case SDL_GAMEPAD_BUTTON_MISC1: // Move + return OPBDO::L1; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return OPBDO::R1; case SDL_GAMEPAD_BUTTON_LEFT_STICK: @@ -222,10 +332,19 @@ InputBinding GetBindingFromString(std::string& line) { return InputBinding(keys[0], keys[1], keys[2]); } +std::optional parseInt(const std::string& s) { + try { + return std::stoi(s); + } catch (...) { + return std::nullopt; + } +}; + void ParseInputConfig(const std::string game_id = "") { - std::string game_id_or_default = Config::GetUseUnifiedInputConfig() ? "default" : game_id; - const auto config_file = Config::GetInputConfigFile(game_id_or_default); - const auto global_config_file = Config::GetInputConfigFile("global"); + std::string game_id_or_default = + EmulatorSettings.IsUseUnifiedInputConfig() ? "default" : game_id; + const auto config_file = GetInputConfigFile(game_id_or_default); + const auto global_config_file = GetInputConfigFile("global"); // we reset these here so in case the user fucks up or doesn't include some of these, // we can fall back to default @@ -234,13 +353,14 @@ void ParseInputConfig(const std::string game_id = "") { float mouse_speed = 1; float mouse_speed_offset = 0.125; - leftjoystick_deadzone = {1, 127}; - rightjoystick_deadzone = {1, 127}; - lefttrigger_deadzone = {1, 127}; - righttrigger_deadzone = {1, 127}; + // me when I'm in a type deduction tournament and my opponent is clang + constexpr std::array, 4> default_deadzone = { + std::pair{1, 127}, {1, 127}, {1, 127}, {1, 127}}; - Config::SetOverrideControllerColor(false); - Config::SetControllerCustomColor(0, 0, 255); + leftjoystick_deadzone = default_deadzone; + rightjoystick_deadzone = default_deadzone; + lefttrigger_deadzone = default_deadzone; + righttrigger_deadzone = default_deadzone; int lineCount = 0; @@ -277,21 +397,37 @@ void ParseInputConfig(const std::string game_id = "") { std::string output_string = line.substr(0, equal_pos); std::string input_string = line.substr(equal_pos + 1); - // Remove trailing semicolon from input_string - if (!input_string.empty() && input_string[input_string.length() - 1] == ';' && - input_string != ";") { - line = line.substr(0, line.length() - 1); + s8 input_gamepad_id = -1, output_gamepad_id = -1; // -1 means it's not specified + + // input gamepad id is only for controllers, it's discarded otherwise + std::size_t input_colon_pos = input_string.find(':'); + if (input_colon_pos != std::string::npos) { + auto temp = parseInt(input_string.substr(input_colon_pos + 1)); + if (!temp) { + LOG_WARNING(Input, "Invalid gamepad ID value at line {}: \"{}\"", lineCount, line); + } else { + input_gamepad_id = *temp; + } + input_string = input_string.substr(0, input_colon_pos); + } + + // if not provided, assume it's for all gamepads, if the input is a controller and that also + // doesn't have an ID, and for the first otherwise + std::size_t output_colon_pos = output_string.find(':'); + if (output_colon_pos != std::string::npos) { + auto temp = parseInt(output_string.substr(output_colon_pos + 1)); + if (!temp) { + LOG_WARNING(Input, "Invalid gamepad ID value at line {}: \"{}\"", lineCount, line); + } else { + output_gamepad_id = *temp; + } + output_string = output_string.substr(0, output_colon_pos); } std::size_t comma_pos = input_string.find(','); - auto parseInt = [](const std::string& s) -> std::optional { - try { - return std::stoi(s); - } catch (...) { - return std::nullopt; - } - }; + // todo make override_controller_color and analog_deadzone be controller specific + // instead of global if (output_string == "mouse_to_joystick") { if (input_string == "left") { SetMouseToJoystick(1); @@ -314,7 +450,7 @@ void ParseInputConfig(const std::string game_id = "") { return; } ControllerOutput* toggle_out = - &*std::ranges::find(output_array, ControllerOutput(KEY_TOGGLE)); + &*std::ranges::find(output_arrays[0].data, ControllerOutput(KEY_TOGGLE)); BindingConnection toggle_connection = BindingConnection( InputBinding(toggle_keys.keys[0]), toggle_out, 0, toggle_keys.keys[1]); connections.insert(connections.end(), toggle_connection); @@ -355,15 +491,17 @@ void ParseInputConfig(const std::string game_id = "") { std::pair deadzone = {*inner_deadzone, *outer_deadzone}; - static std::unordered_map&> deadzone_map = { - {"leftjoystick", leftjoystick_deadzone}, - {"rightjoystick", rightjoystick_deadzone}, - {"l2", lefttrigger_deadzone}, - {"r2", righttrigger_deadzone}, - }; + static std::unordered_map, 4>&> + deadzone_map = { + {"leftjoystick", leftjoystick_deadzone}, + {"rightjoystick", rightjoystick_deadzone}, + {"l2", lefttrigger_deadzone}, + {"r2", righttrigger_deadzone}, + }; + output_gamepad_id = output_gamepad_id == -1 ? 1 : output_gamepad_id; if (auto it = deadzone_map.find(device); it != deadzone_map.end()) { - it->second = deadzone; + it->second[output_gamepad_id - 1] = deadzone; LOG_DEBUG(Input, "Parsed deadzone: {} {} {}", device, inner_deadzone_str, outer_deadzone_str); } else { @@ -389,10 +527,12 @@ void ParseInputConfig(const std::string game_id = "") { lineCount, line); return; } - Config::SetOverrideControllerColor(enable == "true"); - Config::SetControllerCustomColor(*r, *g, *b); - LOG_DEBUG(Input, "Parsed color settings: {} {} {} {}", - enable == "true" ? "override" : "no override", *r, *b, *g); + output_gamepad_id = output_gamepad_id == -1 ? 1 : output_gamepad_id; + if (enable == "true") { + GameControllers::SetControllerCustomColor(output_gamepad_id - 1, *r, *g, *b); + } + LOG_DEBUG(Input, "Parsed color settings: {} {} - {} {} {}", + enable == "true" ? "override" : "no override", output_gamepad_id, *r, *b, *g); return; } @@ -409,31 +549,46 @@ void ParseInputConfig(const std::string game_id = "") { auto axis_it = string_to_axis_map.find(output_string); if (button_it != string_to_cbutton_map.end()) { connection = BindingConnection( - binding, &*std::ranges::find(output_array, ControllerOutput(button_it->second))); - connections.insert(connections.end(), connection); + binding, + &*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data, + ControllerOutput(button_it->second))); } else if (hotkey_it != string_to_hotkey_map.end()) { connection = BindingConnection( - binding, &*std::ranges::find(output_array, ControllerOutput(hotkey_it->second))); - connections.insert(connections.end(), connection); + binding, + &*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data, + ControllerOutput(hotkey_it->second))); } else if (axis_it != string_to_axis_map.end()) { int value_to_set = binding.keys[2].type == InputType::Axis ? 0 : axis_it->second.value; connection = BindingConnection( binding, - &*std::ranges::find(output_array, ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, - axis_it->second.axis, - axis_it->second.value >= 0)), + &*std::ranges::find(output_arrays[std::clamp(output_gamepad_id - 1, 0, 3)].data, + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, + axis_it->second.axis, + axis_it->second.value >= 0)), value_to_set); - connections.insert(connections.end(), connection); } else { LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.", lineCount, line); return; } + // if the input binding contains a controller input, and gamepad ID + // isn't specified for either inputs or output (both are -1), then multiply the binding and + // add it to all 4 controllers + if (connection.HasGamepadInput() && input_gamepad_id == -1 && output_gamepad_id == -1) { + for (int i = 0; i < output_arrays.size(); i++) { + BindingConnection copy = connection.CopyWithChangedGamepadId(i + 1); + copy.output = &*std::ranges::find(output_arrays[i].data, *connection.output); + connections.push_back(copy); + } + } else { + connections.push_back(connection); + } LOG_DEBUG(Input, "Succesfully parsed line {}", lineCount); }; while (std::getline(global_config_stream, line)) { ProcessLine(); } + lineCount = 0; while (std::getline(config_stream, line)) { ProcessLine(); } @@ -445,6 +600,16 @@ void ParseInputConfig(const std::string game_id = "") { LOG_DEBUG(Input, "Done parsing the input config!"); } +BindingConnection BindingConnection::CopyWithChangedGamepadId(u8 gamepad) { + BindingConnection copy = *this; + for (auto& key : copy.binding.keys) { + if (key.type == InputType::Controller || key.type == InputType::Axis) { + key.gamepad_id = gamepad; + } + } + return copy; +} + u32 GetMouseWheelEvent(const SDL_Event& event) { if (event.type != SDL_EVENT_MOUSE_WHEEL && event.type != SDL_EVENT_MOUSE_WHEEL_OFF) { LOG_WARNING(Input, "Something went wrong with wheel input parsing!"); @@ -463,6 +628,7 @@ u32 GetMouseWheelEvent(const SDL_Event& event) { } InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) { + u8 gamepad = 1; switch (e.type) { case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: @@ -477,21 +643,17 @@ InputEvent InputBinding::GetInputEventFromSDLEvent(const SDL_Event& e) { e.type == SDL_EVENT_MOUSE_WHEEL, 0); case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: - return InputEvent(InputType::Controller, static_cast(e.gbutton.button), e.gbutton.down, - 0); // clang made me do it + gamepad = ControllerOutput::controllers.GetGamepadIndexFromJoystickId(e.gbutton.which) + 1; + return InputEvent({InputType::Controller, (u32)e.gbutton.button, gamepad}, e.gbutton.down, + 0); case SDL_EVENT_GAMEPAD_AXIS_MOTION: - return InputEvent(InputType::Axis, static_cast(e.gaxis.axis), true, - e.gaxis.value / 256); // this too + gamepad = ControllerOutput::controllers.GetGamepadIndexFromJoystickId(e.gaxis.which) + 1; + return InputEvent({InputType::Axis, (u32)e.gaxis.axis, gamepad}, true, e.gaxis.value / 256); default: return InputEvent(); } } -GameController* ControllerOutput::controller = nullptr; -void ControllerOutput::SetControllerOutputController(GameController* c) { - ControllerOutput::controller = c; -} - void ToggleKeyInList(InputID input) { if (input.type == InputType::Axis) { LOG_ERROR(Input, "Toggling analog inputs is not supported!"); @@ -537,7 +699,7 @@ void ControllerOutput::AddUpdate(InputEvent event) { *new_param = (event.active ? event.axis_value : 0) + *new_param; } } -void ControllerOutput::FinalizeUpdate() { +void ControllerOutput::FinalizeUpdate(u8 gamepad_index) { auto PushSDLEvent = [&](u32 event_type) { if (new_button_state) { SDL_Event e; @@ -552,20 +714,24 @@ void ControllerOutput::FinalizeUpdate() { } old_button_state = new_button_state; old_param = *new_param; - bool is_game_specific = EmulatorState::GetInstance()->IsGameSpecifigConfigUsed(); + GameController* controller; + if (gamepad_index < 5) + controller = controllers[gamepad_index]; + else + UNREACHABLE(); if (button != SDL_GAMEPAD_BUTTON_INVALID) { switch (button) { case SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT: controller->SetTouchpadState(0, new_button_state, 0.25f, 0.5f); - controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(SDLGamepadToOrbisButton(button), new_button_state); break; case SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER: controller->SetTouchpadState(0, new_button_state, 0.50f, 0.5f); - controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(SDLGamepadToOrbisButton(button), new_button_state); break; case SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT: controller->SetTouchpadState(0, new_button_state, 0.75f, 0.5f); - controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(SDLGamepadToOrbisButton(button), new_button_state); break; case LEFTJOYSTICK_HALFMODE: leftjoystick_halfmode = new_button_state; @@ -597,14 +763,20 @@ void ControllerOutput::FinalizeUpdate() { case HOTKEY_RENDERDOC: PushSDLEvent(SDL_EVENT_RDOC_CAPTURE); break; + case HOTKEY_ADD_VIRTUAL_USER: + PushSDLEvent(SDL_EVENT_ADD_VIRTUAL_USER); + break; + case HOTKEY_REMOVE_VIRTUAL_USER: + PushSDLEvent(SDL_EVENT_REMOVE_VIRTUAL_USER); + break; case HOTKEY_VOLUME_UP: - Config::setVolumeSlider(std::clamp(Config::getVolumeSlider() + 10, 0, 500), - is_game_specific); + EmulatorSettings.SetVolumeSlider( + std::clamp(EmulatorSettings.GetVolumeSlider() + 10, 0, 500)); Overlay::ShowVolume(); break; case HOTKEY_VOLUME_DOWN: - Config::setVolumeSlider(std::clamp(Config::getVolumeSlider() - 10, 0, 500), - is_game_specific); + EmulatorSettings.SetVolumeSlider( + std::clamp(EmulatorSettings.GetVolumeSlider() - 10, 0, 500)); Overlay::ShowVolume(); break; case HOTKEY_QUIT: @@ -617,7 +789,7 @@ void ControllerOutput::FinalizeUpdate() { SetMouseGyroRollMode(new_button_state); break; default: // is a normal key (hopefully) - controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(SDLGamepadToOrbisButton(button), new_button_state); break; } } else if (axis != SDL_GAMEPAD_AXIS_INVALID && positive_axis) { @@ -637,28 +809,28 @@ void ControllerOutput::FinalizeUpdate() { switch (c_axis) { case Axis::LeftX: case Axis::LeftY: - ApplyDeadzone(new_param, leftjoystick_deadzone); + ApplyDeadzone(new_param, leftjoystick_deadzone[gamepad_index]); multiplier = leftjoystick_halfmode ? 0.5 : 1.0; break; case Axis::RightX: case Axis::RightY: - ApplyDeadzone(new_param, rightjoystick_deadzone); + ApplyDeadzone(new_param, rightjoystick_deadzone[gamepad_index]); multiplier = rightjoystick_halfmode ? 0.5 : 1.0; break; case Axis::TriggerLeft: - ApplyDeadzone(new_param, lefttrigger_deadzone); - controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param)); - controller->Button(0, OrbisPadButtonDataOffset::L2, *new_param > 0x20); + ApplyDeadzone(new_param, lefttrigger_deadzone[gamepad_index]); + controller->Axis(c_axis, GetAxis(0x0, 0x7f, *new_param)); + controller->Button(OrbisPadButtonDataOffset::L2, *new_param > 0x20); return; case Axis::TriggerRight: - ApplyDeadzone(new_param, righttrigger_deadzone); - controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param)); - controller->Button(0, OrbisPadButtonDataOffset::R2, *new_param > 0x20); + ApplyDeadzone(new_param, righttrigger_deadzone[gamepad_index]); + controller->Axis(c_axis, GetAxis(0x0, 0x7f, *new_param)); + controller->Button(OrbisPadButtonDataOffset::R2, *new_param > 0x20); return; default: break; } - controller->Axis(0, c_axis, GetAxis(-0x80, 0x7f, *new_param * multiplier)); + controller->Axis(c_axis, GetAxis(-0x80, 0x7f, *new_param * multiplier)); } } @@ -673,11 +845,9 @@ bool UpdatePressedKeys(InputEvent event) { if (input.type == InputType::Axis) { // analog input, it gets added when it first sends an event, // and from there, it only changes the parameter - auto it = std::lower_bound(pressed_keys.begin(), pressed_keys.end(), input, - [](const std::pair& e, InputID i) { - return std::tie(e.first.input.type, e.first.input.sdl_id) < - std::tie(i.type, i.sdl_id); - }); + auto it = std::lower_bound( + pressed_keys.begin(), pressed_keys.end(), input, + [](const std::pair& e, InputID i) { return e.first.input < i; }); if (it == pressed_keys.end() || it->first.input != input) { pressed_keys.insert(it, {event, false}); LOG_DEBUG(Input, "Added axis {} to the input list", event.input.sdl_id); @@ -790,25 +960,33 @@ InputEvent BindingConnection::ProcessBinding() { } void ActivateOutputsFromInputs() { - // Reset values and flags - for (auto& it : pressed_keys) { - it.second = false; - } - for (auto& it : output_array) { - it.ResetUpdate(); - } - // Check for input blockers - ApplyMouseInputBlockers(); + // todo find a better solution + for (int i = 0; i < output_arrays.size(); i++) { - // Iterate over all inputs, and update their respecive outputs accordingly - for (auto& it : connections) { - it.output->AddUpdate(it.ProcessBinding()); - } + // Reset values and flags + for (auto& it : pressed_keys) { + it.second = false; + } + for (auto& it : output_arrays[i].data) { + it.ResetUpdate(); + } - // Update all outputs - for (auto& it : output_array) { - it.FinalizeUpdate(); + // Check for input blockers + ApplyMouseInputBlockers(); + + // Iterate over all inputs, and update their respecive outputs accordingly + for (auto& it : connections) { + // only update this when it's the correct pass + if (it.output->gamepad_id == i) { + it.output->AddUpdate(it.ProcessBinding()); + } + } + + // Update all outputs + for (auto& it : output_arrays[i].data) { + it.FinalizeUpdate(i); + } } } diff --git a/src/input/input_handler.h b/src/input/input_handler.h index 844870b5d..ee286aea9 100644 --- a/src/input/input_handler.h +++ b/src/input/input_handler.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "SDL3/SDL_events.h" #include "SDL3/SDL_timer.h" @@ -35,9 +36,11 @@ #define SDL_EVENT_MOUSE_TO_JOYSTICK SDL_EVENT_USER + 6 #define SDL_EVENT_MOUSE_TO_GYRO SDL_EVENT_USER + 7 #define SDL_EVENT_MOUSE_TO_TOUCHPAD SDL_EVENT_USER + 8 -#define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 9 -#define SDL_EVENT_QUIT_DIALOG SDL_EVENT_USER + 10 -#define SDL_EVENT_MOUSE_WHEEL_OFF SDL_EVENT_USER + 11 +#define SDL_EVENT_QUIT_DIALOG SDL_EVENT_USER + 9 +#define SDL_EVENT_MOUSE_WHEEL_OFF SDL_EVENT_USER + 10 +#define SDL_EVENT_ADD_VIRTUAL_USER SDL_EVENT_USER + 11 +#define SDL_EVENT_REMOVE_VIRTUAL_USER SDL_EVENT_USER + 12 +#define SDL_EVENT_RDOC_CAPTURE SDL_EVENT_USER + 13 #define LEFTJOYSTICK_HALFMODE 0x00010000 #define RIGHTJOYSTICK_HALFMODE 0x00020000 @@ -57,6 +60,8 @@ #define HOTKEY_RENDERDOC 0xf0000009 #define HOTKEY_VOLUME_UP 0xf000000a #define HOTKEY_VOLUME_DOWN 0xf000000b +#define HOTKEY_ADD_VIRTUAL_USER 0xf000000c +#define HOTKEY_REMOVE_VIRTUAL_USER 0xf000000d #define SDL_UNMAPPED UINT32_MAX - 1 @@ -77,21 +82,24 @@ class InputID { public: InputType type; u32 sdl_id; - InputID(InputType d = InputType::Count, u32 i = SDL_UNMAPPED) : type(d), sdl_id(i) {} + u8 gamepad_id; + InputID(InputType d = InputType::Count, u32 i = (u32)-1, u8 g = 1) + : type(d), sdl_id(i), gamepad_id(g) {} bool operator==(const InputID& o) const { - return type == o.type && sdl_id == o.sdl_id; + return type == o.type && sdl_id == o.sdl_id && gamepad_id == o.gamepad_id; } bool operator!=(const InputID& o) const { - return type != o.type || sdl_id != o.sdl_id; + return type != o.type || sdl_id != o.sdl_id || gamepad_id != o.gamepad_id; } - bool operator<=(const InputID& o) const { - return type <= o.type && sdl_id <= o.sdl_id; + auto operator<=>(const InputID& o) const { + return std::tie(gamepad_id, type, sdl_id, gamepad_id) <=> + std::tie(o.gamepad_id, o.type, o.sdl_id, o.gamepad_id); } bool IsValid() const { return *this != InputID(); } std::string ToString() { - return fmt::format("({}: {:x})", input_type_names[static_cast(type)], sdl_id); + return fmt::format("({}. {}: {:x})", gamepad_id, input_type_names[(u8)type], sdl_id); } }; @@ -149,6 +157,8 @@ const std::map string_to_hotkey_map = { {"hotkey_toggle_mouse_to_gyro", HOTKEY_TOGGLE_MOUSE_TO_GYRO}, {"hotkey_toggle_mouse_to_touchpad", HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD}, {"hotkey_renderdoc_capture", HOTKEY_RENDERDOC}, + {"hotkey_add_virtual_user", HOTKEY_ADD_VIRTUAL_USER}, + {"hotkey_remove_virtual_user", HOTKEY_REMOVE_VIRTUAL_USER}, {"hotkey_volume_up", HOTKEY_VOLUME_UP}, {"hotkey_volume_down", HOTKEY_VOLUME_DOWN}, }; @@ -401,7 +411,7 @@ public: inline bool IsEmpty() { return !(keys[0].IsValid() || keys[1].IsValid() || keys[2].IsValid()); } - std::string ToString() { // todo add device type + std::string ToString() { switch (KeyCount()) { case 1: return fmt::format("({})", keys[0].ToString()); @@ -420,14 +430,14 @@ public: }; class ControllerOutput { - static GameController* controller; - public: - static void SetControllerOutputController(GameController* c); + static GameControllers controllers; + static void GetGetGamepadIndexFromSDLJoystickID(const SDL_JoystickID id) {} static void LinkJoystickAxes(); u32 button; u32 axis; + u8 gamepad_id; // these are only used as s8, // but I added some padding to avoid overflow if it's activated by multiple inputs // axis_plus and axis_minus pairs share a common new_param, the other outputs have their own @@ -441,6 +451,7 @@ public: new_param = new s16(0); old_param = 0; positive_axis = p; + gamepad_id = 0; } ControllerOutput(const ControllerOutput& o) : button(o.button), axis(o.axis) { new_param = new s16(*o.new_param); @@ -466,7 +477,7 @@ public: void ResetUpdate(); void AddUpdate(InputEvent event); - void FinalizeUpdate(); + void FinalizeUpdate(u8 gamepad_index); }; class BindingConnection { public: @@ -481,6 +492,13 @@ public: output = out; toggle = t; } + BindingConnection& operator=(const BindingConnection& o) { + binding = o.binding; + output = o.output; + axis_param = o.axis_param; + toggle = o.toggle; + return *this; + } bool operator<(const BindingConnection& other) const { // a button is a higher priority than an axis, as buttons can influence axes // (e.g. joystick_halfmode) @@ -494,9 +512,82 @@ public: } return false; } + bool HasGamepadInput() { + for (auto& key : binding.keys) { + if (key.type == InputType::Controller || key.type == InputType::Axis) { + return true; + } + } + return false; + } + BindingConnection CopyWithChangedGamepadId(u8 gamepad); InputEvent ProcessBinding(); }; +class ControllerAllOutputs { +public: + static constexpr u64 output_count = 40; + std::array data = { + // Important: these have to be the first, or else they will update in the wrong order + ControllerOutput(LEFTJOYSTICK_HALFMODE), + ControllerOutput(RIGHTJOYSTICK_HALFMODE), + ControllerOutput(KEY_TOGGLE), + ControllerOutput(MOUSE_GYRO_ROLL_MODE), + + // Button mappings + ControllerOutput(SDL_GAMEPAD_BUTTON_NORTH), // Triangle + ControllerOutput(SDL_GAMEPAD_BUTTON_EAST), // Circle + ControllerOutput(SDL_GAMEPAD_BUTTON_SOUTH), // Cross + ControllerOutput(SDL_GAMEPAD_BUTTON_WEST), // Square + ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), // L1 + ControllerOutput(SDL_GAMEPAD_BUTTON_LEFT_STICK), // L3 + ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), // R1 + ControllerOutput(SDL_GAMEPAD_BUTTON_RIGHT_STICK), // R3 + ControllerOutput(SDL_GAMEPAD_BUTTON_START), // Options + ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT), // TouchPad + ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER), // TouchPad + ControllerOutput(SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT), // TouchPad + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_UP), // Up + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_DOWN), // Down + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_LEFT), // Left + ControllerOutput(SDL_GAMEPAD_BUTTON_DPAD_RIGHT), // Right + + // Axis mappings + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX, false), + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY, false), + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX, false), + // ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY, false), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTX), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFTY), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTX), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHTY), + + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_LEFT_TRIGGER), + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER), + + ControllerOutput(HOTKEY_FULLSCREEN), + ControllerOutput(HOTKEY_PAUSE), + ControllerOutput(HOTKEY_SIMPLE_FPS), + ControllerOutput(HOTKEY_QUIT), + ControllerOutput(HOTKEY_RELOAD_INPUTS), + ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_JOYSTICK), + ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_GYRO), + ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD), + ControllerOutput(HOTKEY_RENDERDOC), + ControllerOutput(HOTKEY_ADD_VIRTUAL_USER), + ControllerOutput(HOTKEY_REMOVE_VIRTUAL_USER), + ControllerOutput(HOTKEY_VOLUME_UP), + ControllerOutput(HOTKEY_VOLUME_DOWN), + + ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID), + }; + ControllerAllOutputs(u8 g) { + for (int i = 0; i < output_count; i++) { + data[i].gamepad_id = g; + } + } +}; + // Updates the list of pressed keys with the given input. // Returns whether the list was updated or not. bool UpdatePressedKeys(InputEvent event); diff --git a/src/input/input_mouse.cpp b/src/input/input_mouse.cpp index 19daab3d6..f90c20484 100644 --- a/src/input/input_mouse.cpp +++ b/src/input/input_mouse.cpp @@ -77,11 +77,11 @@ void EmulateJoystick(GameController* controller, u32 interval) { float a_x = cos(angle) * output_speed, a_y = sin(angle) * output_speed; if (d_x != 0 || d_y != 0) { - controller->Axis(0, axis_x, GetAxis(-0x80, 0x7f, a_x)); - controller->Axis(0, axis_y, GetAxis(-0x80, 0x7f, a_y)); + controller->Axis(axis_x, GetAxis(-0x80, 0x7f, a_x), false); + controller->Axis(axis_y, GetAxis(-0x80, 0x7f, a_y), false); } else { - controller->Axis(0, axis_x, GetAxis(-0x80, 0x7f, 0)); - controller->Axis(0, axis_y, GetAxis(-0x80, 0x7f, 0)); + controller->Axis(axis_x, GetAxis(-0x80, 0x7f, 0), false); + controller->Axis(axis_y, GetAxis(-0x80, 0x7f, 0), false); } } @@ -89,13 +89,13 @@ constexpr float constant_down_accel[3] = {0.0f, 9.81f, 0.0f}; void EmulateGyro(GameController* controller, u32 interval) { float d_x = 0, d_y = 0; SDL_GetRelativeMouseState(&d_x, &d_y); - controller->Acceleration(1, constant_down_accel); + controller->UpdateAcceleration(constant_down_accel); float gyro_from_mouse[3] = {-d_y / 100, -d_x / 100, 0.0f}; if (mouse_gyro_roll_mode) { gyro_from_mouse[1] = 0.0f; gyro_from_mouse[2] = -d_x / 100; } - controller->Gyro(1, gyro_from_mouse); + controller->UpdateGyro(gyro_from_mouse); } void EmulateTouchpad(GameController* controller, u32 interval) { @@ -104,7 +104,7 @@ void EmulateTouchpad(GameController* controller, u32 interval) { controller->SetTouchpadState(0, (mouse_buttons & SDL_BUTTON_LMASK) != 0, std::clamp(x / g_window->GetWidth(), 0.0f, 1.0f), std::clamp(y / g_window->GetHeight(), 0.0f, 1.0f)); - controller->Button(0, Libraries::Pad::OrbisPadButtonDataOffset::TouchPad, + controller->Button(Libraries::Pad::OrbisPadButtonDataOffset::TouchPad, (mouse_buttons & SDL_BUTTON_RMASK) != 0); } diff --git a/src/main.cpp b/src/main.cpp index d3799e2ec..fb9ff3078 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include "common/config.h" #include "common/key_manager.h" @@ -19,10 +20,10 @@ #include "core/file_sys/fs.h" #include "core/ipc/ipc.h" #include "emulator.h" - #ifdef _WIN32 #include #endif +#include int main(int argc, char* argv[]) { #ifdef _WIN32 @@ -33,6 +34,7 @@ int main(int argc, char* argv[]) { auto emu_state = std::make_shared(); EmulatorState::SetInstance(emu_state); + UserSettings.Load(); const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); Config::load(user_dir / "config.toml"); @@ -51,6 +53,11 @@ int main(int argc, char* argv[]) { } } + // Load configurations + std::shared_ptr emu_settings = std::make_shared(); + EmulatorSettingsImpl::SetInstance(emu_settings); + emu_settings->Load(); + CLI::App app{"shadPS4 Emulator CLI"}; // ---- CLI state ---- @@ -120,15 +127,15 @@ int main(int argc, char* argv[]) { // ---- Utility commands ---- if (addGameFolder) { - Config::addGameInstallDir(*addGameFolder); - Config::save(user_dir / "config.toml"); + EmulatorSettings.AddGameInstallDir(*addGameFolder); + EmulatorSettings.Save(); std::cout << "Game folder successfully saved.\n"; return 0; } if (setAddonFolder) { - Config::setAddonInstallDir(*setAddonFolder); - Config::save(user_dir / "config.toml"); + EmulatorSettings.SetAddonInstallDir(*setAddonFolder); + EmulatorSettings.Save(); std::cout << "Addon folder successfully saved.\n"; return 0; } @@ -142,6 +149,14 @@ int main(int argc, char* argv[]) { return 1; } } + if (!gameArgs.empty()) { + if (gameArgs.front() == "--") { + gameArgs.erase(gameArgs.begin()); + } else { + std::cerr << "Error: unhandled flags\n"; + return 1; + } + } // ---- Apply flags ---- if (patchFile) @@ -152,9 +167,9 @@ int main(int argc, char* argv[]) { if (fullscreenStr) { if (*fullscreenStr == "true") { - Config::setIsFullscreen(true); + EmulatorSettings.SetFullScreen(true); } else if (*fullscreenStr == "false") { - Config::setIsFullscreen(false); + EmulatorSettings.SetFullScreen(false); } else { std::cerr << "Error: Invalid argument for --fullscreen (use true|false)\n"; return 1; @@ -162,13 +177,13 @@ int main(int argc, char* argv[]) { } if (showFps) - Config::setShowFpsCounter(true); + EmulatorSettings.SetShowFpsCounter(true); if (configClean) - Config::setConfigMode(Config::ConfigMode::Clean); + EmulatorSettings.SetConfigMode(ConfigMode::Clean); if (configGlobal) - Config::setConfigMode(Config::ConfigMode::Global); + EmulatorSettings.SetConfigMode(ConfigMode::Global); if (logAppend) Common::Log::SetAppend(); @@ -178,7 +193,7 @@ int main(int argc, char* argv[]) { if (!std::filesystem::exists(ebootPath)) { bool found = false; constexpr int maxDepth = 5; - for (const auto& installDir : Config::getGameInstallDirs()) { + for (const auto& installDir : EmulatorSettings.GetGameInstallDirs()) { if (auto foundPath = Common::FS::FindGameByID(installDir, *gamePath, maxDepth)) { ebootPath = *foundPath; found = true; diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index ae4646de6..766a336c2 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -8,12 +8,14 @@ #include "SDL3/SDL_timer.h" #include "SDL3/SDL_video.h" #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "core/debug_state.h" #include "core/devtools/layer.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/time.h" #include "core/libraries/pad/pad.h" +#include "core/libraries/system/userservice.h" +#include "core/user_settings.h" #include "imgui/renderer/imgui_core.h" #include "input/controller.h" #include "input/input_handler.h" @@ -24,10 +26,11 @@ #ifdef __APPLE__ #include "SDL3/SDL_metal.h" #endif +#include -namespace Input { +namespace Frontend { -using Libraries::Pad::OrbisPadButtonDataOffset; +using namespace Libraries::Pad; static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) { using OPBDO = OrbisPadButtonDataOffset; @@ -68,226 +71,33 @@ static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) { } } -static SDL_GamepadAxis InputAxisToSDL(Axis axis) { - switch (axis) { - case Axis::LeftX: - return SDL_GAMEPAD_AXIS_LEFTX; - case Axis::LeftY: - return SDL_GAMEPAD_AXIS_LEFTY; - case Axis::RightX: - return SDL_GAMEPAD_AXIS_RIGHTX; - case Axis::RightY: - return SDL_GAMEPAD_AXIS_RIGHTY; - case Axis::TriggerLeft: - return SDL_GAMEPAD_AXIS_LEFT_TRIGGER; - case Axis::TriggerRight: - return SDL_GAMEPAD_AXIS_RIGHT_TRIGGER; - default: - UNREACHABLE(); - } -} - -SDLInputEngine::~SDLInputEngine() { - if (m_gamepad) { - SDL_CloseGamepad(m_gamepad); - } -} - -void SDLInputEngine::Init() { - if (m_gamepad) { - SDL_CloseGamepad(m_gamepad); - m_gamepad = nullptr; - } - - int gamepad_count; - SDL_JoystickID* gamepads = SDL_GetGamepads(&gamepad_count); - if (!gamepads) { - LOG_ERROR(Input, "Cannot get gamepad list: {}", SDL_GetError()); - return; - } - if (gamepad_count == 0) { - LOG_INFO(Input, "No gamepad found!"); - SDL_free(gamepads); - return; - } - - int selectedIndex = GamepadSelect::GetIndexfromGUID(gamepads, gamepad_count, - GamepadSelect::GetSelectedGamepad()); - int defaultIndex = - GamepadSelect::GetIndexfromGUID(gamepads, gamepad_count, Config::getDefaultControllerID()); - - // If user selects a gamepad in the GUI, use that, otherwise try the default - if (!m_gamepad) { - if (selectedIndex != -1) { - m_gamepad = SDL_OpenGamepad(gamepads[selectedIndex]); - LOG_INFO(Input, "Opening gamepad selected in GUI."); - } else if (defaultIndex != -1) { - m_gamepad = SDL_OpenGamepad(gamepads[defaultIndex]); - LOG_INFO(Input, "Opening default gamepad."); - } else { - m_gamepad = SDL_OpenGamepad(gamepads[0]); - LOG_INFO(Input, "Got {} gamepads. Opening the first one.", gamepad_count); - } - } - - if (!m_gamepad) { - if (!m_gamepad) { - LOG_ERROR(Input, "Failed to open gamepad: {}", SDL_GetError()); - SDL_free(gamepads); - return; - } - } - - SDL_Joystick* joystick = SDL_GetGamepadJoystick(m_gamepad); - Uint16 vendor = SDL_GetJoystickVendor(joystick); - Uint16 product = SDL_GetJoystickProduct(joystick); - - bool isDualSense = (vendor == 0x054C && product == 0x0CE6); - - LOG_INFO(Input, "Gamepad Vendor: {:04X}, Product: {:04X}", vendor, product); - if (isDualSense) { - LOG_INFO(Input, "Detected DualSense Controller"); - } - - if (Config::getIsMotionControlsEnabled()) { - if (SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_GYRO, true)) { - m_gyro_poll_rate = SDL_GetGamepadSensorDataRate(m_gamepad, SDL_SENSOR_GYRO); - LOG_INFO(Input, "Gyro initialized, poll rate: {}", m_gyro_poll_rate); - } else { - LOG_ERROR(Input, "Failed to initialize gyro controls for gamepad, error: {}", - SDL_GetError()); - SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_GYRO, false); - } - if (SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_ACCEL, true)) { - m_accel_poll_rate = SDL_GetGamepadSensorDataRate(m_gamepad, SDL_SENSOR_ACCEL); - LOG_INFO(Input, "Accel initialized, poll rate: {}", m_accel_poll_rate); - } else { - LOG_ERROR(Input, "Failed to initialize accel controls for gamepad, error: {}", - SDL_GetError()); - SDL_SetGamepadSensorEnabled(m_gamepad, SDL_SENSOR_ACCEL, false); - } - } - - SDL_free(gamepads); - - int* rgb = Config::GetControllerCustomColor(); - - if (isDualSense) { - if (SDL_SetJoystickLED(joystick, rgb[0], rgb[1], rgb[2]) == 0) { - LOG_INFO(Input, "Set DualSense LED to R:{} G:{} B:{}", rgb[0], rgb[1], rgb[2]); - } else { - LOG_ERROR(Input, "Failed to set DualSense LED: {}", SDL_GetError()); - } - } else { - SetLightBarRGB(rgb[0], rgb[1], rgb[2]); - } -} - -void SDLInputEngine::SetLightBarRGB(u8 r, u8 g, u8 b) { - if (m_gamepad) { - SDL_SetGamepadLED(m_gamepad, r, g, b); - } -} - -void SDLInputEngine::SetVibration(u8 smallMotor, u8 largeMotor) { - if (m_gamepad) { - const auto low_freq = (smallMotor / 255.0f) * 0xFFFF; - const auto high_freq = (largeMotor / 255.0f) * 0xFFFF; - SDL_RumbleGamepad(m_gamepad, low_freq, high_freq, -1); - } -} - -State SDLInputEngine::ReadState() { - State state{}; - state.time = Libraries::Kernel::sceKernelGetProcessTime(); - - // Buttons - for (u8 i = 0; i < SDL_GAMEPAD_BUTTON_COUNT; ++i) { - auto orbisButton = SDLGamepadToOrbisButton(i); - if (orbisButton == OrbisPadButtonDataOffset::None) { - continue; - } - state.OnButton(orbisButton, SDL_GetGamepadButton(m_gamepad, (SDL_GamepadButton)i)); - } - - // Axes - for (int i = 0; i < static_cast(Axis::AxisMax); ++i) { - const auto axis = static_cast(i); - const auto value = SDL_GetGamepadAxis(m_gamepad, InputAxisToSDL(axis)); - switch (axis) { - case Axis::TriggerLeft: - case Axis::TriggerRight: - state.OnAxis(axis, GetAxis(0, 0x8000, value)); - break; - default: - state.OnAxis(axis, GetAxis(-0x8000, 0x8000, value)); - break; - } - } - - // Touchpad - if (SDL_GetNumGamepadTouchpads(m_gamepad) > 0) { - for (int finger = 0; finger < 2; ++finger) { - bool down; - float x, y; - if (SDL_GetGamepadTouchpadFinger(m_gamepad, 0, finger, &down, &x, &y, NULL)) { - state.OnTouchpad(finger, down, x, y); - } - } - } - - // Gyro - if (SDL_GamepadHasSensor(m_gamepad, SDL_SENSOR_GYRO)) { - float gyro[3]; - if (SDL_GetGamepadSensorData(m_gamepad, SDL_SENSOR_GYRO, gyro, 3)) { - state.OnGyro(gyro); - } - } - - // Accel - if (SDL_GamepadHasSensor(m_gamepad, SDL_SENSOR_ACCEL)) { - float accel[3]; - if (SDL_GetGamepadSensorData(m_gamepad, SDL_SENSOR_ACCEL, accel, 3)) { - state.OnAccel(accel); - } - } - - return state; -} - -float SDLInputEngine::GetGyroPollRate() const { - return m_gyro_poll_rate; -} - -float SDLInputEngine::GetAccelPollRate() const { - return m_accel_poll_rate; -} - -} // namespace Input - -namespace Frontend { - -using namespace Libraries::Pad; - -std::mutex motion_control_mutex; -float gyro_buf[3] = {0.0f, 0.0f, 0.0f}, accel_buf[3] = {0.0f, 9.81f, 0.0f}; -static Uint32 SDLCALL PollGyroAndAccel(void* userdata, SDL_TimerID timer_id, Uint32 interval) { +static Uint32 SDLCALL PollController(void* userdata, SDL_TimerID timer_id, Uint32 interval) { auto* controller = reinterpret_cast(userdata); - std::scoped_lock l{motion_control_mutex}; - controller->Gyro(0, gyro_buf); - controller->Acceleration(0, accel_buf); - return 4; + controller->UpdateAxisSmoothing(); + controller->Gyro(0); + controller->Acceleration(0); + return interval; } -WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_, +static Uint32 SDLCALL PollControllerLightColour(void* userdata, SDL_TimerID timer_id, + Uint32 interval) { + auto* controller = reinterpret_cast(userdata); + controller->PollLightColour(); + return interval; +} + +WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controllers_, std::string_view window_title) - : width{width_}, height{height_}, controller{controller_} { + : width{width_}, height{height_}, controllers{*controllers_} { if (!SDL_SetHint(SDL_HINT_APP_NAME, "shadPS4")) { UNREACHABLE_MSG("Failed to set SDL window hint: {}", SDL_GetError()); } if (!SDL_Init(SDL_INIT_VIDEO)) { UNREACHABLE_MSG("Failed to initialize SDL video subsystem: {}", SDL_GetError()); } + if (!SDL_Init(SDL_INIT_CAMERA)) { + UNREACHABLE_MSG("Failed to initialize SDL camera subsystem: {}", SDL_GetError()); + } SDL_InitSubSystem(SDL_INIT_AUDIO); SDL_PropertiesID props = SDL_CreateProperties(); @@ -320,12 +130,12 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ } if (!error) { SDL_SetWindowFullscreenMode( - window, Config::getFullscreenMode() == "Fullscreen" ? displayMode : NULL); + window, EmulatorSettings.GetFullScreenMode() == "Fullscreen" ? displayMode : NULL); } - SDL_SetWindowFullscreen(window, Config::getIsFullscreen()); + SDL_SetWindowFullscreen(window, EmulatorSettings.IsFullScreen()); + SDL_SyncWindow(window); SDL_InitSubSystem(SDL_INIT_GAMEPAD); - controller->SetEngine(std::make_unique()); #if defined(SDL_PLATFORM_WIN32) window_info.type = WindowSystemType::Windows; @@ -350,11 +160,11 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(window)); #endif // input handler init-s - Input::ControllerOutput::SetControllerOutputController(controller); Input::ControllerOutput::LinkJoystickAxes(); Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); + controllers.TryOpenSDLControllers(); - if (Config::getBackgroundControllerInput()) { + if (EmulatorSettings.IsBackgroundControllerInput()) { SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); } } @@ -394,37 +204,16 @@ void WindowSDL::WaitEvent() { break; case SDL_EVENT_GAMEPAD_ADDED: case SDL_EVENT_GAMEPAD_REMOVED: - controller->SetEngine(std::make_unique()); - break; - case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: - case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: - case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: - controller->SetTouchpadState(event.gtouchpad.finger, - event.type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP, event.gtouchpad.x, - event.gtouchpad.y); + controllers.TryOpenSDLControllers(); break; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: case SDL_EVENT_GAMEPAD_AXIS_MOTION: - OnGamepadEvent(&event); - break; - // i really would have appreciated ANY KIND OF DOCUMENTATION ON THIS - // AND IT DOESN'T EVEN USE PROPER ENUMS + case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: - switch ((SDL_SensorType)event.gsensor.sensor) { - case SDL_SENSOR_GYRO: { - std::scoped_lock l{motion_control_mutex}; - memcpy(gyro_buf, event.gsensor.data, sizeof(gyro_buf)); - break; - } - case SDL_SENSOR_ACCEL: { - std::scoped_lock l{motion_control_mutex}; - memcpy(accel_buf, event.gsensor.data, sizeof(accel_buf)); - break; - } - default: - break; - } + OnGamepadEvent(&event); break; case SDL_EVENT_QUIT: is_open = false; @@ -450,7 +239,7 @@ void WindowSDL::WaitEvent() { } break; case SDL_EVENT_CHANGE_CONTROLLER: - controller->GetEngine()->Init(); + UNREACHABLE_MSG("todo"); break; case SDL_EVENT_TOGGLE_SIMPLE_FPS: Overlay::ToggleSimpleFps(); @@ -471,6 +260,29 @@ void WindowSDL::WaitEvent() { Input::ToggleMouseModeTo(Input::MouseMode::Touchpad)); SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), false); break; + case SDL_EVENT_ADD_VIRTUAL_USER: + for (int i = 0; i < 4; i++) { + if (controllers[i]->user_id == -1) { + auto u = UserManagement.GetUserByPlayerIndex(i + 1); + if (!u) { + break; + } + controllers[i]->user_id = u->user_id; + UserManagement.LoginUser(u, i + 1); + break; + } + } + break; + case SDL_EVENT_REMOVE_VIRTUAL_USER: + LOG_INFO(Input, "Remove user"); + for (int i = 3; i >= 0; i--) { + if (controllers[i]->user_id != -1) { + UserManagement.LogoutUser(UserManagement.GetUserByID(controllers[i]->user_id)); + controllers[i]->user_id = -1; + break; + } + } + break; case SDL_EVENT_RDOC_CAPTURE: VideoCore::TriggerCapture(); break; @@ -480,8 +292,10 @@ void WindowSDL::WaitEvent() { } void WindowSDL::InitTimers() { - SDL_AddTimer(4, &PollGyroAndAccel, controller); - SDL_AddTimer(33, Input::MousePolling, (void*)controller); + for (int i = 0; i < 4; ++i) { + SDL_AddTimer(4, &PollController, controllers[i]); + } + SDL_AddTimer(33, Input::MousePolling, (void*)controllers[0]); } void WindowSDL::RequestKeyboard() { @@ -549,10 +363,44 @@ void WindowSDL::OnGamepadEvent(const SDL_Event* event) { // as it would break the entire touchpad handling // You can still bind other things to it though if (event->gbutton.button == SDL_GAMEPAD_BUTTON_TOUCHPAD) { - controller->Button(0, OrbisPadButtonDataOffset::TouchPad, input_down); + controllers[controllers.GetGamepadIndexFromJoystickId(event->gbutton.which)]->Button( + OrbisPadButtonDataOffset::TouchPad, input_down); return; } + u8 gamepad; + + switch (event->type) { + case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: + switch ((SDL_SensorType)event->gsensor.sensor) { + case SDL_SENSOR_GYRO: + gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which); + if (gamepad < 5) { + controllers[gamepad]->UpdateGyro(event->gsensor.data); + } + break; + case SDL_SENSOR_ACCEL: + gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which); + if (gamepad < 5) { + controllers[gamepad]->UpdateAcceleration(event->gsensor.data); + } + break; + default: + break; + } + return; + case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: + controllers[controllers.GetGamepadIndexFromJoystickId(event->gtouchpad.which)] + ->SetTouchpadState(event->gtouchpad.finger, + event->type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP, event->gtouchpad.x, + event->gtouchpad.y); + return; + default: + break; + } + // add/remove it from the list bool inputs_changed = Input::UpdatePressedKeys(input_event); diff --git a/src/sdl_window.h b/src/sdl_window.h index 3a4341de5..4fc750bbc 100644 --- a/src/sdl_window.h +++ b/src/sdl_window.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -14,23 +14,8 @@ struct SDL_Gamepad; union SDL_Event; namespace Input { - -class SDLInputEngine : public Engine { -public: - ~SDLInputEngine() override; - void Init() override; - void SetLightBarRGB(u8 r, u8 g, u8 b) override; - void SetVibration(u8 smallMotor, u8 largeMotor) override; - float GetGyroPollRate() const override; - float GetAccelPollRate() const override; - State ReadState() override; - -private: - float m_gyro_poll_rate = 0.0f; - float m_accel_poll_rate = 0.0f; -}; - -} // namespace Input +class GameController; +} namespace Frontend { @@ -62,7 +47,7 @@ class WindowSDL { int keyboard_grab = 0; public: - explicit WindowSDL(s32 width, s32 height, Input::GameController* controller, + explicit WindowSDL(s32 width, s32 height, Input::GameControllers* controllers, std::string_view window_title); ~WindowSDL(); @@ -100,7 +85,7 @@ private: private: s32 width; s32 height; - Input::GameController* controller; + Input::GameControllers controllers{}; WindowSystemInfo window_info{}; SDL_Window* window{}; bool is_shown{}; diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp index 6dd1637dd..1055bf081 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp @@ -140,6 +140,15 @@ Id ImageAtomicF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id va const auto [scope, semantics]{AtomicArgs(ctx)}; return (ctx.*atomic_func)(ctx.F32[1], pointer, scope, semantics, value); } + +Id ImageAtomicU32CmpSwap(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value, + Id cmp_value, + Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id, Id, Id)) { + const auto& texture = ctx.images[handle & 0xFFFF]; + const Id pointer{ctx.OpImageTexelPointer(ctx.image_u32, texture.id, coords, ctx.ConstU32(0U))}; + const auto [scope, semantics]{AtomicArgs(ctx)}; + return (ctx.*atomic_func)(ctx.U32[1], pointer, scope, semantics, semantics, value, cmp_value); +} } // Anonymous namespace Id EmitSharedAtomicIAdd32(EmitContext& ctx, Id offset, Id value) { @@ -420,6 +429,12 @@ Id EmitImageAtomicExchange32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id co return ImageAtomicU32(ctx, inst, handle, coords, value, &Sirit::Module::OpAtomicExchange); } +Id EmitImageAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value, + Id cmp_value) { + return ImageAtomicU32CmpSwap(ctx, inst, handle, coords, value, cmp_value, + &Sirit::Module::OpAtomicCompareExchange); +} + Id EmitDataAppend(EmitContext& ctx, u32 gds_addr, u32 binding) { const auto& buffer = ctx.buffers[binding]; const auto [id, pointer_type] = buffer.Alias(PointerType::U32); diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index 554448b13..81f8d08d7 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -1,8 +1,8 @@ -// 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/assert.h" -#include "common/config.h" +#include "core/emulator_settings.h" #include "shader_recompiler/backend/spirv/emit_spirv_bounds.h" #include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" #include "shader_recompiler/backend/spirv/spirv_emit_context.h" @@ -58,7 +58,7 @@ Id EmitGetUserData(EmitContext& ctx, IR::ScalarReg reg) { Id EmitReadConst(EmitContext& ctx, IR::Inst* inst, Id addr, Id offset) { const u32 flatbuf_off_dw = inst->Flags(); - if (!Config::directMemoryAccess()) { + if (!EmulatorSettings.IsDirectMemoryAccessEnabled()) { return ctx.EmitFlatbufferLoad(ctx.ConstU32(flatbuf_off_dw)); } // We can only provide a fallback for immediate offsets. diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp index e2a969b61..0b05dcef4 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp @@ -220,20 +220,33 @@ Id EmitImageGradient(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id Id EmitImageRead(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id lod, Id ms) { const auto& texture = ctx.images[handle & 0xFFFF]; - const Id image = ctx.OpLoad(texture.image_type, texture.id); const Id color_type = texture.data_types->Get(4); ImageOperands operands; operands.Add(spv::ImageOperandsMask::Sample, ms); Id texel; if (!texture.is_storage) { + const Id image = ctx.OpLoad(texture.image_type, texture.id); operands.Add(spv::ImageOperandsMask::Lod, lod); texel = ctx.OpImageFetch(color_type, image, coords, operands.mask, operands.operands); } else { + Id image_ptr = texture.id; if (ctx.profile.supports_image_load_store_lod) { operands.Add(spv::ImageOperandsMask::Lod, lod); } else if (Sirit::ValidId(lod)) { - LOG_WARNING(Render, "Image read with LOD not supported by driver"); +#if 1 + // It's confusing what interactions will cause this code path so leave it as + // unreachable until a case is found. + // Normally IMAGE_LOAD_MIP should translate -> OpImageFetch + UNREACHABLE_MSG("Unsupported ImageRead with Lod"); +#else + LOG_WARNING(Render, "Fallback for ImageRead with LOD"); + ASSERT(texture.mip_fallback_mode == MipStorageFallbackMode::DynamicIndex); + const Id single_image_ptr_type = + ctx.TypePointer(spv::StorageClass::UniformConstant, texture.image_type); + image_ptr = ctx.OpAccessChain(single_image_ptr_type, image_ptr, std::array{lod}); +#endif } + const Id image = ctx.OpLoad(texture.image_type, image_ptr); texel = ctx.OpImageRead(color_type, image, coords, operands.mask, operands.operands); } return texture.is_integer ? ctx.OpBitcast(ctx.F32[4], texel) : texel; @@ -242,15 +255,20 @@ Id EmitImageRead(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id lod void EmitImageWrite(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id lod, Id ms, Id color) { const auto& texture = ctx.images[handle & 0xFFFF]; - const Id image = ctx.OpLoad(texture.image_type, texture.id); + Id image_ptr = texture.id; const Id color_type = texture.data_types->Get(4); ImageOperands operands; operands.Add(spv::ImageOperandsMask::Sample, ms); if (ctx.profile.supports_image_load_store_lod) { operands.Add(spv::ImageOperandsMask::Lod, lod); } else if (Sirit::ValidId(lod)) { - LOG_WARNING(Render, "Image write with LOD not supported by driver"); + LOG_WARNING(Render, "Fallback for ImageWrite with LOD"); + ASSERT(texture.mip_fallback_mode == MipStorageFallbackMode::DynamicIndex); + const Id single_image_ptr_type = + ctx.TypePointer(spv::StorageClass::UniformConstant, texture.image_type); + image_ptr = ctx.OpAccessChain(single_image_ptr_type, image_ptr, std::array{lod}); } + const Id image = ctx.OpLoad(texture.image_type, image_ptr); const Id texel = texture.is_integer ? ctx.OpBitcast(color_type, color) : color; ctx.OpImageWrite(image, coords, texel, operands.mask, operands.operands); } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 80968eaf0..69fa36eaa 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -456,6 +456,8 @@ Id EmitImageAtomicAnd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id EmitImageAtomicOr32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value); Id EmitImageAtomicXor32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value); Id EmitImageAtomicExchange32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value); +Id EmitImageAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id cmp_value); Id EmitCubeFaceIndex(EmitContext& ctx, IR::Inst* inst, Id cube_coords); Id EmitLaneId(EmitContext& ctx); Id EmitWarpId(EmitContext& ctx); diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 261155ab5..c0e469964 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -961,23 +961,33 @@ void EmitContext::DefineImagesAndSamplers() { const auto nfmt = sharp.GetNumberFmt(); const bool is_integer = AmdGpu::IsInteger(nfmt); const bool is_storage = image_desc.is_written; + const MipStorageFallbackMode mip_fallback_mode = image_desc.mip_fallback_mode; const VectorIds& data_types = GetAttributeType(*this, nfmt); const Id sampled_type = data_types[1]; const Id image_type{ImageType(*this, image_desc, sampled_type)}; - const Id pointer_type{TypePointer(spv::StorageClass::UniformConstant, image_type)}; + + const u32 num_bindings = image_desc.NumBindings(info); + Id pointee_type = image_type; + if (mip_fallback_mode == MipStorageFallbackMode::DynamicIndex) { + pointee_type = TypeArray(pointee_type, ConstU32(num_bindings)); + } + + const Id pointer_type{TypePointer(spv::StorageClass::UniformConstant, pointee_type)}; const Id id{AddGlobalVariable(pointer_type, spv::StorageClass::UniformConstant)}; - Decorate(id, spv::Decoration::Binding, binding.unified++); + Decorate(id, spv::Decoration::Binding, binding.unified); + binding.unified += num_bindings; Decorate(id, spv::Decoration::DescriptorSet, 0U); + // TODO better naming for resources (flattened sharp_idx is not informative) Name(id, fmt::format("{}_{}{}", stage, "img", image_desc.sharp_idx)); images.push_back({ .data_types = &data_types, .id = id, .sampled_type = is_storage ? sampled_type : TypeSampledImage(image_type), - .pointer_type = pointer_type, .image_type = image_type, .view_type = sharp.GetViewType(image_desc.is_array), .is_integer = is_integer, .is_storage = is_storage, + .mip_fallback_mode = mip_fallback_mode, }); interfaces.push_back(id); } diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index 9bb2b7d7a..a9c6f0968 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -293,11 +293,11 @@ public: const VectorIds* data_types; Id id; Id sampled_type; - Id pointer_type; Id image_type; AmdGpu::ImageType view_type; bool is_integer = false; bool is_storage = false; + MipStorageFallbackMode mip_fallback_mode{}; }; enum class PointerType : u32 { diff --git a/src/shader_recompiler/frontend/format.cpp b/src/shader_recompiler/frontend/format.cpp index d26873396..4a90fe358 100644 --- a/src/shader_recompiler/frontend/format.cpp +++ b/src/shader_recompiler/frontend/format.cpp @@ -3430,8 +3430,8 @@ constexpr std::array InstructionFormatMIMG = {{ {InstClass::VectorMemImgNoSmp, InstCategory::VectorMemory, 4, 1, ScalarType::Uint32, ScalarType::Uint32}, // 16 = IMAGE_ATOMIC_CMPSWAP - {InstClass::VectorMemImgNoSmp, InstCategory::VectorMemory, 4, 1, ScalarType::Undefined, - ScalarType::Undefined}, + {InstClass::VectorMemImgNoSmp, InstCategory::VectorMemory, 4, 1, ScalarType::Uint32, + ScalarType::Uint32}, // 17 = IMAGE_ATOMIC_ADD {InstClass::VectorMemImgNoSmp, InstCategory::VectorMemory, 4, 1, ScalarType::Uint32, ScalarType::Uint32}, diff --git a/src/shader_recompiler/frontend/translate/data_share.cpp b/src/shader_recompiler/frontend/translate/data_share.cpp index 634486fc4..b1aca83d3 100644 --- a/src/shader_recompiler/frontend/translate/data_share.cpp +++ b/src/shader_recompiler/frontend/translate/data_share.cpp @@ -277,12 +277,21 @@ void Translator::DS_SWIZZLE_B32(const GcnInst& inst) { const u8 offset0 = inst.control.ds.offset0; const u8 offset1 = inst.control.ds.offset1; const IR::U32 src{GetSrc(inst.src[0])}; - // ASSERT(offset1 & 0x80); const IR::U32 lane_id = ir.LaneId(); - const IR::U32 id_in_group = ir.BitwiseAnd(lane_id, ir.Imm32(0b11)); - const IR::U32 base = ir.ShiftLeftLogical(id_in_group, ir.Imm32(1)); - const IR::U32 index = ir.BitFieldExtract(ir.Imm32(offset0), base, ir.Imm32(2)); - SetDst(inst.dst[0], ir.QuadShuffle(src, index)); + if (offset1 & 0x80) { + const IR::U32 id_in_group = ir.BitwiseAnd(lane_id, ir.Imm32(0b11)); + const IR::U32 base = ir.ShiftLeftLogical(id_in_group, ir.Imm32(1)); + const IR::U32 index = ir.BitFieldExtract(ir.Imm32(offset0), base, ir.Imm32(2)); + SetDst(inst.dst[0], ir.QuadShuffle(src, index)); + } else { + const u8 and_mask = (offset0 & 0x1f) | (~u8{0} << 5); + const u8 or_mask = (offset0 >> 5) | ((offset1 & 0x3) << 3); + const u8 xor_mask = offset1 >> 2; + const IR::U32 index = ir.BitwiseXor( + ir.BitwiseOr(ir.BitwiseAnd(lane_id, ir.Imm32(and_mask)), ir.Imm32(or_mask)), + ir.Imm32(xor_mask)); + SetDst(inst.dst[0], ir.ReadLane(src, index)); + } } void Translator::DS_APPEND(const GcnInst& inst) { diff --git a/src/shader_recompiler/frontend/translate/translate.cpp b/src/shader_recompiler/frontend/translate/translate.cpp index 3aa70e2ec..de3822296 100644 --- a/src/shader_recompiler/frontend/translate/translate.cpp +++ b/src/shader_recompiler/frontend/translate/translate.cpp @@ -1,9 +1,9 @@ -// 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/config.h" #include "common/io_file.h" #include "common/path_util.h" +#include "core/emulator_settings.h" #include "shader_recompiler/frontend/decode.h" #include "shader_recompiler/frontend/fetch_shader.h" #include "shader_recompiler/frontend/translate/translate.h" @@ -352,10 +352,10 @@ T Translator::GetSrc(const InstOperand& operand) { } } else { if (operand.input_modifier.abs) { - value = ir.IAbs(value); + value = ir.BitwiseAnd(value, ir.Imm32(0x7FFFFFFFu)); } if (operand.input_modifier.neg) { - value = ir.INeg(value); + value = ir.BitwiseXor(value, ir.Imm32(0x80000000u)); } } return value; @@ -453,6 +453,23 @@ T Translator::GetSrc64(const InstOperand& operand) { if (operand.input_modifier.neg) { value = ir.FPNeg(value); } + } else { + // GCN VOP3 abs/neg modifier bits operate on the sign bit (bit 63 for + // 64-bit values). Unpack, modify the high dword's bit 31, repack. + if (operand.input_modifier.abs) { + const auto unpacked = ir.UnpackUint2x32(value); + const auto lo = IR::U32{ir.CompositeExtract(unpacked, 0)}; + const auto hi = IR::U32{ir.CompositeExtract(unpacked, 1)}; + const auto hi_abs = ir.BitwiseAnd(hi, ir.Imm32(0x7FFFFFFFu)); + value = ir.PackUint2x32(ir.CompositeConstruct(lo, hi_abs)); + } + if (operand.input_modifier.neg) { + const auto unpacked = ir.UnpackUint2x32(value); + const auto lo = IR::U32{ir.CompositeExtract(unpacked, 0)}; + const auto hi = IR::U32{ir.CompositeExtract(unpacked, 1)}; + const auto hi_neg = ir.BitwiseXor(hi, ir.Imm32(0x80000000u)); + value = ir.PackUint2x32(ir.CompositeConstruct(lo, hi_neg)); + } } return value; } @@ -552,7 +569,7 @@ void Translator::EmitFetch(const GcnInst& inst) { const auto fetch_data = ParseFetchShader(info); ASSERT(fetch_data.has_value()); - if (Config::dumpShaders()) { + if (EmulatorSettings.IsDumpShaders()) { using namespace Common::FS; const auto dump_dir = GetUserPath(PathType::ShaderDir) / "dumps"; if (!std::filesystem::exists(dump_dir)) { diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 08b0192f5..5ee75e336 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -153,6 +153,7 @@ public: void V_SUB_F32(const GcnInst& inst); void V_SUBREV_F32(const GcnInst& inst); void V_MUL_F32(const GcnInst& inst); + void V_MUL_LEGACY_F32(const GcnInst& inst); void V_MUL_I32_I24(const GcnInst& inst, bool is_signed); void V_MIN_F32(const GcnInst& inst, bool is_legacy = false); void V_MAX_F32(const GcnInst& inst, bool is_legacy = false); diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index 08a0f6527..23236b702 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -25,7 +25,7 @@ void Translator::EmitVectorAlu(const GcnInst& inst) { case Opcode::V_MAC_LEGACY_F32: return V_MAC_F32(inst); case Opcode::V_MUL_LEGACY_F32: - return V_MUL_F32(inst); + return V_MUL_LEGACY_F32(inst); case Opcode::V_MUL_F32: return V_MUL_F32(inst); case Opcode::V_MUL_I32_I24: @@ -493,6 +493,19 @@ void Translator::V_MUL_F32(const GcnInst& inst) { SetDst(inst.dst[0], ir.FPMul(GetSrc(inst.src[0]), GetSrc(inst.src[1]))); } +void Translator::V_MUL_LEGACY_F32(const GcnInst& inst) { + // GCN V_MUL_LEGACY_F32: if either source is zero, the result is +0.0 + // regardless of the other operand (even if NaN or Inf). + // Standard IEEE multiply would produce NaN for 0 * Inf. + const IR::F32 src0{GetSrc(inst.src[0])}; + const IR::F32 src1{GetSrc(inst.src[1])}; + const IR::F32 zero{ir.Imm32(0.0f)}; + const IR::U1 src0_zero{ir.FPEqual(src0, zero)}; + const IR::U1 src1_zero{ir.FPEqual(src1, zero)}; + const IR::U1 either_zero{ir.LogicalOr(src0_zero, src1_zero)}; + SetDst(inst.dst[0], IR::F32{ir.Select(either_zero, zero, ir.FPMul(src0, src1))}); +} + void Translator::V_MUL_I32_I24(const GcnInst& inst, bool is_signed) { const IR::U32 src0{ ir.BitFieldExtract(GetSrc(inst.src[0]), ir.Imm32(0), ir.Imm32(24), is_signed)}; diff --git a/src/shader_recompiler/frontend/translate/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index 72286b29c..0d9e8f220 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -137,6 +137,8 @@ void Translator::EmitVectorMemory(const GcnInst& inst) { // Image atomic operations case Opcode::IMAGE_ATOMIC_SWAP: return IMAGE_ATOMIC(AtomicOp::Swap, inst); + case Opcode::IMAGE_ATOMIC_CMPSWAP: + return IMAGE_ATOMIC(AtomicOp::CmpSwap, inst); case Opcode::IMAGE_ATOMIC_ADD: return IMAGE_ATOMIC(AtomicOp::Add, inst); case Opcode::IMAGE_ATOMIC_SMIN: @@ -520,6 +522,10 @@ void Translator::IMAGE_ATOMIC(AtomicOp op, const GcnInst& inst) { switch (op) { case AtomicOp::Swap: return ir.ImageAtomicExchange(handle, body, value, {}); + case AtomicOp::CmpSwap: { + const IR::Value cmp_val = ir.GetVectorReg(val_reg + 1); + return ir.ImageAtomicCmpSwap(handle, body, value, cmp_val, info); + } case AtomicOp::Add: return ir.ImageAtomicIAdd(handle, body, value, info); case AtomicOp::Smin: diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index 1e77dc677..c681c3120 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -2055,6 +2055,11 @@ Value IREmitter::ImageAtomicExchange(const Value& handle, const Value& coords, c return Inst(Opcode::ImageAtomicExchange32, Flags{info}, handle, coords, value); } +Value IREmitter::ImageAtomicCmpSwap(const Value& handle, const Value& coords, const Value& value, + const Value& cmp_value, TextureInstInfo info) { + return Inst(Opcode::ImageAtomicCmpSwap32, Flags{info}, handle, coords, value, cmp_value); +} + Value IREmitter::ImageSampleRaw(const Value& image_handle, const Value& sampler_handle, const Value& address1, const Value& address2, const Value& address3, const Value& address4, TextureInstInfo info) { diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index 6f20d5780..adc8f5fb1 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -360,6 +360,9 @@ public: TextureInstInfo info); [[nodiscard]] Value ImageAtomicExchange(const Value& handle, const Value& coords, const Value& value, TextureInstInfo info); + [[nodiscard]] Value ImageAtomicCmpSwap(const Value& handle, const Value& coords, + const Value& value, const Value& cmp_value, + TextureInstInfo info); [[nodiscard]] Value ImageSampleRaw(const Value& image_handle, const Value& sampler_handle, const Value& address1, const Value& address2, diff --git a/src/shader_recompiler/ir/microinstruction.cpp b/src/shader_recompiler/ir/microinstruction.cpp index 40ce69df8..cd0131770 100644 --- a/src/shader_recompiler/ir/microinstruction.cpp +++ b/src/shader_recompiler/ir/microinstruction.cpp @@ -123,6 +123,7 @@ bool Inst::MayHaveSideEffects() const noexcept { case Opcode::ImageAtomicOr32: case Opcode::ImageAtomicXor32: case Opcode::ImageAtomicExchange32: + case Opcode::ImageAtomicCmpSwap32: case Opcode::DebugPrint: case Opcode::EmitVertex: case Opcode::EmitPrimitive: diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index 4875375bc..6304a96fa 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -436,6 +436,7 @@ OPCODE(ImageAtomicAnd32, U32, Opaq OPCODE(ImageAtomicOr32, U32, Opaque, Opaque, U32, ) OPCODE(ImageAtomicXor32, U32, Opaque, Opaque, U32, ) OPCODE(ImageAtomicExchange32, U32, Opaque, Opaque, U32, ) +OPCODE(ImageAtomicCmpSwap32, U32, Opaque, Opaque, U32, U32, ) // Cube operations - optional, usable if profile.supports_native_cube_calc OPCODE(CubeFaceIndex, F32, F32x3, ) diff --git a/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp b/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp index e1f9f2c5a..a23ee2319 100644 --- a/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp +++ b/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp @@ -1,16 +1,15 @@ - -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include -#include "common/config.h" #include "common/io_file.h" #include "common/logging/log.h" #include "common/path_util.h" #include "common/signal_context.h" +#include "core/emulator_settings.h" #include "core/signals.h" #include "shader_recompiler/info.h" #include "shader_recompiler/ir/breadth_first_search.h" @@ -229,7 +228,7 @@ static void GenerateSrtProgram(Info& info, PassInfo& pass_info) { info.srt_info.walker_func_size = c.getCurr() - reinterpret_cast(info.srt_info.walker_func); - if (Config::dumpShaders()) { + if (EmulatorSettings.IsDumpShaders()) { DumpSrtProgram(info, reinterpret_cast(info.srt_info.walker_func), info.srt_info.walker_func_size); } diff --git a/src/shader_recompiler/ir/passes/ir_passes.h b/src/shader_recompiler/ir/passes/ir_passes.h index f103b6736..1b14a1c6b 100644 --- a/src/shader_recompiler/ir/passes/ir_passes.h +++ b/src/shader_recompiler/ir/passes/ir_passes.h @@ -19,7 +19,7 @@ void DeadCodeEliminationPass(IR::Program& program); void ConstantPropagationPass(IR::BlockList& program); void FlattenExtendedUserdataPass(IR::Program& program); void ReadLaneEliminationPass(IR::Program& program); -void ResourceTrackingPass(IR::Program& program); +void ResourceTrackingPass(IR::Program& program, const Profile& profile); void CollectShaderInfoPass(IR::Program& program, const Profile& profile); void LowerBufferFormatToRaw(IR::Program& program); void LowerFp64ToFp32(IR::Program& program); diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 93129ac0e..3b7888ab3 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -9,6 +9,7 @@ #include "shader_recompiler/ir/operand_helper.h" #include "shader_recompiler/ir/program.h" #include "shader_recompiler/ir/reinterpret.h" +#include "shader_recompiler/profile.h" #include "video_core/amdgpu/resource.h" namespace Shader::Optimization { @@ -214,6 +215,7 @@ bool IsImageAtomicInstruction(const IR::Inst& inst) { case IR::Opcode::ImageAtomicOr32: case IR::Opcode::ImageAtomicXor32: case IR::Opcode::ImageAtomicExchange32: + case IR::Opcode::ImageAtomicCmpSwap32: return true; default: return false; @@ -254,7 +256,9 @@ public: u32 Add(const ImageResource& desc) { const u32 index{Add(image_resources, desc, [&desc](const auto& existing) { - return desc.sharp_idx == existing.sharp_idx && desc.is_array == existing.is_array; + return desc.sharp_idx == existing.sharp_idx && desc.is_array == existing.is_array && + desc.mip_fallback_mode == existing.mip_fallback_mode && + desc.constant_mip_index == existing.constant_mip_index; })}; auto& image = image_resources[index]; image.is_atomic |= desc.is_atomic; @@ -528,14 +532,21 @@ void PatchBufferSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& inst.SetArg(0, ir.Imm32(buffer_binding)); } -void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& descriptors) { +void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& descriptors, + const Profile& profile) { // Read image sharp. const auto inst_info = inst.Flags(); const IR::Inst* image_handle = inst.Arg(0).InstRecursive(); const auto tsharp = TrackSharp(image_handle, block, inst_info.pc); const bool is_atomic = IsImageAtomicInstruction(inst); const bool is_written = inst.GetOpcode() == IR::Opcode::ImageWrite || is_atomic; - const ImageResource image_res = { + const bool is_storage = + inst.GetOpcode() == IR::Opcode::ImageRead || inst.GetOpcode() == IR::Opcode::ImageWrite; + // ImageRead with !is_written gets emitted as OpImageFetch with LOD operand, doesn't + // need fallback (TODO is this 100% true?) + const bool needs_mip_storage_fallback = + inst_info.has_lod && is_written && !profile.supports_image_load_store_lod; + ImageResource image_res = { .sharp_idx = tsharp, .is_depth = bool(inst_info.is_depth), .is_atomic = is_atomic, @@ -543,9 +554,42 @@ void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& .is_written = is_written, .is_r128 = bool(inst_info.is_r128), }; + auto image = image_res.GetSharp(info); ASSERT(image.GetType() != AmdGpu::ImageType::Invalid); + if (needs_mip_storage_fallback) { + // If the mip level to IMAGE_(LOAD/STORE)_MIP is a constant, set up ImageResource + // so that we will only bind a single level. + // If index is dynamic, we will bind levels as an array + const auto view_type = image.GetViewType(image_res.is_array); + + IR::Inst* body = inst.Arg(1).InstRecursive(); + const auto lod_arg = [&] -> IR::Value { + switch (view_type) { + case AmdGpu::ImageType::Color1D: // x, [lod] + return body->Arg(1); + case AmdGpu::ImageType::Color1DArray: // x, slice, [lod] + case AmdGpu::ImageType::Color2D: // x, y, [lod] + return body->Arg(2); + case AmdGpu::ImageType::Color2DArray: // x, y, slice, [lod] + case AmdGpu::ImageType::Color3D: // x, y, z, [lod] + return body->Arg(3); + case AmdGpu::ImageType::Color2DMsaa: + case AmdGpu::ImageType::Color2DMsaaArray: + default: + UNREACHABLE_MSG("Invalid image type {}", view_type); + } + }(); + + if (lod_arg.IsImmediate()) { + image_res.mip_fallback_mode = MipStorageFallbackMode::ConstantIndex; + image_res.constant_mip_index = lod_arg.U32(); + } else { + image_res.mip_fallback_mode = MipStorageFallbackMode::DynamicIndex; + } + } + // Patch image instruction if image is FMask. if (AmdGpu::IsFmask(image.GetDataFmt())) { ASSERT_MSG(!is_written, "FMask storage instructions are not supported"); @@ -1079,7 +1123,11 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) { const auto has_ms = view_type == AmdGpu::ImageType::Color2DMsaa || view_type == AmdGpu::ImageType::Color2DMsaaArray; ASSERT(!inst_info.has_lod || !has_ms); - const auto lod = inst_info.has_lod ? IR::U32{arg} : IR::U32{}; + // If we are binding a single mip level as fallback, drop the argument + const auto lod = + (inst_info.has_lod && image_res.mip_fallback_mode != MipStorageFallbackMode::ConstantIndex) + ? IR::U32{arg} + : IR::U32{}; const auto ms = has_ms ? IR::U32{arg} : IR::U32{}; const auto is_storage = image_res.is_written; @@ -1110,7 +1158,7 @@ void PatchImageArgs(IR::Block& block, IR::Inst& inst, Info& info) { } } -void ResourceTrackingPass(IR::Program& program) { +void ResourceTrackingPass(IR::Program& program, const Profile& profile) { // Iterate resource instructions and patch them after finding the sharp. auto& info = program.info; @@ -1121,7 +1169,7 @@ void ResourceTrackingPass(IR::Program& program) { if (IsBufferInstruction(inst)) { PatchBufferSharp(*block, inst, info, descriptors); } else if (IsImageInstruction(inst)) { - PatchImageSharp(*block, inst, info, descriptors); + PatchImageSharp(*block, inst, info, descriptors, profile); } } } diff --git a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp index c298a1092..e26d3f078 100644 --- a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp +++ b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp @@ -1,7 +1,7 @@ -// 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/config.h" +#include "core/emulator_settings.h" #include "shader_recompiler/ir/program.h" #include "shader_recompiler/profile.h" #include "video_core/buffer_cache/buffer_cache.h" @@ -176,7 +176,7 @@ void CollectShaderInfoPass(IR::Program& program, const Profile& profile) { // info.readconst_types |= Info::ReadConstType::Immediate; } - if (!Config::directMemoryAccess()) { + if (!EmulatorSettings.IsDirectMemoryAccessEnabled()) { info.uses_dma = false; info.readconst_types = Info::ReadConstType::None; } diff --git a/src/shader_recompiler/ir/program.cpp b/src/shader_recompiler/ir/program.cpp index 1d03ea9ab..926f8f29d 100644 --- a/src/shader_recompiler/ir/program.cpp +++ b/src/shader_recompiler/ir/program.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -6,9 +7,9 @@ #include -#include "common/config.h" #include "common/io_file.h" #include "common/path_util.h" +#include "core/emulator_settings.h" #include "shader_recompiler/ir/basic_block.h" #include "shader_recompiler/ir/program.h" #include "shader_recompiler/ir/value.h" @@ -18,7 +19,7 @@ namespace Shader::IR { void DumpProgram(const Program& program, const Info& info, const std::string& type) { using namespace Common::FS; - if (!Config::dumpShaders()) { + if (!EmulatorSettings.IsDumpShaders()) { return; } diff --git a/src/shader_recompiler/recompiler.cpp b/src/shader_recompiler/recompiler.cpp index f4fa45afc..d6efa2890 100644 --- a/src/shader_recompiler/recompiler.cpp +++ b/src/shader_recompiler/recompiler.cpp @@ -80,7 +80,7 @@ IR::Program TranslateProgram(const std::span& code, Pools& pools, Inf Shader::Optimization::RingAccessElimination(program, runtime_info); Shader::Optimization::ReadLaneEliminationPass(program); Shader::Optimization::FlattenExtendedUserdataPass(program); - Shader::Optimization::ResourceTrackingPass(program); + Shader::Optimization::ResourceTrackingPass(program, profile); Shader::Optimization::LowerBufferFormatToRaw(program); Shader::Optimization::SharedMemorySimplifyPass(program, profile); Shader::Optimization::SharedMemoryToStoragePass(program, runtime_info, profile); diff --git a/src/shader_recompiler/resource.h b/src/shader_recompiler/resource.h index 5ae3179f6..82a861e2a 100644 --- a/src/shader_recompiler/resource.h +++ b/src/shader_recompiler/resource.h @@ -71,6 +71,8 @@ struct BufferResource { }; using BufferResourceList = boost::container::static_vector; +enum class MipStorageFallbackMode : u32 { None, DynamicIndex, ConstantIndex }; + struct ImageResource { u32 sharp_idx; bool is_depth{}; @@ -78,6 +80,8 @@ struct ImageResource { bool is_array{}; bool is_written{}; bool is_r128{}; + MipStorageFallbackMode mip_fallback_mode{}; + u32 constant_mip_index{}; constexpr AmdGpu::Image GetSharp(const auto& info) const noexcept { AmdGpu::Image image{}; @@ -102,6 +106,13 @@ struct ImageResource { } return image; } + + u32 NumBindings(const auto& info) const { + const AmdGpu::Image tsharp = GetSharp(info); + return (mip_fallback_mode == MipStorageFallbackMode::DynamicIndex) + ? (tsharp.last_level - tsharp.base_level + 1) + : 1; + } }; using ImageResourceList = boost::container::static_vector; diff --git a/src/shader_recompiler/specialization.h b/src/shader_recompiler/specialization.h index 4f6bb44bf..fa14583af 100644 --- a/src/shader_recompiler/specialization.h +++ b/src/shader_recompiler/specialization.h @@ -52,6 +52,8 @@ struct ImageSpecialization { bool is_srgb = false; AmdGpu::CompMapping dst_select{}; AmdGpu::NumberConversion num_conversion{}; + // FIXME any pipeline cache changes needed? + u32 num_bindings = 0; bool operator==(const ImageSpecialization&) const = default; }; @@ -133,7 +135,7 @@ struct StageSpecialization { } }); ForEachSharp(binding, images, info->images, - [](auto& spec, const auto& desc, AmdGpu::Image sharp) { + [&](auto& spec, const auto& desc, AmdGpu::Image sharp) { spec.type = sharp.GetViewType(desc.is_array); spec.is_integer = AmdGpu::IsInteger(sharp.GetNumberFmt()); spec.is_storage = desc.is_written; @@ -144,6 +146,7 @@ struct StageSpecialization { spec.is_srgb = sharp.GetNumberFmt() == AmdGpu::NumberFormat::Srgb; } spec.num_conversion = sharp.GetNumberConversion(); + spec.num_bindings = desc.NumBindings(*info); }); ForEachSharp(binding, fmasks, info->fmasks, [](auto& spec, const auto& desc, AmdGpu::Image sharp) { diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index c4f1d6695..0648df922 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -1,14 +1,14 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/polyfill_thread.h" #include "common/thread.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/process.h" #include "core/libraries/videoout/driver.h" #include "core/memory.h" @@ -229,8 +229,8 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(dcb.data()); while (!dcb.empty()) { @@ -899,7 +899,7 @@ template Liverpool::Task Liverpool::ProcessCompute(std::span acb, u32 vqid) { FIBER_ENTER(acb_task_name[vqid]); auto& queue = asc_queues[{vqid}]; - const bool host_markers_enabled = rasterizer && Config::getVkHostMarkersEnabled(); + const bool host_markers_enabled = rasterizer && EmulatorSettings.IsVkHostMarkersEnabled(); struct IndirectPatch { const PM4Header* header; @@ -1202,7 +1202,7 @@ Liverpool::CmdBuffer Liverpool::CopyCmdBuffers(std::span dcb, std::sp void Liverpool::SubmitGfx(std::span dcb, std::span ccb) { auto& queue = mapped_queues[GfxQueueId]; - if (Config::copyGPUCmdBuffers()) { + if (EmulatorSettings.IsCopyGpuBuffers()) { std::tie(dcb, ccb) = CopyCmdBuffers(dcb, ccb); } diff --git a/src/video_core/buffer_cache/memory_tracker.h b/src/video_core/buffer_cache/memory_tracker.h index 2ec86de35..a093be8dd 100644 --- a/src/video_core/buffer_cache/memory_tracker.h +++ b/src/video_core/buffer_cache/memory_tracker.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -11,6 +11,7 @@ #include "common/debug.h" #include "common/types.h" +#include "core/emulator_settings.h" #include "video_core/buffer_cache/region_manager.h" namespace VideoCore { @@ -73,7 +74,7 @@ public: // modified. If we need to flush the flush function is going to perform CPU // state change. std::scoped_lock lk{manager->lock}; - if (Config::getReadbacksMode() != Config::GpuReadbacksMode::Disabled && + if (EmulatorSettings.GetReadbacksMode() != GpuReadbacksMode::Disabled && manager->template IsRegionModified(offset, size)) { return true; } diff --git a/src/video_core/buffer_cache/region_manager.h b/src/video_core/buffer_cache/region_manager.h index ecf9406af..a760dd596 100644 --- a/src/video_core/buffer_cache/region_manager.h +++ b/src/video_core/buffer_cache/region_manager.h @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "common/config.h" #include "common/div_ceil.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #ifdef __linux__ #include "common/adaptive_mutex.h" @@ -95,7 +95,7 @@ public: } if constexpr (type == Type::CPU) { UpdateProtection(); - } else if (Config::getReadbacksMode() == Config::GpuReadbacksMode::Precise) { + } else if (EmulatorSettings.GetReadbacksMode() == GpuReadbacksMode::Precise) { UpdateProtection(); } } @@ -126,7 +126,7 @@ public: bits.UnsetRange(start_page, end_page); if constexpr (type == Type::CPU) { UpdateProtection(); - } else if (Config::getReadbacksMode() != Config::GpuReadbacksMode::Disabled) { + } else if (EmulatorSettings.GetReadbacksMode() != GpuReadbacksMode::Disabled) { UpdateProtection(); } } diff --git a/src/video_core/cache_storage.cpp b/src/video_core/cache_storage.cpp index 1c46a4cf5..8d6abf9b5 100644 --- a/src/video_core/cache_storage.cpp +++ b/src/video_core/cache_storage.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/elf_info.h" #include "common/io_file.h" #include "common/polyfill_thread.h" #include "common/thread.h" +#include "core/emulator_settings.h" #include "video_core/cache_storage.h" #include "video_core/renderer_vulkan/vk_instance.h" @@ -95,7 +95,7 @@ void DataBase::Open() { const auto& game_info = Common::ElfInfo::Instance(); using namespace Common::FS; - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { mz_zip_zero_struct(&zip_ar); cache_path = GetUserPath(PathType::CacheDir) / @@ -128,7 +128,7 @@ void DataBase::Close() { io_worker.request_stop(); io_worker.join(); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { mz_zip_writer_finalize_archive(&zip_ar); mz_zip_writer_end(&zip_ar); } @@ -142,7 +142,7 @@ bool WriteVector(const BlobType type, std::filesystem::path&& path_, std::vector auto request = std::packaged_task{[=]() { auto path{path_}; path.replace_extension(GetBlobFileExtension(type)); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { ASSERT_MSG(!ar_is_read_only, "The archive is read-only. Did you forget to call `FinishPreload`?"); if (!mz_zip_writer_add_mem(&zip_ar, path.string().c_str(), v.data(), @@ -169,7 +169,7 @@ template void LoadVector(BlobType type, std::filesystem::path& path, std::vector& v) { using namespace Common::FS; path.replace_extension(GetBlobFileExtension(type)); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { int index{-1}; index = mz_zip_reader_locate_file(&zip_ar, path.string().c_str(), nullptr, 0); if (index < 0) { @@ -192,7 +192,8 @@ bool DataBase::Save(BlobType type, const std::string& name, std::vector&& da return false; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return WriteVector(type, std::move(path), std::move(data)); } @@ -201,7 +202,8 @@ bool DataBase::Save(BlobType type, const std::string& name, std::vector&& d return false; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return WriteVector(type, std::move(path), std::move(data)); } @@ -210,7 +212,8 @@ void DataBase::Load(BlobType type, const std::string& name, std::vector& dat return; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return LoadVector(type, path, data); } @@ -219,13 +222,14 @@ void DataBase::Load(BlobType type, const std::string& name, std::vector& da return; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return LoadVector(type, path, data); } void DataBase::ForEachBlob(BlobType type, const std::function&& data)>& func) { const auto& ext = GetBlobFileExtension(type); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { const auto num_files = mz_zip_reader_get_num_files(&zip_ar); for (int index = 0; index < num_files; ++index) { std::array file_name{}; @@ -255,7 +259,7 @@ void DataBase::ForEachBlob(BlobType type, const std::function @@ -31,7 +31,7 @@ void LoadRenderDoc() { // Check if we are running by RDoc GUI HMODULE mod = GetModuleHandleA("renderdoc.dll"); - if (!mod && Config::isRdocEnabled()) { + if (!mod && EmulatorSettings.IsRenderdocEnabled()) { // If enabled in config, try to load RDoc runtime in offline mode HKEY h_reg_key; LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, @@ -67,7 +67,7 @@ void LoadRenderDoc() { #endif // Check if we are running by RDoc GUI void* mod = dlopen(RENDERDOC_LIB, RTLD_NOW | RTLD_NOLOAD); - if (!mod && Config::isRdocEnabled()) { + if (!mod && EmulatorSettings.IsRenderdocEnabled()) { // If enabled in config, try to load RDoc runtime in offline mode if ((mod = dlopen(RENDERDOC_LIB, RTLD_NOW))) { const auto RENDERDOC_GetAPI = diff --git a/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp b/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp index 8f58f3499..a4ebc859d 100644 --- a/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp +++ b/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp @@ -1,8 +1,8 @@ -// 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/assert.h" -#include "common/config.h" +#include "core/emulator_settings.h" #include "video_core/host_shaders/fsr_comp.h" #include "video_core/renderer_vulkan/host_passes/fsr_pass.h" #include "video_core/renderer_vulkan/vk_platform.h" @@ -164,7 +164,7 @@ vk::ImageView FsrPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input, CreateImages(img); } - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{ .pLabelName = "Host/FSR", }); @@ -387,7 +387,7 @@ vk::ImageView FsrPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input, .pImageMemoryBarriers = return_barrier.data(), }); - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.endDebugUtilsLabelEXT(); } @@ -407,6 +407,13 @@ void FsrPass::ResizeAndInvalidate(u32 width, u32 height) { void FsrPass::CreateImages(Img& img) const { img.dirty = false; + // Destroy previous resources before re-creating at new size. + // Views first, then images (views reference the images). + img.intermediary_image_view.reset(); + img.output_image_view.reset(); + img.intermediary_image.Destroy(); + img.output_image.Destroy(); + vk::ImageCreateInfo image_create_info{ .imageType = vk::ImageType::e2D, .format = vk::Format::eR16G16B16A16Sfloat, diff --git a/src/video_core/renderer_vulkan/host_passes/pp_pass.cpp b/src/video_core/renderer_vulkan/host_passes/pp_pass.cpp index 5c1fb4638..4b073c5fe 100644 --- a/src/video_core/renderer_vulkan/host_passes/pp_pass.cpp +++ b/src/video_core/renderer_vulkan/host_passes/pp_pass.cpp @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "video_core/renderer_vulkan/host_passes/pp_pass.h" #include "common/assert.h" -#include "common/config.h" +#include "core/emulator_settings.h" #include "video_core/host_shaders/fs_tri_vert.h" #include "video_core/host_shaders/post_process_frag.h" #include "video_core/renderer_vulkan/vk_platform.h" @@ -188,7 +188,7 @@ void PostProcessingPass::Create(vk::Device device, const vk::Format surface_form void PostProcessingPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input, vk::Extent2D input_size, Frame& frame, Settings settings) { - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{ .pLabelName = "Host/Post processing", }); @@ -279,7 +279,7 @@ void PostProcessingPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input, .pImageMemoryBarriers = &post_barrier, }); - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.endDebugUtilsLabelEXT(); } } diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp index 35eda86da..ba0a3afa2 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp @@ -21,7 +21,11 @@ ComputePipeline::ComputePipeline(const Instance& instance, Scheduler& scheduler, info = &info_; const auto debug_str = GetDebugString(); + const vk::PipelineShaderStageRequiredSubgroupSizeCreateInfo subgroup_size_ci = { + .requiredSubgroupSize = 64, + }; const vk::PipelineShaderStageCreateInfo shader_ci = { + .pNext = instance.IsSubgroupSize64Supported() ? &subgroup_size_ci : nullptr, .stage = vk::ShaderStageFlagBits::eCompute, .module = module, .pName = "main", @@ -44,13 +48,15 @@ ComputePipeline::ComputePipeline(const Instance& instance, Scheduler& scheduler, }); } for (const auto& image : info->images) { + const u32 num_bindings = image.NumBindings(*info); bindings.push_back({ - .binding = binding++, + .binding = binding, .descriptorType = image.is_written ? vk::DescriptorType::eStorageImage : vk::DescriptorType::eSampledImage, - .descriptorCount = 1, + .descriptorCount = num_bindings, .stageFlags = vk::ShaderStageFlagBits::eCompute, }); + binding += num_bindings; } for (const auto& sampler : info->samplers) { bindings.push_back({ diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 242c9b6f2..bc9ef571b 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -457,13 +457,15 @@ void GraphicsPipeline::BuildDescSetLayout(bool preloading) { }); } for (const auto& image : stage->images) { + const u32 num_bindings = image.NumBindings(*stage); bindings.push_back({ - .binding = binding++, + .binding = binding, .descriptorType = image.is_written ? vk::DescriptorType::eStorageImage : vk::DescriptorType::eSampledImage, - .descriptorCount = 1, + .descriptorCount = num_bindings, .stageFlags = stage_bit, }); + binding += num_bindings; } for (const auto& sampler : stage->samplers) { bindings.push_back({ diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 44aa79d98..6898df97a 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -220,9 +220,11 @@ bool Instance::CreateDevice() { const vk::StructureChain properties_chain = physical_device.getProperties2< vk::PhysicalDeviceProperties2, vk::PhysicalDeviceVulkan11Properties, - vk::PhysicalDeviceVulkan12Properties, vk::PhysicalDevicePushDescriptorPropertiesKHR>(); + vk::PhysicalDeviceVulkan12Properties, vk::PhysicalDeviceVulkan13Properties, + vk::PhysicalDevicePushDescriptorPropertiesKHR>(); vk11_props = properties_chain.get(); vk12_props = properties_chain.get(); + vk13_props = properties_chain.get(); push_descriptor_props = properties_chain.get(); LOG_INFO(Render_Vulkan, "Physical device subgroup size {}", vk11_props.subgroupSize); @@ -367,7 +369,7 @@ bool Instance::CreateDevice() { feature_chain.get(); const auto vk11_features = feature_chain.get(); vk12_features = feature_chain.get(); - const auto vk13_features = feature_chain.get(); + vk13_features = feature_chain.get(); vk::StructureChain device_chain = { vk::DeviceCreateInfo{ .queueCreateInfoCount = 1u, @@ -429,6 +431,7 @@ bool Instance::CreateDevice() { vk::PhysicalDeviceVulkan13Features{ .robustImageAccess = vk13_features.robustImageAccess, .shaderDemoteToHelperInvocation = vk13_features.shaderDemoteToHelperInvocation, + .subgroupSizeControl = vk13_features.subgroupSizeControl, .synchronization2 = vk13_features.synchronization2, .dynamicRendering = vk13_features.dynamicRendering, .maintenance4 = vk13_features.maintenance4, diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 8975669bb..7a8a906d5 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -233,6 +233,11 @@ public: return vk12_features.shaderSharedInt64Atomics; } + /// Returns true if the subgroup size can be set to match guest subgroup size + bool IsSubgroupSize64Supported() const { + return vk13_features.subgroupSizeControl && vk13_props.maxSubgroupSize >= 64; + } + /// Returns true when VK_KHR_workgroup_memory_explicit_layout is supported. bool IsWorkgroupMemoryExplicitLayoutSupported() const { return workgroup_memory_explicit_layout && @@ -455,9 +460,11 @@ private: vk::PhysicalDeviceMemoryProperties memory_properties; vk::PhysicalDeviceVulkan11Properties vk11_props; vk::PhysicalDeviceVulkan12Properties vk12_props; + vk::PhysicalDeviceVulkan13Properties vk13_props; vk::PhysicalDevicePushDescriptorPropertiesKHR push_descriptor_props; vk::PhysicalDeviceFeatures features; vk::PhysicalDeviceVulkan12Features vk12_features; + vk::PhysicalDeviceVulkan13Features vk13_features; vk::PhysicalDevicePortabilitySubsetFeaturesKHR portability_features; vk::PhysicalDeviceExtendedDynamicState3FeaturesEXT dynamic_state_3_features; vk::PhysicalDeviceRobustness2FeaturesEXT robustness2_features; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 1b0af1d17..2666f05d3 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -1,13 +1,13 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/hash.h" #include "common/io_file.h" #include "common/path_util.h" #include "core/debug_state.h" +#include "core/emulator_settings.h" #include "shader_recompiler/backend/spirv/emit_spirv.h" #include "shader_recompiler/info.h" #include "shader_recompiler/recompiler.h" @@ -300,7 +300,7 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline() { RegisterPipelineData(graphics_key, pipeline_hash, sdata); ++num_new_pipelines; - if (Config::collectShadersForDebug()) { + if (EmulatorSettings.IsShaderCollect()) { for (auto stage = 0; stage < MaxShaderStages; ++stage) { if (infos[stage]) { auto& m = modules[stage]; @@ -329,7 +329,7 @@ const ComputePipeline* PipelineCache::GetComputePipeline() { RegisterPipelineData(compute_key, sdata); ++num_new_pipelines; - if (Config::collectShadersForDebug()) { + if (EmulatorSettings.IsShaderCollect()) { auto& m = modules[0]; module_related_pipelines[m].emplace_back(compute_key); } @@ -554,7 +554,7 @@ vk::ShaderModule PipelineCache::CompileModule(Shader::Info& info, Shader::Runtim vk::ShaderModule module; auto patch = GetShaderPatch(info.pgm_hash, info.stage, perm_idx, "spv"); - const bool is_patched = patch && Config::patchShaders(); + const bool is_patched = patch && EmulatorSettings.IsPatchShaders(); if (is_patched) { LOG_INFO(Loader, "Loaded patch for {} shader {:#x}", info.stage, info.pgm_hash); module = CompileSPV(*patch, instance.GetDevice()); @@ -566,7 +566,7 @@ vk::ShaderModule PipelineCache::CompileModule(Shader::Info& info, Shader::Runtim const auto name = GetShaderName(info.stage, info.pgm_hash, perm_idx); Vulkan::SetObjectName(instance.GetDevice(), module, name); - if (Config::collectShadersForDebug()) { + if (EmulatorSettings.IsShaderCollect()) { DebugState.CollectShader(name, info.l_stage, module, spv, code, patch ? *patch : std::span{}, is_patched); } @@ -659,7 +659,7 @@ std::string PipelineCache::GetShaderName(Shader::Stage stage, u64 hash, void PipelineCache::DumpShader(std::span code, u64 hash, Shader::Stage stage, size_t perm_idx, std::string_view ext) { - if (!Config::dumpShaders()) { + if (!EmulatorSettings.IsDumpShaders()) { return; } diff --git a/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp b/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp index 61c4bac7e..eb31d4994 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp @@ -1,8 +1,8 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/serdes.h" +#include "core/emulator_settings.h" #include "shader_recompiler/frontend/fetch_shader.h" #include "shader_recompiler/info.h" #include "video_core/cache_storage.h" @@ -295,7 +295,7 @@ bool PipelineCache::LoadPipelineStage(Serialization::Archive& ar, size_t stage) } void PipelineCache::WarmUp() { - if (!Config::isPipelineCacheEnabled()) { + if (!EmulatorSettings.IsPipelineCacheEnabled()) { return; } diff --git a/src/video_core/renderer_vulkan/vk_platform.cpp b/src/video_core/renderer_vulkan/vk_platform.cpp index 7027d62f8..c77c80223 100644 --- a/src/video_core/renderer_vulkan/vk_platform.cpp +++ b/src/video_core/renderer_vulkan/vk_platform.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 the vulkan platform specific header @@ -17,9 +17,9 @@ #include #include "common/assert.h" -#include "common/config.h" #include "common/logging/log.h" #include "common/path_util.h" +#include "core/emulator_settings.h" #include "sdl_window.h" #include "video_core/renderer_vulkan/vk_platform.h" @@ -87,7 +87,7 @@ vk::SurfaceKHR CreateSurface(vk::Instance instance, const Frontend::WindowSDL& e UNREACHABLE(); } } else if (window_info.type == Frontend::WindowSystemType::Wayland) { - if (Config::isRdocEnabled()) { + if (EmulatorSettings.IsRenderdocEnabled()) { LOG_ERROR(Render_Vulkan, "RenderDoc is not compatible with Wayland, use an X11 window instead."); } @@ -200,7 +200,7 @@ std::vector GetInstanceExtensions(Frontend::WindowSystemType window extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME); } - if (Config::allowHDR()) { + if (EmulatorSettings.IsHdrAllowed()) { extensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); } @@ -306,9 +306,9 @@ vk::UniqueInstance CreateInstance(Frontend::WindowSystemType window_type, bool e LOG_INFO(Render_Vulkan, "Enabled instance layers: {}", layers_string); // Validation settings - vk::Bool32 enable_core = Config::vkValidationCoreEnabled() ? vk::True : vk::False; - vk::Bool32 enable_sync = Config::vkValidationSyncEnabled() ? vk::True : vk::False; - vk::Bool32 enable_gpuav = Config::vkValidationGpuEnabled() ? vk::True : vk::False; + vk::Bool32 enable_core = EmulatorSettings.IsVkValidationCoreEnabled() ? vk::True : vk::False; + vk::Bool32 enable_sync = EmulatorSettings.IsVkValidationSyncEnabled() ? vk::True : vk::False; + vk::Bool32 enable_gpuav = EmulatorSettings.IsVkValidationGpuEnabled() ? vk::True : vk::False; // Crash diagnostics settings static const auto crash_diagnostic_path = diff --git a/src/video_core/renderer_vulkan/vk_presenter.cpp b/src/video_core/renderer_vulkan/vk_presenter.cpp index 1694d137f..c2a2a6621 100644 --- a/src/video_core/renderer_vulkan/vk_presenter.cpp +++ b/src/video_core/renderer_vulkan/vk_presenter.cpp @@ -1,12 +1,12 @@ -// 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/config.h" #include "common/debug.h" #include "common/elf_info.h" #include "common/singleton.h" #include "core/debug_state.h" #include "core/devtools/layer.h" +#include "core/emulator_settings.h" #include "core/libraries/system/systemservice.h" #include "imgui/renderer/imgui_core.h" #include "imgui/renderer/imgui_impl_vulkan.h" @@ -104,8 +104,8 @@ static vk::Rect2D FitImage(s32 frame_width, s32 frame_height, s32 swapchain_widt Presenter::Presenter(Frontend::WindowSDL& window_, AmdGpu::Liverpool* liverpool_) : window{window_}, liverpool{liverpool_}, - instance{window, Config::getGpuId(), Config::vkValidationEnabled(), - Config::getVkCrashDiagnosticEnabled()}, + instance{window, EmulatorSettings.GetGpuId(), EmulatorSettings.IsVkValidationEnabled(), + EmulatorSettings.IsVkCrashDiagnosticEnabled()}, draw_scheduler{instance}, present_scheduler{instance}, flip_scheduler{instance}, swapchain{instance, window}, rasterizer{std::make_unique(instance, draw_scheduler, liverpool)}, @@ -124,9 +124,10 @@ Presenter::Presenter(Frontend::WindowSDL& window_, AmdGpu::Liverpool* liverpool_ free_queue.push(&frame); } - fsr_settings.enable = Config::getFsrEnabled(); - fsr_settings.use_rcas = Config::getRcasEnabled(); - fsr_settings.rcas_attenuation = static_cast(Config::getRcasAttenuation() / 1000.f); + fsr_settings.enable = EmulatorSettings.IsFsrEnabled(); + fsr_settings.use_rcas = EmulatorSettings.IsRcasEnabled(); + fsr_settings.rcas_attenuation = + static_cast(EmulatorSettings.GetRcasAttenuation() / 1000.f); fsr_pass.Create(device, instance.GetAllocator(), num_images); pp_pass.Create(device, swapchain.GetSurfaceFormat().format); @@ -465,7 +466,7 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) { auto& scheduler = present_scheduler; const auto cmdbuf = scheduler.CommandBuffer(); - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{ .pLabelName = "Present", }); @@ -577,7 +578,7 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) { ImGui::SetCursorPos(ImGui::GetCursorStartPos() + offset); ImGui::Image(game_texture, size); - if (Config::nullGpu()) { + if (EmulatorSettings.IsNullGPU()) { Core::Devtools::Layer::DrawNullGpuNotice(); } } @@ -595,8 +596,7 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame) { TracyVkCollect(profiler_ctx, cmdbuf); } } - - if (Config::getVkHostMarkersEnabled()) { + if (EmulatorSettings.IsVkHostMarkersEnabled()) { cmdbuf.endDebugUtilsLabelEXT(); } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 737c9feed..800941fe3 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1,8 +1,8 @@ -// 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/config.h" #include "common/debug.h" +#include "core/emulator_settings.h" #include "core/memory.h" #include "shader_recompiler/runtime_info.h" #include "video_core/amdgpu/liverpool.h" @@ -38,7 +38,7 @@ Rasterizer::Rasterizer(const Instance& instance_, Scheduler& scheduler_, texture_cache{instance, scheduler, liverpool_, buffer_cache, page_manager}, liverpool{liverpool_}, memory{Core::Memory::Instance()}, pipeline_cache{instance, scheduler, liverpool} { - if (!Config::nullGpu()) { + if (!EmulatorSettings.IsNullGPU()) { liverpool->BindRasterizer(this); } memory->SetRasterizer(this); @@ -662,6 +662,13 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Binding void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindings& binding) { image_bindings.clear(); + const u32 first_image_idx = image_infos.size(); + // For loading/storing to explicit mip levels, when no native instruction support, bind an array + // of descriptors consecutively, 1 for each mip level. The shader can index this with LOD + // operand. + // This array holds the size of each consecutive array with the number of bindings consumed. + // This is currently always 1 for anything other than mip fallback arrays. + boost::container::small_vector image_descriptor_array_sizes; for (const auto& image_desc : stage.images) { const auto tsharp = image_desc.GetSharp(stage); @@ -671,25 +678,43 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin if (tsharp.GetDataFmt() == AmdGpu::DataFormat::FormatInvalid) { image_bindings.emplace_back(std::piecewise_construct, std::tuple{}, std::tuple{}); + image_descriptor_array_sizes.push_back(1); continue; } - auto& [image_id, desc] = image_bindings.emplace_back(std::piecewise_construct, std::tuple{}, - std::tuple{tsharp, image_desc}); - image_id = texture_cache.FindImage(desc); - auto* image = &texture_cache.GetImage(image_id); - if (image->depth_id) { - // If this image has an associated depth image, it's a stencil attachment. - // Redirect the access to the actual depth-stencil buffer. - image_id = image->depth_id; - image = &texture_cache.GetImage(image_id); + const Shader::MipStorageFallbackMode mip_fallback_mode = image_desc.mip_fallback_mode; + const u32 num_bindings = image_desc.NumBindings(stage); + + for (auto i = 0; i < num_bindings; i++) { + auto& [image_id, desc] = image_bindings.emplace_back( + std::piecewise_construct, std::tuple{}, std::tuple{tsharp, image_desc}); + + if (mip_fallback_mode == Shader::MipStorageFallbackMode::ConstantIndex) { + ASSERT(num_bindings == 1); + desc.view_info.range.base.level += image_desc.constant_mip_index; + desc.view_info.range.extent.levels = 1; + } else if (mip_fallback_mode == Shader::MipStorageFallbackMode::DynamicIndex) { + desc.view_info.range.base.level += i; + desc.view_info.range.extent.levels = 1; + } + + image_id = texture_cache.FindImage(desc); + auto* image = &texture_cache.GetImage(image_id); + if (image->depth_id) { + // If this image has an associated depth image, it's a stencil attachment. + // Redirect the access to the actual depth-stencil buffer. + image_id = image->depth_id; + image = &texture_cache.GetImage(image_id); + } + if (image->binding.is_bound) { + // The image is already bound. In case if it is about to be used as storage we + // need to force general layout on it. + image->binding.force_general |= image_desc.is_written; + } + image->binding.is_bound = 1u; } - if (image->binding.is_bound) { - // The image is already bound. In case if it is about to be used as storage we need - // to force general layout on it. - image->binding.force_general |= image_desc.is_written; - } - image->binding.is_bound = 1u; + + image_descriptor_array_sizes.push_back(num_bindings); } // Second pass to re-bind images that were updated after binding @@ -749,16 +774,26 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin image_infos.emplace_back(VK_NULL_HANDLE, *image_view.image_view, image.backing->state.layout); } + } + u32 image_info_idx = first_image_idx; + u32 image_binding_idx = 0; + for (u32 array_size : image_descriptor_array_sizes) { + const auto& [_, desc] = image_bindings[image_binding_idx]; + const bool is_storage = desc.type == VideoCore::TextureCache::BindingType::Storage; set_writes.push_back({ .dstSet = VK_NULL_HANDLE, - .dstBinding = binding.unified++, + .dstBinding = binding.unified, .dstArrayElement = 0, - .descriptorCount = 1, + .descriptorCount = array_size, .descriptorType = is_storage ? vk::DescriptorType::eStorageImage : vk::DescriptorType::eSampledImage, - .pImageInfo = &image_infos.back(), + .pImageInfo = &image_infos[image_info_idx], }); + + image_info_idx += array_size; + image_binding_idx += array_size; + binding.unified += array_size; } for (const auto& sampler : stage.samplers) { @@ -1278,8 +1313,8 @@ void Rasterizer::UpdateColorBlendingState(const GraphicsPipeline* pipeline) cons } void Rasterizer::ScopeMarkerBegin(const std::string_view& str, bool from_guest) { - if ((from_guest && !Config::getVkGuestMarkersEnabled()) || - (!from_guest && !Config::getVkHostMarkersEnabled())) { + if ((from_guest && !EmulatorSettings.IsVkGuestMarkersEnabled()) || + (!from_guest && !EmulatorSettings.IsVkHostMarkersEnabled())) { return; } const auto cmdbuf = scheduler.CommandBuffer(); @@ -1289,8 +1324,8 @@ void Rasterizer::ScopeMarkerBegin(const std::string_view& str, bool from_guest) } void Rasterizer::ScopeMarkerEnd(bool from_guest) { - if ((from_guest && !Config::getVkGuestMarkersEnabled()) || - (!from_guest && !Config::getVkHostMarkersEnabled())) { + if ((from_guest && !EmulatorSettings.IsVkGuestMarkersEnabled()) || + (!from_guest && !EmulatorSettings.IsVkHostMarkersEnabled())) { return; } const auto cmdbuf = scheduler.CommandBuffer(); @@ -1298,8 +1333,8 @@ void Rasterizer::ScopeMarkerEnd(bool from_guest) { } void Rasterizer::ScopedMarkerInsert(const std::string_view& str, bool from_guest) { - if ((from_guest && !Config::getVkGuestMarkersEnabled()) || - (!from_guest && !Config::getVkHostMarkersEnabled())) { + if ((from_guest && !EmulatorSettings.IsVkGuestMarkersEnabled()) || + (!from_guest && !EmulatorSettings.IsVkHostMarkersEnabled())) { return; } const auto cmdbuf = scheduler.CommandBuffer(); @@ -1310,8 +1345,8 @@ void Rasterizer::ScopedMarkerInsert(const std::string_view& str, bool from_guest void Rasterizer::ScopedMarkerInsertColor(const std::string_view& str, const u32 color, bool from_guest) { - if ((from_guest && !Config::getVkGuestMarkersEnabled()) || - (!from_guest && !Config::getVkHostMarkersEnabled())) { + if ((from_guest && !EmulatorSettings.IsVkGuestMarkersEnabled()) || + (!from_guest && !EmulatorSettings.IsVkHostMarkersEnabled())) { return; } const auto cmdbuf = scheduler.CommandBuffer(); diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index 4dd3bd502..04f9d8504 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include "common/assert.h" -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "imgui/renderer/imgui_core.h" #include "sdl_window.h" #include "video_core/renderer_vulkan/vk_instance.h" @@ -164,7 +164,7 @@ void Swapchain::FindPresentFormat() { return format == SURFACE_FORMAT_HDR; }) != formats.end(); // Also make sure that user allowed us to use HDR - supports_hdr &= Config::allowHDR(); + supports_hdr &= EmulatorSettings.IsHdrAllowed(); // If there is a single undefined surface format, the device doesn't care, so we'll just use // RGBA sRGB. @@ -199,7 +199,7 @@ void Swapchain::FindPresentMode() { return; } - const auto requested_mode = Config::getPresentMode(); + const auto requested_mode = EmulatorSettings.GetPresentMode(); if (requested_mode == "Mailbox") { present_mode = vk::PresentModeKHR::eMailbox; } else if (requested_mode == "Fifo") { @@ -208,7 +208,7 @@ void Swapchain::FindPresentMode() { present_mode = vk::PresentModeKHR::eImmediate; } else { LOG_ERROR(Render_Vulkan, "Unknown present mode {}, defaulting to Mailbox.", - Config::getPresentMode()); + EmulatorSettings.GetPresentMode()); present_mode = vk::PresentModeKHR::eMailbox; } diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index 972f028d4..44ddd55c6 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -82,6 +82,14 @@ UniqueImage::~UniqueImage() { } } +void UniqueImage::Destroy() { + if (image) { + vmaDestroyImage(allocator, image, allocation); + image = vk::Image{}; + allocation = {}; + } +} + void UniqueImage::Create(const vk::ImageCreateInfo& image_ci) { this->image_ci = image_ci; ASSERT(!image); @@ -239,7 +247,11 @@ Image::Barriers Image::GetBarriers(vk::ImageLayout dst_layout, vk::AccessFlags2 ASSERT(subres_idx < subresource_states.size()); auto& state = subresource_states[subres_idx]; - if (state.layout != dst_layout || state.access_mask != dst_mask) { + constexpr auto write_flags = vk::AccessFlagBits2::eTransferWrite | + vk::AccessFlagBits2::eShaderWrite | + vk::AccessFlagBits2::eMemoryWrite; + const bool is_write = static_cast(state.access_mask & write_flags); + if (state.layout != dst_layout || state.access_mask != dst_mask || is_write) { barriers.emplace_back(vk::ImageMemoryBarrier2{ .srcStageMask = state.pl_stage, .srcAccessMask = state.access_mask, @@ -269,7 +281,11 @@ Image::Barriers Image::GetBarriers(vk::ImageLayout dst_layout, vk::AccessFlags2 subresource_states.clear(); } } else { // Full resource transition - if (last_state.layout == dst_layout && last_state.access_mask == dst_mask) { + constexpr auto write_flags = vk::AccessFlagBits2::eTransferWrite | + vk::AccessFlagBits2::eShaderWrite | + vk::AccessFlagBits2::eMemoryWrite; + const bool is_write = static_cast(last_state.access_mask & write_flags); + if (last_state.layout == dst_layout && last_state.access_mask == dst_mask && !is_write) { return {}; } @@ -366,6 +382,8 @@ void Image::Upload(std::span upload_copies, vk::Buffe .bufferMemoryBarrierCount = 1, .pBufferMemoryBarriers = &post_barrier, }); + Transit(vk::ImageLayout::eGeneral, + vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); flags &= ~ImageFlagBits::Dirty; } @@ -636,6 +654,8 @@ void Image::CopyImageWithBuffer(Image& src_image, vk::Buffer buffer, u64 offset) cmdbuf.copyBufferToImage(buffer, GetImage(), vk::ImageLayout::eTransferDstOptimal, buffer_copies); + Transit(vk::ImageLayout::eGeneral, + vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); } void Image::CopyMip(Image& src_image, u32 mip, u32 slice) { @@ -675,6 +695,8 @@ void Image::CopyMip(Image& src_image, u32 mip, u32 slice) { const auto cmdbuf = scheduler->CommandBuffer(); cmdbuf.copyImage(src_image.GetImage(), src_image.backing->state.layout, GetImage(), backing->state.layout, image_copy); + Transit(vk::ImageLayout::eGeneral, + vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); } void Image::Resolve(Image& src_image, const VideoCore::SubresourceRange& mrt0_range, diff --git a/src/video_core/texture_cache/image.h b/src/video_core/texture_cache/image.h index 00b9296b7..0bf471dce 100644 --- a/src/video_core/texture_cache/image.h +++ b/src/video_core/texture_cache/image.h @@ -59,6 +59,8 @@ struct UniqueImage { void Create(const vk::ImageCreateInfo& image_ci); + void Destroy(); + operator vk::Image() const { return image; } diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 8163902cc..163712756 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -1,12 +1,12 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/assert.h" -#include "common/config.h" #include "common/debug.h" #include "common/scope_exit.h" +#include "core/emulator_settings.h" #include "core/memory.h" #include "video_core/buffer_cache/buffer_cache.h" #include "video_core/page_manager.h" @@ -641,7 +641,7 @@ ImageView& TextureCache::FindTexture(ImageId image_id, const ImageDesc& desc) { Image& image = slot_images[image_id]; if (desc.type == BindingType::Storage) { image.flags |= ImageFlagBits::GpuModified; - if (Config::readbackLinearImages() && !image.info.props.is_tiled && + if (EmulatorSettings.IsReadbackLinearImagesEnabled() && !image.info.props.is_tiled && image.info.guest_address != 0) { download_images.emplace(image_id); } @@ -653,7 +653,7 @@ ImageView& TextureCache::FindTexture(ImageId image_id, const ImageDesc& desc) { ImageView& TextureCache::FindRenderTarget(ImageId image_id, const ImageDesc& desc) { Image& image = slot_images[image_id]; image.flags |= ImageFlagBits::GpuModified; - if (Config::readbackLinearImages() && !image.info.props.is_tiled) { + if (EmulatorSettings.IsReadbackLinearImagesEnabled() && !image.info.props.is_tiled) { download_images.emplace(image_id); } image.usage.render_target = 1u; diff --git a/tests/.clang-format b/tests/.clang-format new file mode 100644 index 000000000..d69044b0f --- /dev/null +++ b/tests/.clang-format @@ -0,0 +1,259 @@ +# SPDX-FileCopyrightText: 2016 Emmanuel Gil Peyrot +# SPDX-License-Identifier: GPL-2.0-or-later + +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +IncludeCategories: + - Regex: '^\<[^Q][^/.>]*\>' + Priority: -2 + - Regex: '^\<' + Priority: -1 + - Regex: '^\"' + Priority: 0 +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 150 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Never +--- +Language: Java +# BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 100 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +IncludeCategories: + - Regex: '^\<[^Q][^/.>]*\>' + Priority: -2 + - Regex: '^\<' + Priority: -1 + - Regex: '^\"' + Priority: 0 +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 150 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never +--- +Language: ObjC +# BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 100 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +IncludeCategories: + - Regex: '^\<[^Q][^/.>]*\>' + Priority: -2 + - Regex: '^\<' + Priority: -1 + - Regex: '^\"' + Priority: 0 +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 150 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never +... diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 000000000..6b656762a --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +# Find or download Google Test +include(FetchContent) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip +) +FetchContent_MakeAvailable(googletest) + +set(SETTINGS_TEST_SOURCES + # Under test + ${CMAKE_SOURCE_DIR}/src/core/emulator_settings.cpp + ${CMAKE_SOURCE_DIR}/src/core/emulator_state.cpp + + # Minimal common support + ${CMAKE_SOURCE_DIR}/src/common/path_util.cpp + ${CMAKE_SOURCE_DIR}/src/common/assert.cpp + ${CMAKE_SOURCE_DIR}/src/common/error.cpp + ${CMAKE_SOURCE_DIR}/src/common/string_util.cpp + ${CMAKE_SOURCE_DIR}/src/common/logging/filter.cpp + ${CMAKE_SOURCE_DIR}/src/common/logging/text_formatter.cpp + + # Stubs that replace dependencies + stubs/log_stub.cpp + stubs/scm_rev_stub.cpp + stubs/sdl_stub.cpp + + # Tests + test_emulator_settings.cpp +) + +add_executable(shadps4_settings_test ${SETTINGS_TEST_SOURCES}) + +target_include_directories(shadps4_settings_test PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR} +) + +target_link_libraries(shadps4_settings_test PRIVATE + GTest::gtest_main + fmt::fmt + nlohmann_json::nlohmann_json + toml11::toml11 + SDL3::SDL3 +) + +target_compile_features(shadps4_settings_test PRIVATE cxx_std_23) + +target_compile_definitions(shadps4_settings_test PRIVATE BOOST_ASIO_STANDALONE) + +if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR + CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + include(CheckCXXSymbolExists) + check_cxx_symbol_exists(_LIBCPP_VERSION version LIBCPP) + if (LIBCPP) + target_compile_options(shadps4_settings_test PRIVATE -fexperimental-library) + endif() +endif() + +if (WIN32) + target_compile_definitions(shadps4_settings_test PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + NTDDI_VERSION=0x0A000006 + _WIN32_WINNT=0x0A00 + WINVER=0x0A00 + ) + if (MSVC) + target_compile_definitions(shadps4_settings_test PRIVATE + _CRT_SECURE_NO_WARNINGS + _CRT_NONSTDC_NO_DEPRECATE + _SCL_SECURE_NO_WARNINGS + _TIMESPEC_DEFINED + ) + endif() +endif() + +include(GoogleTest) +gtest_discover_tests(shadps4_settings_test + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + PROPERTIES TIMEOUT 60 +) diff --git a/tests/stubs/log_stub.cpp b/tests/stubs/log_stub.cpp new file mode 100644 index 000000000..64d6df644 --- /dev/null +++ b/tests/stubs/log_stub.cpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include "common/logging/backend.h" +#include "common/logging/filter.h" +#include "common/logging/log.h" + +namespace Common::Log { + + void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, + unsigned int line_num, const char* function, const char* format, + const fmt::format_args& args) { + } + + void Initialize(std::string_view) {} + bool IsActive() { return false; } + void SetGlobalFilter(const Filter&) {} + void SetColorConsoleBackendEnabled(bool) {} + void Start() {} + void Stop() {} + void Denitializer() {} + void SetAppend() {} + +} // namespace Common::Log diff --git a/tests/stubs/scm_rev_stub.cpp b/tests/stubs/scm_rev_stub.cpp new file mode 100644 index 000000000..0f8a5af03 --- /dev/null +++ b/tests/stubs/scm_rev_stub.cpp @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "common/scm_rev.h" + +namespace Common { + + constexpr char g_version[] = "0.0.0 TEST"; + constexpr bool g_is_release = false; + constexpr char g_scm_rev[] = "test_rev_hash"; + constexpr char g_scm_branch[] = "test_branch"; + constexpr char g_scm_desc[] = "test_desc"; + constexpr char g_scm_remote_name[] = "origin"; + constexpr char g_scm_remote_url[] = "https://github.com/test/shadPS4"; + constexpr char g_scm_date[] = "2026-03-23"; + + const std::string GetRemoteNameFromLink() { + return "test"; + } + +} // namespace Common diff --git a/tests/stubs/sdl_stub.cpp b/tests/stubs/sdl_stub.cpp new file mode 100644 index 000000000..859147696 --- /dev/null +++ b/tests/stubs/sdl_stub.cpp @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +extern "C" { + + bool SDL_ShowMessageBox(const SDL_MessageBoxData* /* messageboxdata */, int* buttonid) { + if (buttonid) *buttonid = 0; // "No",skip migration + return true; + } + + bool SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags /* flags */, const char* /* title */, + const char* /* message */, SDL_Window* /* window */) { + return true; + } + +} // extern "C" diff --git a/tests/test_emulator_settings.cpp b/tests/test_emulator_settings.cpp new file mode 100644 index 000000000..1a4a667b3 --- /dev/null +++ b/tests/test_emulator_settings.cpp @@ -0,0 +1,837 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "common/path_util.h" +#include "common/scm_rev.h" +#include "core/emulator_settings.h" +#include "core/emulator_state.h" + +namespace fs = std::filesystem; +using json = nlohmann::json; + +class TempDir { +public: + TempDir() { + auto ns = std::chrono::steady_clock::now().time_since_epoch().count(); + temp_path = fs::temp_directory_path() / ("shadps4_test_" + std::to_string(ns) + "_" + + std::to_string(reinterpret_cast(this))); + fs::create_directories(temp_path); + } + ~TempDir() { + std::error_code ec; + fs::remove_all(temp_path, ec); + } + const fs::path& path() const { + return temp_path; + } + +private: + fs::path temp_path; +}; + +static void WriteJson(const fs::path& p, const json& j) { + std::ofstream out(p); + ASSERT_TRUE(out.is_open()) << "Cannot write: " << p; + out << std::setw(2) << j; +} + +static json ReadJson(const fs::path& p) { + std::ifstream in(p); + EXPECT_TRUE(in.is_open()) << "Cannot read: " << p; + json j; + in >> j; + return j; +} + +class EmulatorSettingsTest : public ::testing::Test { +protected: + void SetUp() override { + temp_dir = std::make_unique(); + const fs::path root = temp_dir->path(); + + using PT = Common::FS::PathType; + const struct { + PT type; + const char* sub; + } dirs[] = { + {PT::UserDir, ""}, + {PT::LogDir, "log"}, + {PT::ScreenshotsDir, "screenshots"}, + {PT::ShaderDir, "shader"}, + {PT::GameDataDir, "data"}, + {PT::TempDataDir, "temp"}, + {PT::SysModuleDir, "sys_modules"}, + {PT::DownloadDir, "download"}, + {PT::CapturesDir, "captures"}, + {PT::CheatsDir, "cheats"}, + {PT::PatchesDir, "patches"}, + {PT::MetaDataDir, "game_data"}, + {PT::CustomTrophy, "custom_trophy"}, + {PT::CustomConfigs, "custom_configs"}, + {PT::CacheDir, "cache"}, + {PT::FontsDir, "fonts"}, + {PT::HomeDir, "home"}, + }; + for (const auto& d : dirs) { + fs::path p = d.sub[0] ? (root / d.sub) : root; + fs::create_directories(p); + Common::FS::SetUserPath(d.type, p); + } + + temp_state = std::make_shared(); + EmulatorState::SetInstance(temp_state); + + temp_settings = std::make_shared(); + EmulatorSettingsImpl::SetInstance(temp_settings); + } + + void TearDown() override { + EmulatorSettingsImpl::SetInstance(nullptr); + EmulatorState::SetInstance(nullptr); + temp_settings.reset(); + temp_state.reset(); + temp_dir.reset(); + } + + fs::path ConfigJson() const { + return temp_dir->path() / "config.json"; + } + fs::path GameConfig(const std::string& serial) const { + return temp_dir->path() / "custom_configs" / (serial + ".json"); + } + + std::unique_ptr temp_dir; + std::shared_ptr temp_settings; + std::shared_ptr temp_state; +}; + +// tests Settting template , default , Global override modes + +TEST(SettingTest, DefaultCtorZeroInitialises) { + Setting s; + EXPECT_EQ(s.value, 0); + EXPECT_EQ(s.default_value, 0); + EXPECT_FALSE(s.game_specific_value.has_value()); +} + +TEST(SettingTest, ValueCtorSetsBothValueAndDefault) { + Setting s{42}; + EXPECT_EQ(s.value, 42); + EXPECT_EQ(s.default_value, 42); +} + +TEST(SettingTest, GetDefaultPrefersGameSpecificOverBase) { + Setting s{10}; + s.value = 20; + s.game_specific_value = 99; + EXPECT_EQ(s.get(ConfigMode::Default), 99); +} + +TEST(SettingTest, GetDefaultFallsBackToBaseWhenNoOverride) { + Setting s{10}; + s.value = 20; + EXPECT_EQ(s.get(ConfigMode::Default), 20); +} + +TEST(SettingTest, GetGlobalIgnoresGameSpecific) { + Setting s{10}; + s.value = 20; + s.game_specific_value = 99; + EXPECT_EQ(s.get(ConfigMode::Global), 20); +} + +TEST(SettingTest, GetCleanAlwaysReturnsFactoryDefault) { + Setting s{10}; + s.value = 20; + s.game_specific_value = 99; + EXPECT_EQ(s.get(ConfigMode::Clean), 10); +} + +TEST(SettingTest, SetWritesToBaseOnly) { + Setting s{0}; + s.game_specific_value = 55; + s.set(77); + EXPECT_EQ(s.value, 77); + EXPECT_EQ(s.game_specific_value.value(), 55); // override untouched +} + +TEST(SettingTest, ResetGameSpecificClearsOverride) { + Setting s{0}; + s.game_specific_value = 55; + s.reset_game_specific(); + EXPECT_FALSE(s.game_specific_value.has_value()); + // base and default must be intact + EXPECT_EQ(s.value, 0); + EXPECT_EQ(s.default_value, 0); +} + +TEST(SettingTest, BoolSettingAllModes) { + Setting s{false}; + s.value = true; + s.game_specific_value = false; + EXPECT_FALSE(s.get(ConfigMode::Default)); + EXPECT_TRUE(s.get(ConfigMode::Global)); + EXPECT_FALSE(s.get(ConfigMode::Clean)); +} + +TEST(SettingTest, StringSettingAllModes) { + Setting s{"shadow"}; + s.value = "rule"; + s.game_specific_value = "override"; + EXPECT_EQ(s.get(ConfigMode::Default), "override"); + EXPECT_EQ(s.get(ConfigMode::Global), "rule"); + EXPECT_EQ(s.get(ConfigMode::Clean), "shadow"); +} + +TEST(SettingTest, NoGameSpecificDefaultAndGlobalAgree) { + Setting s{7}; + s.value = 7; + EXPECT_EQ(s.get(ConfigMode::Default), s.get(ConfigMode::Global)); +} + +// tests for default settings + +TEST_F(EmulatorSettingsTest, SetDefaultValuesResetsAllGroupsToFactory) { + // set random values + temp_settings->SetNeo(true); + temp_settings->SetWindowWidth(3840u); + temp_settings->SetGpuId(2); + temp_settings->SetDebugDump(true); + temp_settings->SetCursorState(HideCursorState::Always); + + temp_settings->SetDefaultValues(); // reset to defaults + // check if values are reset to defaults + EXPECT_FALSE(temp_settings->IsNeo()); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280u); + EXPECT_EQ(temp_settings->GetGpuId(), -1); + EXPECT_FALSE(temp_settings->IsDebugDump()); + EXPECT_EQ(temp_settings->GetCursorState(), static_cast(HideCursorState::Idle)); +} + +TEST_F(EmulatorSettingsTest, SetDefaultValuesClearsGameSpecificOverrides) { + // check that game-specific overrides are cleared by SetDefaultValues + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA00001"), game); + temp_settings->Load("CUSA00001"); + + temp_settings->SetDefaultValues(); + temp_settings->SetConfigMode(ConfigMode::Default); + + EXPECT_FALSE(temp_settings->IsNeo()); // default is false should be loaded instead of override +} + +// configModes tests + +TEST_F(EmulatorSettingsTest, ConfigModeSetAndGetRoundTrips) { + temp_settings->SetConfigMode(ConfigMode::Clean); + EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Clean); + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Global); + temp_settings->SetConfigMode(ConfigMode::Default); + EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Default); +} + +TEST_F(EmulatorSettingsTest, ConfigModeCleanReturnFactoryDefaults) { + temp_settings->SetWindowWidth(3840u); + json game; + game["GPU"]["window_width"] = 2560; + WriteJson(GameConfig("CUSA00001"), game); + temp_settings->Load("CUSA00001"); + + temp_settings->SetConfigMode(ConfigMode::Clean); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); // factory default +} + +TEST_F(EmulatorSettingsTest, ConfigModeGlobalIgnoresGameSpecific) { + temp_settings->SetNeo(false); + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA00001"), game); + temp_settings->Load("CUSA00001"); + + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_FALSE(temp_settings->IsNeo()); +} + +TEST_F(EmulatorSettingsTest, ConfigModeDefaultResolvesGameSpecificWhenPresent) { + temp_settings->SetNeo(false); + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA00001"), game); + temp_settings->Load("CUSA00001"); + + temp_settings->SetConfigMode(ConfigMode::Default); + EXPECT_TRUE(temp_settings->IsNeo()); +} + +// tests for global config.json file + +TEST_F(EmulatorSettingsTest, SaveCreatesConfigJson) { + ASSERT_TRUE(temp_settings->Save()); + EXPECT_TRUE(fs::exists(ConfigJson())); +} + +TEST_F(EmulatorSettingsTest, SaveWritesAllExpectedSections) { + ASSERT_TRUE(temp_settings->Save()); + json j = ReadJson(ConfigJson()); + for (const char* section : {"General", "Debug", "Input", "Audio", "GPU", "Vulkan"}) + EXPECT_TRUE(j.contains(section)) << "Missing section: " << section; +} + +TEST_F(EmulatorSettingsTest, LoadReturnsTrueForExistingFile) { + temp_settings->Save(); + auto fresh = std::make_shared(); + EmulatorSettingsImpl::SetInstance(fresh); + EXPECT_TRUE(fresh->Load()); +} + +TEST_F(EmulatorSettingsTest, RoundTripAllGroups) { + temp_settings->SetNeo(true); + temp_settings->SetDebugDump(true); + temp_settings->SetWindowWidth(1920u); + temp_settings->SetGpuId(1); + temp_settings->SetCursorState(HideCursorState::Always); + temp_settings->SetAudioBackend(AudioBackend::OpenAL); + temp_settings->Save(); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); + EXPECT_TRUE(f->IsNeo()); + EXPECT_TRUE(f->IsDebugDump()); + EXPECT_EQ(f->GetWindowWidth(), 1920u); + EXPECT_EQ(f->GetGpuId(), 1); + EXPECT_EQ(f->GetCursorState(), static_cast(HideCursorState::Always)); + EXPECT_EQ(f->GetAudioBackend(), static_cast(AudioBackend::OpenAL)); +} + +TEST_F(EmulatorSettingsTest, LoadMissingFileCreatesDefaultsOnDisk) { + ASSERT_FALSE(fs::exists(ConfigJson())); + temp_settings->Load(); + EXPECT_TRUE(fs::exists(ConfigJson())); + EXPECT_FALSE(temp_settings->IsNeo()); // defaults +} + +TEST_F(EmulatorSettingsTest, LoadMissingSectionDoesNotZeroOtherSections) { + temp_settings->SetNeo(true); + temp_settings->Save(); + json j = ReadJson(ConfigJson()); + j.erase("GPU"); + WriteJson(ConfigJson(), j); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); + + EXPECT_TRUE(f->IsNeo()); // belongs to General, should be loaded + EXPECT_EQ(f->GetWindowWidth(), 1280); // GPU fell back to default +} + +TEST_F(EmulatorSettingsTest, LoadPreservesUnknownKeysOnResave) { + temp_settings->Save(); + json j = ReadJson(ConfigJson()); + j["General"]["future_feature"] = "preserved"; + WriteJson(ConfigJson(), j); + + // A fresh load + save (triggered by version mismatch) must keep the key + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); + f->Save(); + + json after = ReadJson(ConfigJson()); + EXPECT_EQ(after["General"]["future_feature"], "preserved"); +} + +TEST_F(EmulatorSettingsTest, LoadUnknownTopLevelSectionPreserved) { + temp_settings->Save(); + json j = ReadJson(ConfigJson()); + j["FutureSection"]["key"] = 42; + WriteJson(ConfigJson(), j); + + temp_settings->SetNeo(true); + temp_settings->Save(); // merge path + + json after = ReadJson(ConfigJson()); + EXPECT_TRUE(after.contains("FutureSection")); + EXPECT_EQ(after["FutureSection"]["key"], 42); +} + +TEST_F(EmulatorSettingsTest, LoadCorruptJsonDoesNotCrash) { + { + std::ofstream out(ConfigJson()); + out << "{NOT VALID JSON!!!"; + } + EXPECT_NO_THROW(temp_settings->Load()); +} + +TEST_F(EmulatorSettingsTest, LoadEmptyJsonObjectDoesNotCrash) { + WriteJson(ConfigJson(), json::object()); + EXPECT_NO_THROW(temp_settings->Load()); +} + +// tests for per game config + +TEST_F(EmulatorSettingsTest, SaveSerialCreatesPerGameFile) { + ASSERT_TRUE(temp_settings->Save("CUSA01234")); + EXPECT_TRUE(fs::exists(GameConfig("CUSA01234"))); +} + +TEST_F(EmulatorSettingsTest, LoadSerialReturnsFalseWhenFileAbsent) { + EXPECT_FALSE(temp_settings->Load("CUSA99999")); +} + +TEST_F(EmulatorSettingsTest, LoadSerialAppliesOverrideToGameSpecificValue) { + temp_settings->SetNeo(false); + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA01234"), game); + + ASSERT_TRUE(temp_settings->Load("CUSA01234")); + temp_settings->SetConfigMode(ConfigMode::Default); + EXPECT_TRUE(temp_settings->IsNeo()); +} + +TEST_F(EmulatorSettingsTest, LoadSerialBaseValueUntouched) { + temp_settings->SetWindowWidth(1280); + json game; + game["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); +} + +TEST_F(EmulatorSettingsTest, LoadSerialOverridesMultipleGroups) { + temp_settings->SetNeo(false); + temp_settings->SetWindowWidth(1280u); + temp_settings->SetDebugDump(false); + + json game; + game["General"]["neo_mode"] = true; + game["GPU"]["window_width"] = 3840; + game["Debug"]["debug_dump"] = true; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->SetConfigMode(ConfigMode::Default); + EXPECT_TRUE(temp_settings->IsNeo()); + EXPECT_EQ(temp_settings->GetWindowWidth(), 3840); + EXPECT_TRUE(temp_settings->IsDebugDump()); +} + +TEST_F(EmulatorSettingsTest, LoadSerialUnrecognisedKeyIgnored) { + json game; + game["GPU"]["key_that_does_not_exist"] = 999; + WriteJson(GameConfig("CUSA01234"), game); + EXPECT_NO_THROW(temp_settings->Load("CUSA01234")); +} + +TEST_F(EmulatorSettingsTest, LoadSerialTypeMismatch_DoesNotCrash) { + json game; + game["GPU"]["window_width"] = "not_a_number"; + WriteJson(GameConfig("CUSA01234"), game); + EXPECT_NO_THROW(temp_settings->Load("CUSA01234")); + // base unchanged + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280u); +} + +TEST_F(EmulatorSettingsTest, LoadSerialCorruptFileDoesNotCrash) { + { + std::ofstream out(GameConfig("CUSA01234")); + out << "{{{{totally broken"; + } + EXPECT_NO_THROW(temp_settings->Load("CUSA01234")); +} + +TEST_F(EmulatorSettingsTest, SaveSerialWritesGameSpecificValueWhenOverrideLoaded) { + temp_settings->SetWindowWidth(1280); + json game; + game["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->Save("CUSA01234"); + + json saved = ReadJson(GameConfig("CUSA01234")); + EXPECT_EQ(saved["GPU"]["window_width"].get(), 3840); +} + +TEST_F(EmulatorSettingsTest, SaveSerialWritesBaseValueWhenNoOverrideSet) { + temp_settings->SetWindowWidth(2560); + temp_settings->Save("CUSA01234"); + + json saved = ReadJson(GameConfig("CUSA01234")); + EXPECT_EQ(saved["GPU"]["window_width"].get(), 2560); +} + +TEST_F(EmulatorSettingsTest, MultipleSerialsDoNotInterfere) { + json g1; + g1["General"]["neo_mode"] = true; + g1["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA00001"), g1); + + json g2; + g2["General"]["neo_mode"] = false; + g2["GPU"]["window_width"] = 1920; + WriteJson(GameConfig("CUSA00002"), g2); + + { + auto s = std::make_shared(); + EmulatorSettingsImpl::SetInstance(s); + s->Load(); + s->Load("CUSA00001"); + s->SetConfigMode(ConfigMode::Default); + EXPECT_TRUE(s->IsNeo()); + EXPECT_EQ(s->GetWindowWidth(), 3840); + } + { + auto s = std::make_shared(); + EmulatorSettingsImpl::SetInstance(s); + s->Load(); + s->Load("CUSA00002"); + s->SetConfigMode(ConfigMode::Default); + EXPECT_FALSE(s->IsNeo()); + EXPECT_EQ(s->GetWindowWidth(), 1920); + } +} + +// ClearGameSpecificOverrides tests + +TEST_F(EmulatorSettingsTest, ClearGameSpecificOverridesRemovesAllGroups) { + json game; + game["General"]["neo_mode"] = true; + game["GPU"]["window_width"] = 3840; + game["Debug"]["debug_dump"] = true; + game["Input"]["cursor_state"] = 2; + game["Audio"]["audio_backend"] = 1; + game["Vulkan"]["gpu_id"] = 2; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->ClearGameSpecificOverrides(); + temp_settings->SetConfigMode(ConfigMode::Default); + + EXPECT_FALSE(temp_settings->IsNeo()); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); + EXPECT_FALSE(temp_settings->IsDebugDump()); + EXPECT_EQ(temp_settings->GetCursorState(), static_cast(HideCursorState::Idle)); + EXPECT_EQ(temp_settings->GetGpuId(), -1); +} + +TEST_F(EmulatorSettingsTest, ClearGameSpecificOverridesDoesNotTouchBaseValues) { + temp_settings->SetWindowWidth(1920); + json game; + game["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->ClearGameSpecificOverrides(); + + temp_settings->SetConfigMode(ConfigMode::Global); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1920); +} + +TEST_F(EmulatorSettingsTest, ClearGameSpecificOverrides_NoopWhenNothingLoaded) { + EXPECT_NO_THROW(temp_settings->ClearGameSpecificOverrides()); +} + +// ResetGameSpecificValue tests + +TEST_F(EmulatorSettingsTest, ResetGameSpecificValue_ClearsNamedKey) { + temp_settings->SetWindowWidth(1280); + json game; + game["GPU"]["window_width"] = 3840; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->SetConfigMode(ConfigMode::Default); + ASSERT_EQ(temp_settings->GetWindowWidth(), 3840); + + temp_settings->ResetGameSpecificValue("window_width"); + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); +} + +TEST_F(EmulatorSettingsTest, ResetGameSpecificValueOnlyAffectsTargetKey) { + json game; + game["GPU"]["window_width"] = 3840; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + + temp_settings->ResetGameSpecificValue("window_width"); + temp_settings->SetConfigMode(ConfigMode::Default); + + EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); // cleared + EXPECT_TRUE(temp_settings->IsNeo()); // still set +} + +TEST_F(EmulatorSettingsTest, ResetGameSpecificValueUnknownKeyNoOp) { + EXPECT_NO_THROW(temp_settings->ResetGameSpecificValue("does_not_exist")); +} + +// GameInstallDir tests + +TEST_F(EmulatorSettingsTest, AddGameInstallDirAddsEnabled) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + EXPECT_TRUE(temp_settings->AddGameInstallDir(dir)); + ASSERT_EQ(temp_settings->GetGameInstallDirs().size(), 1u); + EXPECT_EQ(temp_settings->GetGameInstallDirs()[0], dir); +} + +TEST_F(EmulatorSettingsTest, AddGameInstallDirRejectsDuplicate) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + temp_settings->AddGameInstallDir(dir); + EXPECT_FALSE(temp_settings->AddGameInstallDir(dir)); + EXPECT_EQ(temp_settings->GetGameInstallDirs().size(), 1u); +} + +TEST_F(EmulatorSettingsTest, RemoveGameInstallDirRemovesEntry) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + temp_settings->AddGameInstallDir(dir); + temp_settings->RemoveGameInstallDir(dir); + EXPECT_TRUE(temp_settings->GetGameInstallDirs().empty()); +} + +TEST_F(EmulatorSettingsTest, RemoveGameInstallDirNoopForMissing) { + EXPECT_NO_THROW(temp_settings->RemoveGameInstallDir("/nonexistent/path")); +} + +TEST_F(EmulatorSettingsTest, SetGameInstallDirEnabledDisablesDir) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + temp_settings->AddGameInstallDir(dir, true); + temp_settings->SetGameInstallDirEnabled(dir, false); + EXPECT_TRUE(temp_settings->GetGameInstallDirs().empty()); +} + +TEST_F(EmulatorSettingsTest, SetGameInstallDirEnabledReEnablesDir) { + fs::path dir = temp_dir->path() / "games"; + fs::create_directories(dir); + temp_settings->AddGameInstallDir(dir, false); + ASSERT_TRUE(temp_settings->GetGameInstallDirs().empty()); + temp_settings->SetGameInstallDirEnabled(dir, true); + EXPECT_EQ(temp_settings->GetGameInstallDirs().size(), 1u); +} + +TEST_F(EmulatorSettingsTest, SetAllGameInstallDirsReplacesExistingList) { + fs::path d1 = temp_dir->path() / "g1"; + fs::path d2 = temp_dir->path() / "g2"; + fs::create_directories(d1); + fs::create_directories(d2); + temp_settings->AddGameInstallDir(d1); + + temp_settings->SetAllGameInstallDirs({{d2, true}}); + ASSERT_EQ(temp_settings->GetGameInstallDirs().size(), 1u); + EXPECT_EQ(temp_settings->GetGameInstallDirs()[0], d2); +} + +TEST_F(EmulatorSettingsTest, GameInstallDirsFullRoundTripWithEnabledFlags) { + fs::path d1 = temp_dir->path() / "g1"; + fs::path d2 = temp_dir->path() / "g2"; + fs::create_directories(d1); + fs::create_directories(d2); + temp_settings->AddGameInstallDir(d1, true); + temp_settings->AddGameInstallDir(d2, false); + temp_settings->Save(); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); + + const auto& all = f->GetAllGameInstallDirs(); + ASSERT_EQ(all.size(), 2u); + EXPECT_EQ(all[0].path, d1); + EXPECT_TRUE(all[0].enabled); + EXPECT_EQ(all[1].path, d2); + EXPECT_FALSE(all[1].enabled); +} + +TEST_F(EmulatorSettingsTest, GetGameInstallDirsEnabledReflectsState) { + fs::path d1 = temp_dir->path() / "g1"; + fs::path d2 = temp_dir->path() / "g2"; + fs::create_directories(d1); + fs::create_directories(d2); + temp_settings->AddGameInstallDir(d1, true); + temp_settings->AddGameInstallDir(d2, false); + + auto enabled = temp_settings->GetGameInstallDirsEnabled(); + ASSERT_EQ(enabled.size(), 2u); + EXPECT_TRUE(enabled[0]); + EXPECT_FALSE(enabled[1]); +} + +// GetAllOverrideableKeys tests + +TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysIsNonEmpty) { + EXPECT_FALSE(temp_settings->GetAllOverrideableKeys().empty()); +} + +TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysContainsRepresentativeKeys) { + auto keys = temp_settings->GetAllOverrideableKeys(); + auto has = [&](const char* k) { return std::find(keys.begin(), keys.end(), k) != keys.end(); }; + // General + EXPECT_TRUE(has("neo_mode")); + EXPECT_TRUE(has("volume_slider")); + // GPU + EXPECT_TRUE(has("window_width")); + EXPECT_TRUE(has("null_gpu")); + EXPECT_TRUE(has("vblank_frequency")); + // Vulkan + EXPECT_TRUE(has("gpu_id")); + EXPECT_TRUE(has("pipeline_cache_enabled")); + // Debug + EXPECT_TRUE(has("debug_dump")); + EXPECT_TRUE(has("log_enabled")); + // Input + EXPECT_TRUE(has("cursor_state")); + // Audio + EXPECT_TRUE(has("audio_backend")); +} + +TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysNoDuplicates) { + auto keys = temp_settings->GetAllOverrideableKeys(); + std::vector sorted = keys; + std::sort(sorted.begin(), sorted.end()); + auto it = std::unique(sorted.begin(), sorted.end()); + EXPECT_EQ(it, sorted.end()) << "Duplicate key found in overrideable keys list"; +} + +// Per-group GetOverrideableFields tests + +TEST_F(EmulatorSettingsTest, GetGeneralOverrideableFieldsNonEmpty) { + EXPECT_FALSE(temp_settings->GetGeneralOverrideableFields().empty()); +} + +TEST_F(EmulatorSettingsTest, GetGPUOverrideableFieldsContainsWindowAndFullscreen) { + auto fields = temp_settings->GetGPUOverrideableFields(); + auto has = [&](const char* k) { + return std::any_of(fields.begin(), fields.end(), + [k](const OverrideItem& f) { return std::string(f.key) == k; }); + }; + EXPECT_TRUE(has("window_width")); + EXPECT_TRUE(has("window_height")); + EXPECT_TRUE(has("full_screen")); + EXPECT_TRUE(has("vblank_frequency")); +} + +TEST_F(EmulatorSettingsTest, GetVulkanOverrideableFieldsContainsGpuId) { + auto fields = temp_settings->GetVulkanOverrideableFields(); + bool found = std::any_of(fields.begin(), fields.end(), + [](const OverrideItem& f) { return std::string(f.key) == "gpu_id"; }); + EXPECT_TRUE(found); +} + +// Path accessors tests +TEST_F(EmulatorSettingsTest, GetHomeDirReturnsCustomWhenSet) { + fs::path dir = temp_dir->path() / "custom_home"; + fs::create_directories(dir); + temp_settings->SetHomeDir(dir); + EXPECT_EQ(temp_settings->GetHomeDir(), dir); +} +TEST_F(EmulatorSettingsTest, GetSysModulesDirFallsBackToPathUtilWhenEmpty) { + // default_value is empty; GetSysModulesDir falls back to GetUserPath(SysModuleDir) + auto result = temp_settings->GetSysModulesDir(); + EXPECT_FALSE(result.empty()); +} +TEST_F(EmulatorSettingsTest, GetFontsDirFallsBackToPathUtilWhenEmpty) { + auto result = temp_settings->GetFontsDir(); + EXPECT_FALSE(result.empty()); +} + +// edge cases tests + +TEST_F(EmulatorSettingsTest, VersionMismatchPreservesSettings) { + temp_settings->SetNeo(true); + temp_settings->SetWindowWidth(2560u); + temp_settings->Save(); + + // Force a stale version string so the mismatch branch fires + json j = ReadJson(ConfigJson()); + j["Debug"]["config_version"] = "old_hash_0000"; + WriteJson(ConfigJson(), j); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(); // triggers version-bump Save() internally + + EXPECT_TRUE(f->IsNeo()); + EXPECT_EQ(f->GetWindowWidth(), 2560u); +} + +TEST_F(EmulatorSettingsTest, DoubleGlobalLoadIsIdempotent) { + temp_settings->SetNeo(true); + temp_settings->SetWindowWidth(2560u); + temp_settings->Save(); + + auto f = std::make_shared(); + EmulatorSettingsImpl::SetInstance(f); + f->Load(""); // first — loads from disk + f->Load(""); // second — must not reset anything + + EXPECT_TRUE(f->IsNeo()); + EXPECT_EQ(f->GetWindowWidth(), 2560u); +} + +TEST_F(EmulatorSettingsTest, ConfigUsedFlagTrueWhenFileExists) { + json game; + game["General"]["neo_mode"] = true; + WriteJson(GameConfig("CUSA01234"), game); + temp_settings->Load("CUSA01234"); + EXPECT_TRUE(EmulatorState::GetInstance()->IsGameSpecifigConfigUsed()); +} + +TEST_F(EmulatorSettingsTest, ConfigUsedFlagFalseWhenFileAbsent) { + temp_settings->Load("CUSA99999"); + EXPECT_FALSE(EmulatorState::GetInstance()->IsGameSpecifigConfigUsed()); +} + +TEST_F(EmulatorSettingsTest, DestructorNoSaveIfLoadNeverCalled) { + temp_settings->SetNeo(true); + temp_settings->Save(); + auto t0 = fs::last_write_time(ConfigJson()); + + { + // Create and immediately destroy without calling Load() + auto untouched = std::make_shared(); + // destructor fires here + } + + auto t1 = fs::last_write_time(ConfigJson()); + EXPECT_EQ(t0, t1) << "Destructor wrote config.json without a prior Load()"; +} + +TEST_F(EmulatorSettingsTest, DestructorSavesAfterSuccessfulLoad) { + temp_settings->SetNeo(true); + temp_settings->Save(); + + { + auto s = std::make_shared(); + EmulatorSettingsImpl::SetInstance(s); + s->Load(); + s->SetWindowWidth(2560u); // mutate after successful load + // destructor should write this change + } + + auto verify = std::make_shared(); + EmulatorSettingsImpl::SetInstance(verify); + verify->Load(); + EXPECT_EQ(verify->GetWindowWidth(), 2560); +}