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 3d77c5800..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' }} @@ -27,7 +29,7 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@v6 - - uses: fsfe/reuse-action@v5 + - uses: fsfe/reuse-action@v6 clang-format: runs-on: ubuntu-24.04 @@ -65,6 +67,81 @@ 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 @@ -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@v6 + 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 @@ -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@v6 + - uses: actions/upload-artifact@v7 with: name: shadps4-macos-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: upload/ @@ -200,7 +277,7 @@ jobs: run: | ls -la ${{ github.workspace }}/build/shadps4 - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: shadps4-ubuntu64-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: ${{ github.workspace }}/build/shadps4 @@ -211,7 +288,7 @@ jobs: - name: Package and Upload Linux SDL artifact run: | tar cf shadps4-linux-sdl.tar.gz -C ${{github.workspace}}/build shadps4 - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: shadps4-linux-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }} path: Shadps4-sdl.AppImage diff --git a/.gitignore b/.gitignore index 683f6f0a6..7ace5e4f6 100644 --- a/.gitignore +++ b/.gitignore @@ -418,3 +418,6 @@ FodyWeavers.xsd # JetBrains .idea cmake-build-* + +# Nix Result symlink +result diff --git a/CMakeLists.txt b/CMakeLists.txt index ee6f37802..147903a7e 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) @@ -244,7 +245,7 @@ find_package(VulkanMemoryAllocator 3.1.0 CONFIG) find_package(xbyak 7.07 CONFIG) find_package(xxHash 0.8.2 MODULE) find_package(ZLIB 1.3 MODULE) -find_package(Zydis 5.0.0 CONFIG) +find_package(Zydis 5.0.0 MODULE) find_package(pugixml 1.14 CONFIG) if (APPLE) find_package(date 3.0.1 CONFIG) @@ -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 @@ -871,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") @@ -1077,6 +1091,8 @@ set(IMGUI src/imgui/imgui_config.h src/imgui/imgui_layer.h src/imgui/imgui_std.h src/imgui/imgui_texture.h + src/imgui/imgui_translations.cpp + src/imgui/imgui_translations.h src/imgui/renderer/imgui_core.cpp src/imgui/renderer/imgui_core.h src/imgui/renderer/imgui_impl_sdl3.cpp @@ -1101,6 +1117,8 @@ set(EMULATOR src/emulator.cpp src/sdl_window.cpp ) +if(NOT ENABLE_TESTS) + add_executable(shadps4 ${AUDIO_CORE} ${IMGUI} @@ -1121,7 +1139,14 @@ create_target_directory_groups(shadps4) target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG) target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 SDL3_mixer::SDL3_mixer pugixml::pugixml) -target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib) +target_link_libraries(shadps4 PRIVATE stb::headers lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz::miniz fdk-aac CLI11::CLI11 OpenAL::OpenAL Cpp_Httplib) + +if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + target_link_libraries(shadps4 PRIVATE "/usr/lib/libusb.so") + target_link_libraries(shadps4 PRIVATE "/usr/local/lib/libuuid.so") +else() + target_link_libraries(shadps4 PRIVATE libusb::usb) +endif() target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") @@ -1167,6 +1192,8 @@ if (APPLE) # Replacement for std::chrono::time_zone target_link_libraries(shadps4 PRIVATE date::date-tz epoll-shim) +elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + target_link_libraries(shadps4 PRIVATE date::date-tz epoll-shim) endif() if (WIN32) @@ -1254,3 +1281,8 @@ endif() # Install rules install(TARGETS shadps4 BUNDLE DESTINATION .) + +else() + enable_testing() + add_subdirectory(tests) +endif() diff --git a/REUSE.toml b/REUSE.toml index 22bed2a50..e8997f007 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -22,6 +22,7 @@ path = [ "documents/Screenshots/Linux/*", "documents/Screenshots/Windows/*", "externals/MoltenVK/MoltenVK_icd.json", + "flake.lock", "scripts/ps4_names.txt", "src/images/bronze.png", "src/images/gold.png", @@ -130,4 +131,4 @@ SPDX-License-Identifier = "MIT" [[annotations]] path = "src/video_core/host_shaders/fsr/*" SPDX-FileCopyrightText = "Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved." -SPDX-License-Identifier = "MIT" \ No newline at end of file +SPDX-License-Identifier = "MIT" diff --git a/documents/building-linux.md b/documents/building-linux.md index 49aacdfc9..9495a226b 100644 --- a/documents/building-linux.md +++ b/documents/building-linux.md @@ -53,6 +53,23 @@ sudo zypper install clang git cmake libasound2 libpulse-devel \ nix-shell shell.nix ``` +#### Nix Flake Development Shell +```bash +nix develop +cmake -S . -B build/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +ln -s ./build/compile_commands.json . +``` + +#### Nix Flake Build +```bash +nix build .?submodules=1#linux.debug +``` +```bash +nix build .?submodules=1#linux.release +``` +```bash +nix build .?submodules=1#linux.releaseWithDebugInfo +``` #### Other Linux distributions You can try one of two methods: diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 41a0f71c7..9e19e1404 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -210,8 +210,15 @@ endif() # libusb if (NOT TARGET libusb::usb) - add_subdirectory(ext-libusb) - add_library(libusb::usb ALIAS usb-1.0) + if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + # YOU MUST USE NATIVE LIBUSB + # using anything else will crash instantly, also freebsd will NOT like it + # no you cant vendor this libusb, its builtin on freebsd + find_package(libusb) + else() + add_subdirectory(ext-libusb) + add_library(libusb::usb ALIAS usb-1.0) + endif() endif() # Discord RPC @@ -233,25 +240,26 @@ endif() set(HWINFO_STATIC ON) add_subdirectory(hwinfo) -# Apple-only dependencies -if (APPLE) +if (APPLE OR ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") # date if (NOT TARGET date::date-tz) option(BUILD_TZ_LIB "" ON) option(USE_SYSTEM_TZ_DB "" ON) add_subdirectory(date) endif() + if (NOT TARGET epoll-shim) + add_subdirectory(epoll-shim) + endif() +endif() +# Apple-only dependencies +if (APPLE) # MoltenVK if (NOT TARGET MoltenVK) set(MVK_EXCLUDE_SPIRV_TOOLS ON) set(MVK_USE_METAL_PRIVATE_API ON) add_subdirectory(MoltenVK) endif() - - if (NOT TARGET epoll-shim) - add_subdirectory(epoll-shim) - endif() endif() #windows only diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..246cfd4e7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1774386573, + "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..9042105c5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,160 @@ +## SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +## SPDX-License-Identifier: GPL-2.0-or-later + +{ + description = "shadPS4 Nix Flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + pkgsLinux = nixpkgs.legacyPackages.x86_64-linux; + in + { + devShells.x86_64-linux.default = pkgsLinux.mkShell.override { stdenv = pkgsLinux.clangStdenv; } { + packages = with pkgsLinux; [ + clang-tools + cmake + pkg-config + vulkan-tools + + renderdoc + gef + strace + + openal + zlib.dev + libedit.dev + vulkan-headers + vulkan-utility-libraries + ffmpeg.dev + fmt.dev + glslang.dev + wayland.dev + stb + libpng.dev + libuuid + + sdl3.dev + alsa-lib + hidapi + ibus.dev + jack2.dev + libdecor.dev + libthai.dev + fribidi.dev + libxcb.dev + libGL.dev + libpulseaudio.dev + libusb1.dev + libx11.dev + libxcursor.dev + libxext + libxfixes.dev + libxi.dev + libxinerama.dev + libxkbcommon + libxrandr.dev + libxrender.dev + libxtst + pipewire.dev + libxscrnsaver + sndio + ]; + shellHook = '' + echo "Entering shadPS4 development shell!" + ''; + }; + + linux = + let + execName = "shadps4"; + nativeInputs = with pkgsLinux; [ + cmake + ninja + pkg-config + magic-enum + fmt + eudev + ]; + buildInputs = with pkgsLinux; [ + boost + cli11 + openal + nlohmann_json + vulkan-loader + vulkan-headers + vulkan-memory-allocator + toml11 + zlib + zydis + pugixml + ffmpeg + libpulseaudio + pipewire + vulkan-loader + wayland + wayland-scanner + libX11 + libxrandr + libxext + libxcursor + libxi + libxscrnsaver + libxtst + libxcb + libdecor + libxkbcommon + libGL + libuuid + ]; + + defaultFlags = [ + "-DCMAKE_INSTALL_PREFIX=$out" + ]; + in + { + debug = pkgsLinux.stdenv.mkDerivation { + pname = "${execName}"; + version = "git"; + system = "x86_64-linux"; + src = ./.; + dontStrip = true; + + nativeBuildInputs = nativeInputs; + buildInputs = buildInputs; + cmakeFlags = [ + "-DCMAKE_BUILD_TYPE=Debug" + ] ++ [defaultFlags]; + }; + release = pkgsLinux.stdenv.mkDerivation { + pname = "${execName}"; + version = "git"; + system = "x86_64-linux"; + src = ./.; + + nativeBuildInputs = nativeInputs; + buildInputs = buildInputs; + cmakeFlags = [ + "-DCMAKE_BUILD_TYPE=Release" + ] ++ [defaultFlags]; + }; + releaseWithDebugInfo = pkgsLinux.stdenv.mkDerivation { + pname = "${execName}"; + version = "git"; + system = "x86_64-linux"; + src = ./.; + dontStrip = true; + + nativeBuildInputs = nativeInputs; + buildInputs = buildInputs; + cmakeFlags = [ + "-DCMAKE_BUILD_TYPE=Release" + ] ++ [defaultFlags]; + }; + }; + }; +} diff --git a/src/common/adaptive_mutex.h b/src/common/adaptive_mutex.h index 2ab385bdb..247d4c1ec 100644 --- a/src/common/adaptive_mutex.h +++ b/src/common/adaptive_mutex.h @@ -3,7 +3,7 @@ #pragma once -#ifdef __linux__ +#if __unix__ #include #endif diff --git a/src/common/elf_info.h b/src/common/elf_info.h index b84f36ecb..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; @@ -139,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/common/signal_context.cpp b/src/common/signal_context.cpp index 112160bc8..b1ac8d96f 100644 --- a/src/common/signal_context.cpp +++ b/src/common/signal_context.cpp @@ -7,6 +7,9 @@ #ifdef _WIN32 #include +#elif defined(__FreeBSD__) +#include +#include #else #include #endif @@ -22,6 +25,16 @@ void* GetXmmPointer(void* ctx, u8 index) { #define CASE(index) \ case index: \ return (void*)(&((ucontext_t*)ctx)->uc_mcontext->__fs.__fpu_xmm##index); +#elif defined(__FreeBSD__) + // In mc_fpstate + // See for the internals of mc_fpstate[]. +#define CASE(index) \ + case index: { \ + auto& mctx = ((ucontext_t*)ctx)->uc_mcontext; \ + ASSERT(mctx.mc_fpformat == _MC_FPFMT_XMM); \ + auto* s_fpu = (struct savefpu*)(&mctx.mc_fpstate[0]); \ + return (void*)(&(s_fpu->sv_xmm[0])); \ + } #else #define CASE(index) \ case index: \ @@ -57,6 +70,8 @@ void* GetRip(void* ctx) { return (void*)((EXCEPTION_POINTERS*)ctx)->ContextRecord->Rip; #elif defined(__APPLE__) return (void*)((ucontext_t*)ctx)->uc_mcontext->__ss.__rip; +#elif defined(__FreeBSD__) + return (void*)((ucontext_t*)ctx)->uc_mcontext.mc_rip; #else return (void*)((ucontext_t*)ctx)->uc_mcontext.gregs[REG_RIP]; #endif @@ -67,6 +82,8 @@ void IncrementRip(void* ctx, u64 length) { ((EXCEPTION_POINTERS*)ctx)->ContextRecord->Rip += length; #elif defined(__APPLE__) ((ucontext_t*)ctx)->uc_mcontext->__ss.__rip += length; +#elif defined(__FreeBSD__) + ((ucontext_t*)ctx)->uc_mcontext.mc_rip += length; #else ((ucontext_t*)ctx)->uc_mcontext.gregs[REG_RIP] += length; #endif @@ -75,18 +92,16 @@ void IncrementRip(void* ctx, u64 length) { bool IsWriteError(void* ctx) { #if defined(_WIN32) return ((EXCEPTION_POINTERS*)ctx)->ExceptionRecord->ExceptionInformation[0] == 1; -#elif defined(__APPLE__) -#if defined(ARCH_X86_64) +#elif defined(__APPLE__) && defined(ARCH_X86_64) return ((ucontext_t*)ctx)->uc_mcontext->__es.__err & 0x2; -#elif defined(ARCH_ARM64) +#elif defined(__APPLE__) && defined(ARCH_ARM64) return ((ucontext_t*)ctx)->uc_mcontext->__es.__esr & 0x40; -#endif -#else -#if defined(ARCH_X86_64) +#elif defined(__FreeBSD__) && defined(ARCH_X86_64) + return ((ucontext_t*)ctx)->uc_mcontext.mc_err & 0x2; +#elif defined(ARCH_X86_64) return ((ucontext_t*)ctx)->uc_mcontext.gregs[REG_ERR] & 0x2; #else #error "Unsupported architecture" #endif -#endif } -} // namespace Common \ No newline at end of file +} // namespace Common diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index 758c7240c..4830f65a5 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,14 +606,18 @@ 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; user_size = UserSize; constexpr int protection_flags = PROT_READ | PROT_WRITE; - constexpr int map_flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED; + int map_flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED; // compiler knows its constexpr +#if !defined(__FreeBSD__) + map_flags |= MAP_NORESERVE; +#endif + #if defined(__APPLE__) && defined(ARCH_X86_64) // On ARM64 Macs, we run into limitations due to the commpage from 0xFC0000000 - 0xFFFFFFFFF // and the GPU carveout region from 0x1000000000 - 0x6FFFFFFFFF. Because this creates gaps @@ -628,7 +632,7 @@ struct AddressSpace::Impl { mmap(reinterpret_cast(USER_MIN), user_size, protection_flags, map_flags, -1, 0)); #else const auto virtual_size = system_managed_size + system_reserved_size + user_size; -#if defined(ARCH_X86_64) +#if defined(ARCH_X86_64) && !defined(__FreeBSD__) const auto virtual_base = reinterpret_cast(mmap(reinterpret_cast(SYSTEM_MANAGED_MIN), virtual_size, protection_flags, map_flags, -1, 0)); @@ -636,8 +640,10 @@ struct AddressSpace::Impl { system_reserved_base = reinterpret_cast(SYSTEM_RESERVED_MIN); user_base = reinterpret_cast(USER_MIN); #else + // FreeBSD can't stand MAP_FIXED or it may overwrite mmap() itself! // Map memory wherever possible and instruction translation can handle offsetting to the // base. + map_flags &= ~MAP_FIXED; const auto virtual_base = reinterpret_cast(mmap(nullptr, virtual_size, protection_flags, map_flags, -1, 0)); system_managed_base = virtual_base; @@ -676,8 +682,13 @@ struct AddressSpace::Impl { } shm_unlink(shm_path.c_str()); #else +#ifndef __FreeBSD__ madvise(virtual_base, virtual_size, MADV_HUGEPAGE); - +#endif + // NOTE: If you add MFD_HUGETLB or whatever, remember that FBSD will break (libc bug) + // so please, do not, add MFD_* whatever unless you ifdef it away (must be 0 for FBSD) + // using sized pages as well causes incessant vm_reclaim calls in kernel, do not use on FBSD + // under any circumstances. backing_fd = memfd_create("BackingDmem", 0); if (backing_fd < 0) { LOG_CRITICAL(Kernel_Vmm, "memfd_create failed: {}", strerror(errno)); diff --git a/src/core/debugger.cpp b/src/core/debugger.cpp index 16071ee69..b396a3ba5 100644 --- a/src/core/debugger.cpp +++ b/src/core/debugger.cpp @@ -12,7 +12,7 @@ #elif defined(__linux__) #include #include -#elif defined(__APPLE__) +#elif defined(__APPLE__) || defined(__FreeBSD__) #include #include #include @@ -48,6 +48,8 @@ bool Core::Debugger::IsDebuggerAttached() { return (info.kp_proc.p_flag & P_TRACED) != 0; } return false; +#elif defined(__FreeBSD__) + return false; #else #error "Unsupported platform" #endif @@ -66,7 +68,7 @@ void Core::Debugger::WaitForDebuggerAttach() { int Core::Debugger::GetCurrentPid() { #if defined(_WIN32) return GetCurrentProcessId(); -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) return getpid(); #else #error "Unsupported platform" @@ -88,7 +90,7 @@ void Core::Debugger::WaitForPid(int pid) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::cerr << "Waiting for process " << pid << " to exit..." << std::endl; } -#elif defined(__APPLE__) +#elif defined(__APPLE__) || defined(__FreeBSD__) while (kill(pid, 0) == 0) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::cerr << "Waiting for process " << pid << " to exit..." << std::endl; 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/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..aa474d20a 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" @@ -96,7 +95,7 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea std::scoped_lock lk{m_mutex}; path_parts.clear(); auto current_path = host_path; - while (!std::filesystem::exists(current_path)) { + while (!current_path.empty() && !std::filesystem::exists(current_path)) { // We have probably cached this if it's a folder. if (auto it = path_cache.find(current_path); it != path_cache.end()) { current_path = it->second; @@ -105,38 +104,40 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea path_parts.emplace_back(current_path.filename()); current_path = current_path.parent_path(); } - // We have found an anchor. Traverse parts we recoded and see if they - // exist in filesystem but in different case. - auto guest_path = current_path; - while (!path_parts.empty()) { - const auto part = path_parts.back(); - const auto add_match = [&](const auto& host_part) { - current_path /= host_part; - guest_path /= part; - path_cache[guest_path] = current_path; - path_parts.pop_back(); - }; - // Can happen when the mismatch is in upper folder. - if (std::filesystem::exists(current_path / part)) { - add_match(part); - continue; - } - const auto part_low = Common::ToLower(part.string()); - bool found_match = false; - for (const auto& path : std::filesystem::directory_iterator(current_path)) { - const auto candidate = path.path().filename(); - const auto filename = Common::ToLower(candidate.string()); - // Check if a filename matches in case insensitive manner. - if (filename != part_low) { + if (!current_path.empty()) { + // We have found an anchor. Traverse parts we recoded and see if they + // exist in filesystem but in different case. + auto guest_path = current_path; + while (!path_parts.empty()) { + const auto part = path_parts.back(); + const auto add_match = [&](const auto& host_part) { + current_path /= host_part; + guest_path /= part; + path_cache[guest_path] = current_path; + path_parts.pop_back(); + }; + // Can happen when the mismatch is in upper folder. + if (std::filesystem::exists(current_path / part)) { + add_match(part); continue; } - // We found a match, record the actual path in the cache. - add_match(candidate); - found_match = true; - break; - } - if (!found_match) { - return std::optional({}); + const auto part_low = Common::ToLower(part.string()); + bool found_match = false; + for (const auto& path : std::filesystem::directory_iterator(current_path)) { + const auto candidate = path.path().filename(); + const auto filename = Common::ToLower(candidate.string()); + // Check if a filename matches in case insensitive manner. + if (filename != part_low) { + continue; + } + // We found a match, record the actual path in the cache. + add_match(candidate); + found_match = true; + break; + } + if (!found_match) { + return std::optional({}); + } } } return std::optional(current_path); 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.cpp b/src/core/libraries/kernel/kernel.cpp index 6594bfab2..4d74e731b 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -298,6 +298,126 @@ s32 PS4_SYSV_ABI sceKernelGetAppInfo(s32 pid, OrbisKernelAppInfo* app_info) { return ORBIS_OK; } +// Nominally: long sysconf(int name); +u64 PS4_SYSV_ABI posix_sysconf(s32 name) { + switch (name) { + case 0: + return 0x20000; + case POSIX_SC_ARG_MAX: + return 0x588bc000; + case POSIX_SC_CHILD_MAX: + return 0x64; + case POSIX_SC_CLK_TCK: + return 0x20; + case POSIX_SC_NGROUPS_MAX: + return 0x644; + case POSIX_SC_OPEN_MAX: + return -0x1; + case POSIX_SC_JOB_CONTROL: + return 0x6; + case POSIX_SC_SAVED_IDS: + return 0x1; + case POSIX_SC_VERSION: + return 0x1; + case POSIX_SC_BC_BASE_MAX: + return 0x31069; + case POSIX_SC_BC_DIM_MAX: + return -0x1; + case POSIX_SC_BC_SCALE_MAX: + return 0x31069; + case POSIX_SC_BC_STRING_MAX: + return 0x31069; + case POSIX_SC_COLL_WEIGHTS_MAX: + return -0x1; + case POSIX_SC_EXPR_NEST_MAX: + return -0x1; + case POSIX_SC_LINE_MAX: + return 0x31069; + case POSIX_SC_RE_DUP_MAX: + return 0x31069; + case POSIX_SC_2_VERSION: + return 0x31069; + case POSIX_SC_2_C_BIND: + return 0x31069; + case POSIX_SC_2_C_DEV: + return 0x31069; + case POSIX_SC_2_CHAR_TERM: + return 0x31069; + case POSIX_SC_2_FORT_DEV: + return 0x31069; + case POSIX_SC_2_FORT_RUN: + return 0x31069; + case POSIX_SC_2_LOCALEDEF: + return -0x1; + case POSIX_SC_2_SW_DEV: + return -0x1; + case POSIX_SC_2_UPE: + return 0x0; + case POSIX_SC_STREAM_MAX: + return 0x7fffffff; + case POSIX_SC_TZNAME_MAX: + return -0x1; + case POSIX_SC_ASYNCHRONOUS_IO: + return 0x8000; + case POSIX_SC_MAPPED_FILES: + return 0x31069; + case POSIX_SC_MEMLOCK: + return 0x4000; + case POSIX_SC_MEMLOCK_RANGE: + return 0x1e; + case POSIX_SC_MEMORY_PROTECTION: + return 0x100; + case POSIX_SC_MESSAGE_PASSING: + return 0x7fffffff; + case POSIX_SC_PRIORITIZED_IO: + return -0x1; + case POSIX_SC_PRIORITY_SCHEDULING: + return -0x1; + case POSIX_SC_REALTIME_SIGNALS: + return 0x63; + case POSIX_SC_SEMAPHORES: + return 0x800; + case POSIX_SC_FSYNC: + return 0x63; + case POSIX_SC_SHARED_MEMORY_OBJECTS: + return 0x3e8; + case POSIX_SC_SYNCHRONIZED_IO: + return 0x2; + case POSIX_SC_THREAD_ATTR_STACKSIZE: + return 0x1; + case POSIX_SC_THREAD_CPUTIME: + return 0x1; + case POSIX_SC_THREAD_DESTRUCTOR_ITERATIONS: + return 0x48000; + case POSIX_SC_THREAD_KEYS_MAX: + return 0x1a078630b2dd7; + case POSIX_SC_THREAD_PRIO_INHERIT: + return -0x1; + case POSIX_SC_THREAD_PRIO_PROTECT: + return -0x1; + case POSIX_SC_THREAD_PRIORITY_SCHEDULING: + return 0x2bc; + case POSIX_SC_THREAD_PROCESS_SHARED: + return 0x2bc; + case POSIX_SC_THREAD_SAFE_FUNCTIONS: + return 0x1; + case POSIX_SC_THREAD_SPORADIC_SERVER: + return -0x1; + case POSIX_SC_THREAD_STACK_MIN: + return 0x1; + case POSIX_SC_THREAD_THREADS_MAX: + return 0x1; + case POSIX_SC_TIMEOUTS: + return -0x1; + // Manually specified + case POSIX_SC_PAGESIZE: + return posix_getpagesize(); + default: + LOG_ERROR(Lib_Kernel, "unhandled {}", name); + return 0; + } +} + void RegisterLib(Core::Loader::SymbolsResolver* sym) { service_thread = std::jthread{KernelServiceThread}; @@ -327,6 +447,10 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("9BcDykPmo1I", "libkernel", 1, "libkernel", __Error); LIB_FUNCTION("k+AXqu2-eBc", "libkernel", 1, "libkernel", posix_getpagesize); LIB_FUNCTION("k+AXqu2-eBc", "libScePosix", 1, "libkernel", posix_getpagesize); + + LIB_FUNCTION("mkawd0NA9ts", "libkernel", 1, "libkernel", posix_sysconf); + LIB_FUNCTION("mkawd0NA9ts", "libScePosix", 1, "libkernel", posix_sysconf); + LIB_FUNCTION("NWtTN10cJzE", "libSceLibcInternalExt", 1, "libSceLibcInternal", sceLibcHeapGetTraceInfo); diff --git a/src/core/libraries/kernel/kernel.h b/src/core/libraries/kernel/kernel.h index ce6446129..ab5729f3b 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(); @@ -80,4 +80,126 @@ struct OrbisKernelAppInfo { void RegisterLib(Core::Loader::SymbolsResolver* sym); +constexpr u32 POSIX_SC_ARG_MAX = 1; +constexpr u32 POSIX_SC_CHILD_MAX = 2; +constexpr u32 POSIX_SC_CLK_TCK = 3; +constexpr u32 POSIX_SC_NGROUPS_MAX = 4; +constexpr u32 POSIX_SC_OPEN_MAX = 5; +constexpr u32 POSIX_SC_JOB_CONTROL = 6; +constexpr u32 POSIX_SC_SAVED_IDS = 7; +constexpr u32 POSIX_SC_VERSION = 8; +constexpr u32 POSIX_SC_BC_BASE_MAX = 9; +constexpr u32 POSIX_SC_BC_DIM_MAX = 10; +constexpr u32 POSIX_SC_BC_SCALE_MAX = 11; +constexpr u32 POSIX_SC_BC_STRING_MAX = 12; +constexpr u32 POSIX_SC_COLL_WEIGHTS_MAX = 13; +constexpr u32 POSIX_SC_EXPR_NEST_MAX = 14; +constexpr u32 POSIX_SC_LINE_MAX = 15; +constexpr u32 POSIX_SC_RE_DUP_MAX = 16; +constexpr u32 POSIX_SC_2_VERSION = 17; +constexpr u32 POSIX_SC_2_C_BIND = 18; +constexpr u32 POSIX_SC_2_C_DEV = 19; +constexpr u32 POSIX_SC_2_CHAR_TERM = 20; +constexpr u32 POSIX_SC_2_FORT_DEV = 21; +constexpr u32 POSIX_SC_2_FORT_RUN = 22; +constexpr u32 POSIX_SC_2_LOCALEDEF = 23; +constexpr u32 POSIX_SC_2_SW_DEV = 24; +constexpr u32 POSIX_SC_2_UPE = 25; +constexpr u32 POSIX_SC_STREAM_MAX = 26; +constexpr u32 POSIX_SC_TZNAME_MAX = 27; +constexpr u32 POSIX_SC_ASYNCHRONOUS_IO = 28; +constexpr u32 POSIX_SC_MAPPED_FILES = 29; +constexpr u32 POSIX_SC_MEMLOCK = 30; +constexpr u32 POSIX_SC_MEMLOCK_RANGE = 31; +constexpr u32 POSIX_SC_MEMORY_PROTECTION = 32; +constexpr u32 POSIX_SC_MESSAGE_PASSING = 33; +constexpr u32 POSIX_SC_PRIORITIZED_IO = 34; +constexpr u32 POSIX_SC_PRIORITY_SCHEDULING = 35; +constexpr u32 POSIX_SC_REALTIME_SIGNALS = 36; +constexpr u32 POSIX_SC_SEMAPHORES = 37; +constexpr u32 POSIX_SC_FSYNC = 38; +constexpr u32 POSIX_SC_SHARED_MEMORY_OBJECTS = 39; +constexpr u32 POSIX_SC_SYNCHRONIZED_IO = 40; +constexpr u32 POSIX_SC_TIMERS = 41; +constexpr u32 POSIX_SC_AIO_LISTIO_MAX = 42; +constexpr u32 POSIX_SC_AIO_MAX = 43; +constexpr u32 POSIX_SC_AIO_PRIO_DELTA_MAX = 44; +constexpr u32 POSIX_SC_DELAYTIMER_MAX = 45; +constexpr u32 POSIX_SC_MQ_OPEN_MAX = 46; +constexpr u32 POSIX_SC_PAGESIZE = 47; +constexpr u32 POSIX_SC_RTSIG_MAX = 48; +constexpr u32 POSIX_SC_SEM_NSEMS_MAX = 49; +constexpr u32 POSIX_SC_SEM_VALUE_MAX = 50; +constexpr u32 POSIX_SC_SIGQUEUE_MAX = 51; +constexpr u32 POSIX_SC_TIMER_MAX = 52; +constexpr u32 POSIX_SC_2_PBS = 59; +constexpr u32 POSIX_SC_2_PBS_ACCOUNTING = 60; +constexpr u32 POSIX_SC_2_PBS_CHECKPOINT = 61; +constexpr u32 POSIX_SC_2_PBS_LOCATE = 62; +constexpr u32 POSIX_SC_2_PBS_MESSAGE = 63; +constexpr u32 POSIX_SC_2_PBS_TRACK = 64; +constexpr u32 POSIX_SC_ADVISORY_INFO = 65; +constexpr u32 POSIX_SC_BARRIERS = 66; +constexpr u32 POSIX_SC_CLOCK_SELECTION = 67; +constexpr u32 POSIX_SC_CPUTIME = 68; +constexpr u32 POSIX_SC_FILE_LOCKING = 69; +constexpr u32 POSIX_SC_GETGR_R_SIZE_MAX = 70; +constexpr u32 POSIX_SC_GETPW_R_SIZE_MAX = 71; +constexpr u32 POSIX_SC_HOST_NAME_MAX = 72; +constexpr u32 POSIX_SC_LOGIN_NAME_MAX = 73; +constexpr u32 POSIX_SC_MONOTONIC_CLOCK = 74; +constexpr u32 POSIX_SC_MQ_PRIO_MAX = 75; +constexpr u32 POSIX_SC_READER_WRITER_LOCKS = 76; +constexpr u32 POSIX_SC_REGEXP = 77; +constexpr u32 POSIX_SC_SHELL = 78; +constexpr u32 POSIX_SC_SPAWN = 79; +constexpr u32 POSIX_SC_SPIN_LOCKS = 80; +constexpr u32 POSIX_SC_SPORADIC_SERVER = 81; +constexpr u32 POSIX_SC_THREAD_ATTR_STACKADDR = 82; +constexpr u32 POSIX_SC_THREAD_ATTR_STACKSIZE = 83; +constexpr u32 POSIX_SC_THREAD_CPUTIME = 84; +constexpr u32 POSIX_SC_THREAD_DESTRUCTOR_ITERATIONS = 85; +constexpr u32 POSIX_SC_THREAD_KEYS_MAX = 86; +constexpr u32 POSIX_SC_THREAD_PRIO_INHERIT = 87; +constexpr u32 POSIX_SC_THREAD_PRIO_PROTECT = 88; +constexpr u32 POSIX_SC_THREAD_PRIORITY_SCHEDULING = 89; +constexpr u32 POSIX_SC_THREAD_PROCESS_SHARED = 90; +constexpr u32 POSIX_SC_THREAD_SAFE_FUNCTIONS = 91; +constexpr u32 POSIX_SC_THREAD_SPORADIC_SERVER = 92; +constexpr u32 POSIX_SC_THREAD_STACK_MIN = 93; +constexpr u32 POSIX_SC_THREAD_THREADS_MAX = 94; +constexpr u32 POSIX_SC_TIMEOUTS = 95; +constexpr u32 POSIX_SC_THREADS = 96; +constexpr u32 POSIX_SC_TRACE = 97; +constexpr u32 POSIX_SC_TRACE_EVENT_FILTER = 98; +constexpr u32 POSIX_SC_TRACE_INHERIT = 99; +constexpr u32 POSIX_SC_TRACE_LOG = 100; +constexpr u32 POSIX_SC_TTY_NAME_MAX = 101; +constexpr u32 POSIX_SC_TYPED_MEMORY_OBJECTS = 102; +constexpr u32 POSIX_SC_V6_ILP32_OFF32 = 103; +constexpr u32 POSIX_SC_V6_ILP32_OFFBIG = 104; +constexpr u32 POSIX_SC_V6_LP64_OFF64 = 105; +constexpr u32 POSIX_SC_V6_LPBIG_OFFBIG = 106; +constexpr u32 POSIX_SC_IPV6 = 118; +constexpr u32 POSIX_SC_RAW_SOCKETS = 119; +constexpr u32 POSIX_SC_SYMLOOP_MAX = 120; +constexpr u32 POSIX_SC_ATEXIT_MAX = 107; +constexpr u32 POSIX_SC_IOV_MAX = 56; +constexpr u32 POSIX_SC_XOPEN_CRYPT = 108; +constexpr u32 POSIX_SC_XOPEN_ENH_I18N = 109; +constexpr u32 POSIX_SC_XOPEN_LEGACY = 110; +constexpr u32 POSIX_SC_XOPEN_REALTIME = 111; +constexpr u32 POSIX_SC_XOPEN_REALTIME_THREADS = 112; +constexpr u32 POSIX_SC_XOPEN_SHM = 113; +constexpr u32 POSIX_SC_XOPEN_STREAMS = 114; +constexpr u32 POSIX_SC_XOPEN_UNIX = 115; +constexpr u32 POSIX_SC_XOPEN_VERSION = 116; +constexpr u32 POSIX_SC_XOPEN_XCU_VERSION = 117; +constexpr u32 POSIX_SC_NPROCESSORS_CONF = 57; +constexpr u32 POSIX_SC_NPROCESSORS_ONLN = 58; +constexpr u32 POSIX_SC_CPUSET_SIZE = 122; +constexpr u32 POSIX_SC_UEXTERR_MAXLEN = 123; +constexpr u32 POSIX_SC_NSIG = 124; +constexpr u32 POSIX_SC_PHYS_PAGES = 121; + } // namespace Libraries::Kernel 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 e2fd032f5..f9a062da7 100644 --- a/src/core/libraries/kernel/threads/exception.cpp +++ b/src/core/libraries/kernel/threads/exception.cpp @@ -215,6 +215,28 @@ void SigactionHandler(int native_signum, siginfo_t* inf, ucontext_t* raw_context ctx.uc_mcontext.mc_gs = regs.__gs; ctx.uc_mcontext.mc_rip = regs.__rip; ctx.uc_mcontext.mc_addr = reinterpret_cast(inf->si_addr); +#elif defined(__FreeBSD__) + const auto& regs = raw_context->uc_mcontext; + ctx.uc_mcontext.mc_r8 = regs.mc_r8; + ctx.uc_mcontext.mc_r9 = regs.mc_r9; + ctx.uc_mcontext.mc_r10 = regs.mc_r10; + ctx.uc_mcontext.mc_r11 = regs.mc_r11; + ctx.uc_mcontext.mc_r12 = regs.mc_r12; + ctx.uc_mcontext.mc_r13 = regs.mc_r13; + ctx.uc_mcontext.mc_r14 = regs.mc_r14; + ctx.uc_mcontext.mc_r15 = regs.mc_r15; + ctx.uc_mcontext.mc_rdi = regs.mc_rdi; + ctx.uc_mcontext.mc_rsi = regs.mc_rsi; + ctx.uc_mcontext.mc_rbp = regs.mc_rbp; + ctx.uc_mcontext.mc_rbx = regs.mc_rbx; + ctx.uc_mcontext.mc_rdx = regs.mc_rdx; + ctx.uc_mcontext.mc_rax = regs.mc_rax; + ctx.uc_mcontext.mc_rcx = regs.mc_rcx; + ctx.uc_mcontext.mc_rsp = regs.mc_rsp; + ctx.uc_mcontext.mc_fs = regs.mc_fs; + ctx.uc_mcontext.mc_gs = regs.mc_gs; + ctx.uc_mcontext.mc_rip = regs.mc_rip; + ctx.uc_mcontext.mc_addr = uint64_t(regs.mc_addr); #else const auto& regs = raw_context->uc_mcontext.gregs; ctx.uc_mcontext.mc_r8 = regs[REG_R8]; @@ -286,6 +308,28 @@ bool PS4_SYSV_ABI posix_sigisemptyset(Sigset* s) { return s->bits[0] == 0 && s->bits[1] == 0; } +s32 PS4_SYSV_ABI posix_sigalstack(const OrbisKernelExceptionHandlerStack* ss, + OrbisKernelExceptionHandlerStack* old_ss) { +#ifdef __unix__ + stack_t native_ss{}; + if (ss) { + native_ss.ss_sp = ss->ss_sp; + native_ss.ss_flags = ss->ss_flags; + native_ss.ss_size = ss->ss_size; + } + stack_t native_old_ss{}; + sigaltstack(&native_ss, &native_old_ss); + if (old_ss) { + old_ss->ss_sp = native_old_ss.ss_sp; + old_ss->ss_flags = native_old_ss.ss_flags; + old_ss->ss_size = native_old_ss.ss_size; + } +#else + LOG_ERROR(Lib_Kernel, "(stubbed)"); +#endif + return ORBIS_OK; +} + s32 PS4_SYSV_ABI posix_sigaction(s32 sig, Sigaction* act, Sigaction* oact) { if (sig < 1 || sig > 128 || sig == POSIX_SIGTHR || sig == POSIX_SIGKILL || sig == POSIX_SIGSTOP) { @@ -303,7 +347,7 @@ s32 PS4_SYSV_ABI posix_sigaction(s32 sig, Sigaction* act, Sigaction* oact) { *__Error() = POSIX_EINVAL; return ORBIS_FAIL; } -#ifndef __APPLE__ +#if !defined(__APPLE__) && !defined(__FreeBSD__) if (native_sig >= __SIGRTMIN && native_sig < SIGRTMIN) { LOG_ERROR(Lib_Kernel, "Guest is attempting to use the HLE libc-reserved signal {}!", sig); *__Error() = POSIX_EINVAL; @@ -473,9 +517,12 @@ void RegisterException(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("KiJEPEWRyUY", "libkernel", 1, "libkernel", posix_sigaction); LIB_FUNCTION("+F7C-hdk7+E", "libkernel", 1, "libkernel", posix_sigemptyset); LIB_FUNCTION("yH-uQW3LbX0", "libkernel", 1, "libkernel", posix_pthread_kill); + LIB_FUNCTION("sHziAegVp74", "libkernel", 1, "libkernel", posix_sigalstack); + LIB_FUNCTION("KiJEPEWRyUY", "libScePosix", 1, "libkernel", posix_sigaction); LIB_FUNCTION("+F7C-hdk7+E", "libScePosix", 1, "libkernel", posix_sigemptyset); LIB_FUNCTION("yH-uQW3LbX0", "libScePosix", 1, "libkernel", posix_pthread_kill); + LIB_FUNCTION("sHziAegVp74", "libScePosix", 1, "libkernel", posix_sigalstack); } } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/threads/exception.h b/src/core/libraries/kernel/threads/exception.h index c07242c1d..f9655404a 100644 --- a/src/core/libraries/kernel/threads/exception.h +++ b/src/core/libraries/kernel/threads/exception.h @@ -12,6 +12,11 @@ class SymbolsResolver; namespace Libraries::Kernel { using OrbisKernelExceptionHandler = PS4_SYSV_ABI void (*)(int, void*); +struct OrbisKernelExceptionHandlerStack { + void* ss_sp; + int ss_flags; + size_t ss_size; +}; constexpr s32 POSIX_SIGHUP = 1; constexpr s32 POSIX_SIGINT = 2; @@ -47,7 +52,7 @@ constexpr s32 POSIX_SIGUSR2 = 31; constexpr s32 POSIX_SIGTHR = 32; constexpr s32 POSIX_SIGLIBRT = 33; -#ifdef __linux__ +#if defined(__linux__) || defined(__FreeBSD__) constexpr s32 _SIGEMT = 128; constexpr s32 _SIGINFO = 129; #elif !defined(_WIN32) diff --git a/src/core/libraries/kernel/time.cpp b/src/core/libraries/kernel/time.cpp index 3e1648b98..2967dd4bf 100644 --- a/src/core/libraries/kernel/time.cpp +++ b/src/core/libraries/kernel/time.cpp @@ -17,7 +17,7 @@ #include #include "common/ntapi.h" #else -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__FreeBSD__) #include #endif #include @@ -501,7 +501,7 @@ s32 PS4_SYSV_ABI sceKernelConvertUtcToLocaltime(time_t time, time_t* local_time, *dst_sec = res == TIME_ZONE_ID_DAYLIGHT ? -_dstbias : 0; } #else -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(__FreeBSD__) // std::chrono::current_zone() not available yet. const auto* time_zone = date::current_zone(); #else 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 ac35c4b63..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" @@ -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..baf45b64f 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -663,10 +663,12 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or return ORBIS_NET_ERROR_EBADF; } +#ifndef __FreeBSD__ epoll_event native_event = {.events = ConvertEpollEventsIn(event->events), .data = {.fd = id}}; ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_ADD, *native_handle, &native_event) == 0); epoll->events.emplace_back(id, *event); +#endif break; } case Core::FileSys::FileType::Resolver: { @@ -711,10 +713,12 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or return ORBIS_NET_ERROR_EBADF; } +#ifndef __FreeBSD__ epoll_event native_event = {.events = ConvertEpollEventsIn(event->events), .data = {.fd = id}}; ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_MOD, *native_handle, &native_event) == 0); *it = {id, *event}; +#endif break; } default: @@ -752,9 +756,10 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or *sceNetErrnoLoc() = ORBIS_NET_EBADF; return ORBIS_NET_ERROR_EBADF; } - +#ifndef __FreeBSD__ ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_DEL, *native_handle, nullptr) == 0); epoll->events.erase(it); +#endif break; } case Core::FileSys::FileType::Resolver: { @@ -810,6 +815,9 @@ int PS4_SYSV_ABI sceNetEpollDestroy(OrbisNetId epollid) { int PS4_SYSV_ABI sceNetEpollWait(OrbisNetId epollid, OrbisNetEpollEvent* events, int maxevents, int timeout) { +#ifdef __FreeBSD__ + return 0; +#else auto file = FDTable::Instance()->GetEpoll(epollid); if (!file) { *sceNetErrnoLoc() = ORBIS_NET_EBADF; @@ -836,7 +844,6 @@ int PS4_SYSV_ABI sceNetEpollWait(OrbisNetId epollid, OrbisNetEpollEvent* events, } int i = 0; - if (result < 0) { LOG_ERROR(Lib_Net, "epoll_wait failed with {}", Common::GetLastErrorMsg()); switch (errno) { @@ -905,8 +912,8 @@ int PS4_SYSV_ABI sceNetEpollWait(OrbisNetId epollid, OrbisNetEpollEvent* events, ++i; } } - return i; +#endif } int* PS4_SYSV_ABI sceNetErrnoLoc() { @@ -1447,7 +1454,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_epoll.cpp b/src/core/libraries/network/net_epoll.cpp index e64c8ac64..4f1b521ce 100644 --- a/src/core/libraries/network/net_epoll.cpp +++ b/src/core/libraries/network/net_epoll.cpp @@ -10,12 +10,14 @@ namespace Libraries::Net { u32 ConvertEpollEventsIn(u32 orbis_events) { u32 ret = 0; +#ifndef __FreeBSD__ if ((orbis_events & ORBIS_NET_EPOLLIN) != 0) { ret |= EPOLLIN; } if ((orbis_events & ORBIS_NET_EPOLLOUT) != 0) { ret |= EPOLLOUT; } +#endif return ret; } @@ -23,6 +25,7 @@ u32 ConvertEpollEventsIn(u32 orbis_events) { u32 ConvertEpollEventsOut(u32 epoll_events) { u32 ret = 0; +#ifndef __FreeBSD__ if ((epoll_events & EPOLLIN) != 0) { ret |= ORBIS_NET_EPOLLIN; } @@ -35,6 +38,7 @@ u32 ConvertEpollEventsOut(u32 epoll_events) { if ((epoll_events & EPOLLHUP) != 0) { ret |= ORBIS_NET_EPOLLHUP; } +#endif return ret; } diff --git a/src/core/libraries/network/net_epoll.h b/src/core/libraries/network/net_epoll.h index 37555484d..17716b36e 100644 --- a/src/core/libraries/network/net_epoll.h +++ b/src/core/libraries/network/net_epoll.h @@ -14,7 +14,8 @@ #include #endif -#if defined(__linux__) || defined(__APPLE__) +#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) +// ADD libepoll-shim if using freebsd! #include #include #endif @@ -82,4 +83,4 @@ private: std::mutex m_mutex; }; -} // namespace Libraries::Net \ No newline at end of file +} // namespace Libraries::Net 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/net_util.cpp b/src/core/libraries/network/net_util.cpp index de47f7bc1..fd0ed877b 100644 --- a/src/core/libraries/network/net_util.cpp +++ b/src/core/libraries/network/net_util.cpp @@ -25,7 +25,7 @@ typedef int net_socket; #include #include #endif -#if __linux__ +#if defined(__linux__) || defined(__FreeBSD__) #include #include #include @@ -81,6 +81,8 @@ bool NetUtilInternal::RetrieveEthernetAddr() { } freeifaddrs(ifap); } +#elif defined(__FreeBSD__) + // todo #else ifreq ifr; ifconf ifc; @@ -226,7 +228,8 @@ bool NetUtilInternal::RetrieveDefaultGateway() { inet_ntop(AF_INET, gateAddr, str, sizeof(str)); this->default_gateway = str; return true; - +#elif defined(__FreeBSD__) + return true; #else std::ifstream route{"/proc/net/route"}; std::string line; @@ -398,4 +401,4 @@ int NetUtilInternal::ResolveHostname(const char* hostname, Libraries::Net::Orbis return ret; } -} // namespace NetUtil \ No newline at end of file +} // namespace NetUtil 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/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..449ee775a 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 = + EmulatorSettings.GetHomeDir() / std::to_string(user_id) / "trophy" / (np_comm_id + ".xml"); + ctx.xml_dir = trophy_base / "Xml"; + ctx.icons_dir = trophy_base / "Icons"; + ctx.trophy_xml_path = GetTrophyXmlPath(ctx.xml_dir, EmulatorSettings.GetConsoleLanguage()); + LOG_INFO(Lib_NpTrophy, "New context = {}, user_id = {} service label = {}", *context, user_id, service_label); @@ -206,6 +264,10 @@ int PS4_SYSV_ABI sceNpTrophyDestroyContext(OrbisNpTrophyContext context) { return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } + if (!trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + ContextKey contextkey = trophy_contexts[contextId]; trophy_contexts.erase(contextId); contexts_internal.erase(contextkey); @@ -251,12 +313,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGameIcon(OrbisNpTrophyContext context, OrbisNpTro return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto icon_file = trophy_dir / trophy_folder / "Icons" / "ICON0.PNG"; + const auto& ctx = contexts_internal[contextkey]; + + auto icon_file = ctx.icons_dir / "ICON0.PNG"; Common::FS::IOFile icon(icon_file, Common::FS::FileAccessMode::Read); if (!icon.IsOpen()) { @@ -304,12 +364,11 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.trophy_xml_path; + const auto& trophy_save_file = ctx.xml_save_file; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -336,7 +395,18 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro if (node_name == "group") game_info.num_groups++; + } + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); if (node_name == "trophy") { bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); std::string_view current_trophy_grade = node.attribute("ttype").value(); @@ -368,8 +438,9 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro data->unlocked_silver = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER]; data->unlocked_bronze = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE]; - // maybe this should be 1 instead of 100? - data->progress_percentage = 100; + data->progress_percentage = (game_info.num_trophies > 0) + ? (game_info.unlocked_trophies * 100u) / game_info.num_trophies + : 0; return ORBIS_OK; } @@ -411,12 +482,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; } ContextKey contextkey = trophy_contexts[contextId]; - char trophy_folder[9]; - snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second); - - const auto trophy_dir = - Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles"; - auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML"; + const auto& ctx = contexts_internal[contextkey]; + if (!ctx.registered) + return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED; + const auto& trophy_file = ctx.trophy_xml_path; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str()); @@ -450,7 +519,18 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr details->group_id = groupId; data->group_id = groupId; + } + pugi::xml_document save_doc; + pugi::xml_parse_result save_result = save_doc.load_file(ctx.xml_save_file.native().c_str()); + + if (!save_result) { + LOG_ERROR(Lib_NpTrophy, "Failed to parse user trophy xml : {}", result.description()); + return ORBIS_OK; + } + auto save_trophyconf = save_doc.child("trophyconf"); + for (const pugi::xml_node& node : save_trophyconf.children()) { + std::string_view node_name = node.name(); if (node_name == "trophy") { bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool(); std::string_view current_trophy_grade = node.attribute("ttype").value(); @@ -484,15 +564,84 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr data->unlocked_silver = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER]; data->unlocked_bronze = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE]; - // maybe this should be 1 instead of 100? - data->progress_percentage = 100; + data->progress_percentage = + (group_info.num_trophies > 0) + ? (group_info.unlocked_trophies * 100u) / group_info.num_trophies + : 0; return ORBIS_OK; } int PS4_SYSV_ABI sceNpTrophyGetTrophyIcon(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle, OrbisNpTrophyId trophyId, void* buffer, u64* size) { - LOG_ERROR(Lib_NpTrophy, "(STUBBED) called"); + if (size == nullptr) + return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT; + + if (trophyId < 0 || trophyId >= ORBIS_NP_TROPHY_NUM_MAX) + return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID; + + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + + if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + + Common::SlotId contextId; + contextId.index = context - 1; + if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) { + return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } + + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size() || + !trophy_handles.is_allocated({static_cast(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_internal.cpp b/src/core/libraries/sysmodule/sysmodule_internal.cpp index def410e25..56e130289 100644 --- a/src/core/libraries/sysmodule/sysmodule_internal.cpp +++ b/src/core/libraries/sysmodule/sysmodule_internal.cpp @@ -2,9 +2,9 @@ // 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/file_sys/fs.h" #include "core/libraries/disc_map/disc_map.h" #include "core/libraries/font/font.h" @@ -108,7 +108,7 @@ bool validateModuleId(s32 id) { } // Cannot load debug modules on retail hardware. - if (isDebugModule(id) && !Config::isDevKitConsole()) { + if (isDebugModule(id) && !EmulatorSettings.IsDevKit()) { return ORBIS_SYSMODULE_INVALID_ID; } @@ -154,7 +154,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { std::string mod_name = std::string(mod.name); // libSceGnmDriver case - if (index == 0xd && Config::isDevKitConsole()) { + 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"); @@ -168,7 +168,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { } // libSceVrTracker case - if (index == 0xb3 && Config::isDevKitConsole()) { + if (index == 0xb3 && EmulatorSettings.IsDevKit()) { mod_name.append("_debug"); } @@ -178,7 +178,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { // PS4 Pro running in enhanced mode mod_name.append("ForNeoMode"); } else if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsNeo) != 0 && - Config::isNeoModeConsole()) { + EmulatorSettings.IsNeo()) { // PS4 Pro running in base mode mod_name.append("ForNeo"); } @@ -188,7 +188,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) { // 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 = Config::getSysModulesPath(); + 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)) { @@ -299,7 +299,7 @@ s32 loadModule(s32 id, s32 argc, const void* argv, s32* res_out) { 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 ((!Config::isDevKitConsole() && + if ((!EmulatorSettings.IsDevKit() && g_modules_array[mod_index].flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0) { continue; } @@ -361,7 +361,7 @@ s32 unloadModule(s32 id, s32 argc, const void* argv, s32* res_out, bool is_inter 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 && - !Config::isDevKitConsole()) { + !EmulatorSettings.IsDevKit()) { continue; } @@ -398,7 +398,7 @@ s32 preloadModulesForLibkernel() { // These are skipped unless this console is a devkit. if ((module_index == 0x12 || module_index == 0x1e || module_index == 0x24 || module_index == 0x26) && - !Config::isDevKitConsole()) { + !EmulatorSettings.IsDevKit()) { continue; } @@ -409,13 +409,13 @@ s32 preloadModulesForLibkernel() { // libSceDbgAssist is skipped on non-testkit consoles. // For now, stub check to non-devkit. - if (module_index == 0x23 && !Config::isDevKitConsole()) { + if (module_index == 0x23 && !EmulatorSettings.IsDevKit()) { continue; } // libSceRazorCpu, skipped for old non-devkit consoles. if (module_index == 0x25 && sdk_ver < Common::ElfInfo::FW_45 && - !Config::isDevKitConsole()) { + !EmulatorSettings.IsDevKit()) { continue; } 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 3f410e926..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,6 +12,7 @@ #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" @@ -61,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(); } diff --git a/src/core/linker.h b/src/core/linker.h index 3cb59d9ee..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*; 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/tls.cpp b/src/core/tls.cpp index 8b926cb39..f87248114 100644 --- a/src/core/tls.cpp +++ b/src/core/tls.cpp @@ -10,6 +10,8 @@ #ifdef _WIN32 #include +#elif defined(__FreeBSD__) +#include #elif defined(__APPLE__) && defined(ARCH_X86_64) #include #include @@ -157,12 +159,17 @@ Tcb* GetTcbBase() { #elif defined(ARCH_X86_64) -// Other POSIX x86_64 - +// Linux x86_64 +#if defined(__FreeBSD__) +void SetTcbBase(void* image_address) { + amd64_set_gsbase(image_address); +} +#else void SetTcbBase(void* image_address) { const int ret = syscall(SYS_arch_prctl, ARCH_SET_GS, (unsigned long)image_address); ASSERT_MSG(ret == 0, "Failed to set GS base: errno {}", errno); } +#endif Tcb* GetTcbBase() { return Libraries::Kernel::g_curthread->tcb; 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 cc862f8ab..616a21ed1 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,6 +28,7 @@ #include "common/singleton.h" #include "core/debugger.h" #include "core/devtools/widget/module_list.h" +#include "core/emulator_settings.h" #include "core/emulator_state.h" #include "core/file_format/psf.h" #include "core/file_format/trp.h" @@ -38,6 +39,7 @@ #include "core/libraries/save_data/save_backup.h" #include "core/linker.h" #include "core/memory.h" +#include "core/user_settings.h" #include "emulator.h" #include "video_core/cache_storage.h" #include "video_core/renderdoc.h" @@ -50,6 +52,7 @@ #include #include #endif +#include Frontend::WindowSDL* g_window = nullptr; @@ -196,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(); @@ -226,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; @@ -287,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 @@ -296,15 +306,30 @@ 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 = EmulatorSettings.GetHomeDir() / + std::to_string(user.user_id) / "trophy" / + (npCommId + ".xml"); + if (!std::filesystem::exists(user_trophy_file)) { + auto temp = user_trophy_file.parent_path(); + std::filesystem::create_directories(temp); + std::error_code discard; + std::filesystem::copy_file(trophyDir / "Xml" / "TROPCONF.XML", user_trophy_file, + discard); + } + } + index++; } } @@ -328,8 +353,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(); @@ -360,7 +386,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); } @@ -399,7 +425,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, #ifdef ENABLE_DISCORD_RPC // Discord RPC - if (Config::getEnableDiscordRPC()) { + if (EmulatorSettings.IsDiscordRPCEnabled()) { auto* rpc = Common::Singleton::Instance(); if (rpc->getRPCEnabled() == false) { rpc->init(); @@ -509,7 +535,7 @@ void Emulator::Restart(std::filesystem::path eboot_path, CloseHandle(pi.hProcess); CloseHandle(pi.hThread); -#elif defined(__APPLE__) || defined(__linux__) +#elif defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__) std::vector argv; // Emulator executable 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/imgui_translations.cpp b/src/imgui/imgui_translations.cpp new file mode 100644 index 000000000..608f980c3 --- /dev/null +++ b/src/imgui/imgui_translations.cpp @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/emulator_settings.h" +#include "imgui_translations.h" + +namespace ImguiTranslate { + +const std::map> langMap = { + {0, JapaneseMap}, + // {1, EnglishUsMap}, - not used + {2, FrenchMap}, + {3, SpanishMap}, + {4, GermanMap}, + {5, ItalianMap}, + {6, DutchMap}, + {7, PortugesePtMap}, + {8, RussianMap}, + {9, KoreanMap}, + {10, ChineseTraditionalMap}, + {11, ChineseSimplifiedMap}, + {12, FinnishMap}, + {13, SwedishMap}, + {14, DanishMap}, + {15, NorwegianMap}, + {16, PolishMap}, + {17, PortugeseBrMap}, + // {18, "English (UK)"}, - not used + {19, TurkishMap}, + {20, SpanishLatinAmericanMap}, + {21, ArabicMap}, + {22, FrenchCanadaMap}, + {23, CzechMap}, + {24, HungarianMap}, + {25, GreekMap}, + {26, RomanianMap}, + {27, ThaiMap}, + {28, VietnameseMap}, + {29, IndonesianMap}, + {30, UkranianMap}, +}; + +std::string tr(std::string input) { + // since we're coding in English + if (EmulatorSettings.GetConsoleLanguage() == 1 || EmulatorSettings.GetConsoleLanguage() == 18) + return input; + + const std::map translationTable = + langMap.at(EmulatorSettings.GetConsoleLanguage()); + + if (!translationTable.contains(input)) { + return input; + } + + return translationTable.at(input); +} + +} // namespace ImguiTranslate diff --git a/src/imgui/imgui_translations.h b/src/imgui/imgui_translations.h new file mode 100644 index 000000000..d6844b759 --- /dev/null +++ b/src/imgui/imgui_translations.h @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +namespace ImguiTranslate { + +std::string tr(std::string input); + +///////////// ImGui Translation Tables + +// disable clang line limits for ease of translation +// clang-format off + +const std::map JapaneseMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map FrenchMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map FrenchCanadaMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map SpanishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map SpanishLatinAmericanMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map GermanMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ItalianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map DutchMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map PortugesePtMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map PortugeseBrMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map RussianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map KoreanMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ChineseTraditionalMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ChineseSimplifiedMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map FinnishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map SwedishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map DanishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map NorwegianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map PolishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map TurkishMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ArabicMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map CzechMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map HungarianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map GreekMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map RomanianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map ThaiMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map VietnameseMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map IndonesianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +const std::map UkranianMap = { + {"Trophy Earned", "Trophy Earned"}, +}; + +// clang-format on + +///////////// End ImGui Translation Tables + +} // namespace ImguiTranslate 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 d2804ee62..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; } @@ -160,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; @@ -170,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(); @@ -186,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..060197533 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)) { + LOG_ERROR(Input, "Failed to initialize SDL camera subsystem: {}", SDL_GetError()); + } SDL_InitSubSystem(SDL_INIT_AUDIO); SDL_PropertiesID props = SDL_CreateProperties(); @@ -320,18 +130,19 @@ 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; window_info.render_surface = SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); -#elif defined(SDL_PLATFORM_LINUX) +#elif defined(SDL_PLATFORM_LINUX) || defined(__FreeBSD__) + // SDL doesn't have a platform define for FreeBSD AAAAAAAAAA if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) { window_info.type = WindowSystemType::X11; window_info.display_connection = SDL_GetPointerProperty( @@ -350,11 +161,11 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(window)); #endif // input handler init-s - Input::ControllerOutput::SetControllerOutputController(controller); Input::ControllerOutput::LinkJoystickAxes(); Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); + controllers.TryOpenSDLControllers(); - if (Config::getBackgroundControllerInput()) { + if (EmulatorSettings.IsBackgroundControllerInput()) { SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); } } @@ -394,37 +205,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 +240,7 @@ void WindowSDL::WaitEvent() { } break; case SDL_EVENT_CHANGE_CONTROLLER: - controller->GetEngine()->Init(); + UNREACHABLE_MSG("todo"); break; case SDL_EVENT_TOGGLE_SIMPLE_FPS: Overlay::ToggleSimpleFps(); @@ -471,6 +261,29 @@ void WindowSDL::WaitEvent() { Input::ToggleMouseModeTo(Input::MouseMode::Touchpad)); SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), false); break; + case SDL_EVENT_ADD_VIRTUAL_USER: + for (int i = 0; i < 4; i++) { + if (controllers[i]->user_id == -1) { + auto u = UserManagement.GetUserByPlayerIndex(i + 1); + if (!u) { + break; + } + controllers[i]->user_id = u->user_id; + UserManagement.LoginUser(u, i + 1); + break; + } + } + break; + case SDL_EVENT_REMOVE_VIRTUAL_USER: + LOG_INFO(Input, "Remove user"); + for (int i = 3; i >= 0; i--) { + if (controllers[i]->user_id != -1) { + UserManagement.LogoutUser(UserManagement.GetUserByID(controllers[i]->user_id)); + controllers[i]->user_id = -1; + break; + } + } + break; case SDL_EVENT_RDOC_CAPTURE: VideoCore::TriggerCapture(); break; @@ -480,8 +293,10 @@ void WindowSDL::WaitEvent() { } void WindowSDL::InitTimers() { - SDL_AddTimer(4, &PollGyroAndAccel, controller); - SDL_AddTimer(33, Input::MousePolling, (void*)controller); + for (int i = 0; i < 4; ++i) { + SDL_AddTimer(4, &PollController, controllers[i]); + } + SDL_AddTimer(33, Input::MousePolling, (void*)controllers[0]); } void WindowSDL::RequestKeyboard() { @@ -549,10 +364,44 @@ void WindowSDL::OnGamepadEvent(const SDL_Event* event) { // as it would break the entire touchpad handling // You can still bind other things to it though if (event->gbutton.button == SDL_GAMEPAD_BUTTON_TOUCHPAD) { - controller->Button(0, OrbisPadButtonDataOffset::TouchPad, input_down); + controllers[controllers.GetGamepadIndexFromJoystickId(event->gbutton.which)]->Button( + OrbisPadButtonDataOffset::TouchPad, input_down); return; } + u8 gamepad; + + switch (event->type) { + case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: + switch ((SDL_SensorType)event->gsensor.sensor) { + case SDL_SENSOR_GYRO: + gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which); + if (gamepad < 5) { + controllers[gamepad]->UpdateGyro(event->gsensor.data); + } + break; + case SDL_SENSOR_ACCEL: + gamepad = controllers.GetGamepadIndexFromJoystickId(event->gsensor.which); + if (gamepad < 5) { + controllers[gamepad]->UpdateAcceleration(event->gsensor.data); + } + break; + default: + break; + } + return; + case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN: + case SDL_EVENT_GAMEPAD_TOUCHPAD_UP: + case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: + controllers[controllers.GetGamepadIndexFromJoystickId(event->gtouchpad.which)] + ->SetTouchpadState(event->gtouchpad.finger, + event->type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP, event->gtouchpad.x, + event->gtouchpad.y); + return; + default: + break; + } + // add/remove it from the list bool inputs_changed = Input::UpdatePressedKeys(input_event); 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 1055bf081..6155bdb29 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp @@ -351,6 +351,15 @@ Id EmitBufferAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id ad &Sirit::Module::OpAtomicCompareExchange); } +Id EmitBufferAtomicFCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id cmp_value) { + const auto u32_value = ctx.OpBitcast(ctx.U32[1], value); + const auto u32_cmp = ctx.OpBitcast(ctx.U32[1], cmp_value); + const auto result = BufferAtomicU32CmpSwap(ctx, inst, handle, address, u32_value, u32_cmp, + &Sirit::Module::OpAtomicCompareExchange); + return ctx.OpBitcast(ctx.F32[1], result); +} + Id EmitImageAtomicIAdd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value) { return ImageAtomicU32(ctx, inst, handle, coords, value, &Sirit::Module::OpAtomicIAdd); } 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_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 110cbf5fa..3b1a364f2 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -108,6 +108,8 @@ Id EmitBufferAtomicXor32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addres Id EmitBufferAtomicSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, Id cmp_value); +Id EmitBufferAtomicFCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id cmp_value); Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, u32 index); Id EmitGetAttributeU32(EmitContext& ctx, IR::Attribute attr, u32 comp); void EmitSetAttribute(EmitContext& ctx, IR::Attribute attr, Id value, u32 comp); diff --git a/src/shader_recompiler/frontend/translate/translate.cpp b/src/shader_recompiler/frontend/translate/translate.cpp index 611070a86..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" @@ -569,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/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index 0d9e8f220..85ce2311a 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -116,6 +116,8 @@ void Translator::EmitVectorMemory(const GcnInst& inst) { return BUFFER_ATOMIC(AtomicOp::Fmin, inst); case Opcode::BUFFER_ATOMIC_FMAX: return BUFFER_ATOMIC(AtomicOp::Fmax, inst); + case Opcode::BUFFER_ATOMIC_FCMPSWAP: + return BUFFER_ATOMIC(AtomicOp::FCmpSwap, inst); // MIMG // Image load operations @@ -379,6 +381,10 @@ void Translator::BUFFER_ATOMIC(AtomicOp op, const GcnInst& inst) { const IR::Value cmp_val = ir.GetVectorReg(vdata + 1); return ir.BufferAtomicCmpSwap(handle, address, vdata_val, cmp_val, buffer_info); } + case AtomicOp::FCmpSwap: { + const IR::Value cmp_val = ir.GetVectorReg(vdata + 1); + return ir.BufferAtomicFCmpSwap(handle, address, vdata_val, cmp_val, buffer_info); + } case AtomicOp::Add: return ir.BufferAtomicIAdd(handle, address, vdata_val, buffer_info); case AtomicOp::Smin: diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index 835a6186d..29005af54 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -627,6 +627,11 @@ Value IREmitter::BufferAtomicCmpSwap(const Value& handle, const Value& address, return Inst(Opcode::BufferAtomicCmpSwap32, Flags{info}, handle, address, vdata, cmp_value); } +Value IREmitter::BufferAtomicFCmpSwap(const Value& handle, const Value& address, const Value& vdata, + const Value& cmp_value, BufferInstInfo info) { + return Inst(Opcode::BufferAtomicFCmpSwap32, Flags{info}, handle, address, vdata, cmp_value); +} + U32 IREmitter::DataAppend(const U32& counter) { return Inst(Opcode::DataAppend, counter, Imm32(0)); } diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index adc8f5fb1..ec0edfed4 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -166,6 +166,9 @@ public: [[nodiscard]] Value BufferAtomicCmpSwap(const Value& handle, const Value& address, const Value& value, const Value& cmp_value, BufferInstInfo info); + [[nodiscard]] Value BufferAtomicFCmpSwap(const Value& handle, const Value& address, + const Value& value, const Value& cmp_value, + BufferInstInfo info); [[nodiscard]] U32 DataAppend(const U32& counter); [[nodiscard]] U32 DataConsume(const U32& counter); diff --git a/src/shader_recompiler/ir/microinstruction.cpp b/src/shader_recompiler/ir/microinstruction.cpp index cd0131770..837b9601e 100644 --- a/src/shader_recompiler/ir/microinstruction.cpp +++ b/src/shader_recompiler/ir/microinstruction.cpp @@ -82,6 +82,7 @@ bool Inst::MayHaveSideEffects() const noexcept { case Opcode::BufferAtomicXor32: case Opcode::BufferAtomicSwap32: case Opcode::BufferAtomicCmpSwap32: + case Opcode::BufferAtomicFCmpSwap32: case Opcode::DataAppend: case Opcode::DataConsume: case Opcode::WriteSharedU16: diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index ab395e8f9..735a64383 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -150,6 +150,7 @@ OPCODE(BufferAtomicOr32, U32, Opaq OPCODE(BufferAtomicXor32, U32, Opaque, Opaque, U32, ) OPCODE(BufferAtomicSwap32, U32, Opaque, Opaque, U32, ) OPCODE(BufferAtomicCmpSwap32, U32, Opaque, Opaque, U32, U32, ) +OPCODE(BufferAtomicFCmpSwap32, F32, Opaque, Opaque, F32, F32, ) // Vector utility OPCODE(CompositeConstructU32x2, U32x2, U32, U32, ) 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/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 3b7888ab3..0b256a349 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -39,6 +39,7 @@ bool IsBufferAtomic(const IR::Inst& inst) { case IR::Opcode::BufferAtomicXor32: case IR::Opcode::BufferAtomicSwap32: case IR::Opcode::BufferAtomicCmpSwap32: + case IR::Opcode::BufferAtomicFCmpSwap32: return true; default: return false; 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/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..742268753 100644 --- a/src/video_core/buffer_cache/region_manager.h +++ b/src/video_core/buffer_cache/region_manager.h @@ -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 #pragma once -#include "common/config.h" #include "common/div_ceil.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" -#ifdef __linux__ +#ifdef __unix__ #include "common/adaptive_mutex.h" #else #include "common/spin_lock.h" @@ -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> PAGE_BITS; - const u64 page_end = Common::DivCeil(addr + size, PAGE_SIZE); + size_t page = addr >> PM_PAGE_BITS; + const u64 page_end = Common::DivCeil(addr + size, PM_PAGE_SIZE); // Acquire locks for the range of pages const auto lock_start = locks.begin() + (page / PAGES_PER_LOCK); @@ -239,15 +239,15 @@ struct PageManager::Impl { if (range_bytes > 0) { RENDERER_TRACE; // Perform pending (un)protect action - Protect(range_begin << PAGE_BITS, range_bytes, perms); + Protect(range_begin << PM_PAGE_BITS, range_bytes, perms); range_bytes = 0; potential_range_bytes = 0; } }; // Iterate requested pages - const u64 aligned_addr = page << PAGE_BITS; - const u64 aligned_end = page_end << PAGE_BITS; + const u64 aligned_addr = page << PM_PAGE_BITS; + const u64 aligned_end = page_end << PM_PAGE_BITS; if (!rasterizer->IsMapped(aligned_addr, aligned_end - aligned_addr)) { LOG_WARNING(Render, "Tracking memory region {:#x} - {:#x} which is not fully GPU mapped.", @@ -266,7 +266,7 @@ struct PageManager::Impl { perms = new_perms; } else if (range_bytes != 0) { // If the protection did not change, extend the potential range - potential_range_bytes += PAGE_SIZE; + potential_range_bytes += PM_PAGE_SIZE; } // Only start a new range if the page must be (un)protected @@ -274,7 +274,7 @@ struct PageManager::Impl { if (range_bytes == 0) { // Start a new potential range range_begin = page; - potential_range_bytes = PAGE_SIZE; + potential_range_bytes = PM_PAGE_SIZE; } // Extend current range up to potential range range_bytes = potential_range_bytes; @@ -293,12 +293,12 @@ struct PageManager::Impl { if (start_range.second == end_range.second) { // if all pages are contiguous, use the regular UpdatePageWatchers - const VAddr start_addr = base_addr + (start_range.first << PAGE_BITS); - const u64 size = (start_range.second - start_range.first) << PAGE_BITS; + const VAddr start_addr = base_addr + (start_range.first << PM_PAGE_BITS); + const u64 size = (start_range.second - start_range.first) << PM_PAGE_BITS; return UpdatePageWatchers(start_addr, size); } - size_t base_page = (base_addr >> PAGE_BITS); + size_t base_page = (base_addr >> PM_PAGE_BITS); ASSERT(base_page % PAGES_PER_LOCK == 0); std::scoped_lock lk(locks[base_page / PAGES_PER_LOCK]); auto perms = cached_pages[base_page + start_range.first].Perms(); @@ -310,7 +310,7 @@ struct PageManager::Impl { if (range_bytes > 0) { RENDERER_TRACE; // Perform pending (un)protect action - Protect((range_begin << PAGE_BITS), range_bytes, perms); + Protect((range_begin << PM_PAGE_BITS), range_bytes, perms); range_bytes = 0; potential_range_bytes = 0; } @@ -331,7 +331,7 @@ struct PageManager::Impl { perms = new_perms; } else if (range_bytes != 0) { // If the protection did not change, extend the potential range - potential_range_bytes += PAGE_SIZE; + potential_range_bytes += PM_PAGE_SIZE; } // If the page is not being updated, skip it @@ -344,7 +344,7 @@ struct PageManager::Impl { if (range_bytes == 0) { // Start a new potential range range_begin = base_page + page; - potential_range_bytes = PAGE_SIZE; + potential_range_bytes = PM_PAGE_SIZE; } // Extend current rango up to potential range range_bytes = potential_range_bytes; @@ -356,7 +356,7 @@ struct PageManager::Impl { } std::array cached_pages{}; -#ifdef __linux__ +#ifdef PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP using LockType = Common::AdaptiveMutex; #else using LockType = Common::SpinLock; diff --git a/src/video_core/page_manager.h b/src/video_core/page_manager.h index 4ca41cb43..fb53f7c98 100644 --- a/src/video_core/page_manager.h +++ b/src/video_core/page_manager.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "common/alignment.h" #include "common/types.h" @@ -15,9 +16,10 @@ class Rasterizer; namespace VideoCore { class PageManager { + // PAGE_SIZE and PAGE_BITS conflicts with machine/param.h definitions on freebsd! // Use the same page size as the tracker. - static constexpr size_t PAGE_BITS = TRACKER_PAGE_BITS; - static constexpr size_t PAGE_SIZE = TRACKER_BYTES_PER_PAGE; + static constexpr size_t PM_PAGE_BITS = TRACKER_PAGE_BITS; + static constexpr size_t PM_PAGE_SIZE = TRACKER_BYTES_PER_PAGE; // Keep the lock granularity the same as region granularity. (since each regions has // itself a lock) @@ -43,12 +45,12 @@ public: /// Returns page aligned address. static constexpr VAddr GetPageAddr(VAddr addr) { - return Common::AlignDown(addr, PAGE_SIZE); + return Common::AlignDown(addr, PM_PAGE_SIZE); } /// Returns address of the next page. static constexpr VAddr GetNextPageAddr(VAddr addr) { - return Common::AlignUp(addr + 1, PAGE_SIZE); + return Common::AlignUp(addr + 1, PM_PAGE_SIZE); } private: diff --git a/src/video_core/renderdoc.cpp b/src/video_core/renderdoc.cpp index 4cf2ddd53..b02752212 100644 --- a/src/video_core/renderdoc.cpp +++ b/src/video_core/renderdoc.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/renderdoc.h" #include @@ -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_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 80af19372..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); @@ -1313,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(); @@ -1324,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(); @@ -1333,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(); @@ -1345,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); +}