From 2bb20e46509210b517b50694a4ff65e80c858f6a Mon Sep 17 00:00:00 2001 From: Kravickas Date: Fri, 20 Mar 2026 12:43:41 +0100 Subject: [PATCH 1/5] waw fix (#4154) --- src/video_core/texture_cache/image.cpp | 27 +++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index e458f5cb3..44ddd55c6 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -247,13 +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 || - static_cast(dst_mask & - (vk::AccessFlagBits2::eTransferWrite | - vk::AccessFlagBits2::eShaderWrite | - vk::AccessFlagBits2::eColorAttachmentWrite | - vk::AccessFlagBits2::eDepthStencilAttachmentWrite | - vk::AccessFlagBits2::eMemoryWrite))) { + 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, @@ -283,11 +281,10 @@ Image::Barriers Image::GetBarriers(vk::ImageLayout dst_layout, vk::AccessFlags2 subresource_states.clear(); } } else { // Full resource transition - constexpr auto write_flags = - vk::AccessFlagBits2::eTransferWrite | vk::AccessFlagBits2::eShaderWrite | - vk::AccessFlagBits2::eColorAttachmentWrite | - vk::AccessFlagBits2::eDepthStencilAttachmentWrite | vk::AccessFlagBits2::eMemoryWrite; - const bool is_write = static_cast(dst_mask & write_flags); + 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 {}; } @@ -385,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; } @@ -655,8 +654,6 @@ void Image::CopyImageWithBuffer(Image& src_image, vk::Buffer buffer, u64 offset) cmdbuf.copyBufferToImage(buffer, GetImage(), vk::ImageLayout::eTransferDstOptimal, buffer_copies); - - // Match CopyImage: transition to general so shaders can sample the result. Transit(vk::ImageLayout::eGeneral, vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); } @@ -698,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, From 0a722d69e63f49e122a42e3c9654195781bfe333 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 21 Mar 2026 05:02:17 -0500 Subject: [PATCH 2/5] Only iterate to next VMA if we're past the current VMA. (#4158) It's possible we're merging with a later memory area. If that occurred here, we would end up iterating past where we need to be, which then messes up logic. --- src/core/memory.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 9d26142ce..768fb3c90 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -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++; + } } } From 08168dc38656d326e055e15d5737d76a2c201cb7 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 21 Mar 2026 22:26:36 +0200 Subject: [PATCH 3/5] New config mode (part1 of 0.15.1 branch series) (#4145) * using new emulator_settings * the default user is now just player one * transfer install, addon dirs * fix load custom config issue --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> --- CMakeLists.txt | 6 + src/common/logging/backend.cpp | 16 +- src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + src/common/path_util.cpp | 1 + src/common/path_util.h | 2 + src/common/serdes.h | 3 +- src/core/address_space.cpp | 8 +- src/core/devtools/layer.cpp | 16 +- src/core/devtools/widget/frame_graph.cpp | 6 +- src/core/devtools/widget/module_list.h | 6 +- src/core/devtools/widget/shader_list.cpp | 8 +- src/core/emulator_settings.cpp | 647 ++++++++++++++++++ src/core/emulator_settings.h | 636 +++++++++++++++++ src/core/ipc/ipc.cpp | 4 +- .../libraries/app_content/app_content.cpp | 6 +- src/core/libraries/audio/sdl_audio_in.cpp | 4 +- src/core/libraries/audio/sdl_audio_out.cpp | 14 +- src/core/libraries/gnmdriver/gnmdriver.cpp | 6 +- src/core/libraries/kernel/process.cpp | 8 +- src/core/libraries/network/net.cpp | 2 +- src/core/libraries/network/net_ctl_obj.cpp | 14 +- src/core/libraries/network/netctl.cpp | 12 +- src/core/libraries/np/np_auth.cpp | 6 +- src/core/libraries/np/np_matching2.cpp | 4 +- src/core/libraries/np/np_web_api2.cpp | 12 +- src/core/libraries/np/np_web_api_internal.cpp | 8 +- src/core/libraries/np/trophy_ui.cpp | 12 +- src/core/libraries/system/systemservice.cpp | 8 +- src/core/libraries/usbd/usbd.cpp | 12 +- src/core/libraries/videoout/driver.cpp | 7 +- src/core/libraries/videoout/video_out.cpp | 8 +- src/core/linker.cpp | 4 +- src/core/memory.cpp | 8 +- src/core/module.h | 6 +- src/core/user_manager.cpp | 177 +++++ src/core/user_manager.h | 58 ++ src/core/user_settings.cpp | 110 +++ src/core/user_settings.h | 46 ++ src/emulator.cpp | 69 +- src/imgui/renderer/imgui_core.cpp | 8 +- src/imgui/renderer/imgui_impl_sdl3.cpp | 14 +- src/imgui/renderer/texture_manager.cpp | 6 +- src/main.cpp | 29 +- .../spirv/emit_spirv_context_get_set.cpp | 6 +- .../frontend/translate/translate.cpp | 6 +- .../passes/flatten_extended_userdata_pass.cpp | 7 +- .../ir/passes/shader_info_collection_pass.cpp | 6 +- src/shader_recompiler/ir/program.cpp | 5 +- src/video_core/amdgpu/liverpool.cpp | 12 +- src/video_core/buffer_cache/memory_tracker.h | 5 +- src/video_core/buffer_cache/region_manager.h | 8 +- src/video_core/cache_storage.cpp | 28 +- src/video_core/renderdoc.cpp | 8 +- .../renderer_vulkan/host_passes/fsr_pass.cpp | 8 +- .../renderer_vulkan/host_passes/pp_pass.cpp | 8 +- .../renderer_vulkan/vk_pipeline_cache.cpp | 14 +- .../vk_pipeline_serialization.cpp | 4 +- .../renderer_vulkan/vk_platform.cpp | 12 +- .../renderer_vulkan/vk_presenter.cpp | 22 +- .../renderer_vulkan/vk_rasterizer.cpp | 22 +- .../renderer_vulkan/vk_swapchain.cpp | 10 +- 62 files changed, 1967 insertions(+), 263 deletions(-) create mode 100644 src/core/emulator_settings.cpp create mode 100644 src/core/emulator_settings.h create mode 100644 src/core/user_manager.cpp create mode 100644 src/core/user_manager.h create mode 100644 src/core/user_settings.cpp create mode 100644 src/core/user_settings.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ee6f37802..ab204b7a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -871,6 +871,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") 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..f2597603e 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -164,6 +164,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(Input) \ CLS(Tty) \ CLS(KeyManager) \ + CLS(EmuSettings) \ 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..4c6e53453 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -132,6 +132,7 @@ enum class Class : u8 { Input, ///< Input emulation Tty, ///< Debug output from emu KeyManager, ///< Key management system + EmuSettings, /// Emulator settings system Count ///< Total number of logging classes }; diff --git a/src/common/path_util.cpp b/src/common/path_util.cpp index 5d37990ff..103f17d29 100644 --- a/src/common/path_util.cpp +++ b/src/common/path_util.cpp @@ -129,6 +129,7 @@ static auto UserPaths = [] { create_path(PathType::CustomConfigs, user_dir / CUSTOM_CONFIGS); create_path(PathType::CacheDir, user_dir / CACHE_DIR); create_path(PathType::FontsDir, user_dir / FONTS_DIR); + create_path(PathType::HomeDir, user_dir / HOME_DIR); std::ofstream notice_file(user_dir / CUSTOM_TROPHY / "Notice.txt"); if (notice_file.is_open()) { diff --git a/src/common/path_util.h b/src/common/path_util.h index 434f77b0d..485c72270 100644 --- a/src/common/path_util.h +++ b/src/common/path_util.h @@ -26,6 +26,7 @@ enum class PathType { CustomConfigs, // Where custom files for different games are stored. CacheDir, // Where pipeline and shader cache is stored. FontsDir, // Where dumped system fonts are stored. + HomeDir, // PS4 home directory }; constexpr auto PORTABLE_DIR = "user"; @@ -46,6 +47,7 @@ constexpr auto CUSTOM_TROPHY = "custom_trophy"; constexpr auto CUSTOM_CONFIGS = "custom_configs"; constexpr auto CACHE_DIR = "cache"; constexpr auto FONTS_DIR = "fonts"; +constexpr auto HOME_DIR = "home"; // Filenames constexpr auto LOG_FILE = "shad_log.txt"; diff --git a/src/common/serdes.h b/src/common/serdes.h index a36fed4d3..f91a0ace8 100644 --- a/src/common/serdes.h +++ b/src/common/serdes.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -7,6 +7,7 @@ #include "common/types.h" #include +#include namespace Serialization { diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index 758c7240c..ca3d52042 100644 --- a/src/core/address_space.cpp +++ b/src/core/address_space.cpp @@ -1,14 +1,14 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/alignment.h" #include "common/arch.h" #include "common/assert.h" -#include "common/config.h" #include "common/elf_info.h" #include "common/error.h" #include "core/address_space.h" +#include "core/emulator_settings.h" #include "core/libraries/kernel/memory.h" #include "core/memory.h" #include "libraries/error_codes.h" @@ -187,7 +187,7 @@ struct AddressSpace::Impl { user_size = supported_user_max - USER_MIN - 1; // Increase BackingSize to account for config options. - BackingSize += Config::getExtraDmemInMbytes() * 1_MB; + BackingSize += EmulatorSettings.GetExtraDmemInMBytes() * 1_MB; // Allocate backing file that represents the total physical memory. backing_handle = CreateFileMapping2(INVALID_HANDLE_VALUE, nullptr, FILE_MAP_ALL_ACCESS, @@ -606,7 +606,7 @@ enum PosixPageProtection { struct AddressSpace::Impl { Impl() { - BackingSize += Config::getExtraDmemInMbytes() * 1_MB; + BackingSize += EmulatorSettings.GetExtraDmemInMBytes() * 1_MB; // Allocate virtual address placeholder for our address space. system_managed_size = SystemManagedSize; system_reserved_size = SystemReservedSize; diff --git a/src/core/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..c1c0342ea --- /dev/null +++ b/src/core/emulator_settings.cpp @@ -0,0 +1,647 @@ +// 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) { + j = p.string(); + } + static void from_json(const json& j, std::filesystem::path& p) { + p = j.get(); + } +}; +} // 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(EmuSettings, "No game-specific overrides applied"); + return; + } + LOG_DEBUG(EmuSettings, "Game-specific overrides applied:"); + for (const auto& k : changed) + LOG_DEBUG(EmuSettings, " * {}", k); +} + +// ── Singleton ──────────────────────────────────────────────────────── +EmulatorSettingsImpl::EmulatorSettingsImpl() = default; + +EmulatorSettingsImpl::~EmulatorSettingsImpl() { + 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(EmuSettings, "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(EmuSettings, "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(EmuSettings, "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(EmuSettings, "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(EmuSettings, "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(EmuSettings, "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(EmuSettings, "Global config loaded successfully"); + } else { + if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / + "config.toml")) { + SDL_MessageBoxButtonData btns[2]{ + {0, 0, "No"}, + {0, 1, "Yes"}, + }; + 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()) { + Save(); + return true; + } else { + SDL_ShowSimpleMessageBox(0, "Config Migration", + "Error transferring settings, exiting.", + nullptr); + std::quick_exit(1); + } + } + } + LOG_DEBUG(EmuSettings, "Global config not found - using defaults"); + SetDefaultValues(); + Save(); + } + if (GetConfigVersion() != Common::g_scm_rev) { + Save(); + } + 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(EmuSettings, "Applying game config: {}", gamePath.string()); + + if (!std::filesystem::exists(gamePath)) { + LOG_DEBUG(EmuSettings, "No game-specific config found for {}", serial); + return false; + } + + std::ifstream in(gamePath); + if (!in) { + LOG_ERROR(EmuSettings, "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(EmuSettings, "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(EmuSettings, "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(EmuSettings, "Failed to transfer addon 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; +} \ No newline at end of file diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h new file mode 100644 index 000000000..0490aba77 --- /dev/null +++ b/src/core/emulator_settings.h @@ -0,0 +1,636 @@ +// 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(EmuSettings, "[make_override] Processing key: {}", key); + LOG_DEBUG(EmuSettings, "[make_override] Entry JSON: {}", entry.dump()); + Struct* obj = reinterpret_cast(base); + Setting& dst = obj->*member; + try { + T newValue = entry.get(); + LOG_DEBUG(EmuSettings, "[make_override] Parsed value: {}", newValue); + LOG_DEBUG(EmuSettings, "[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(EmuSettings, "[make_override] Recorded change: {}", oss.str()); + } + dst.game_specific_value = newValue; + LOG_DEBUG(EmuSettings, "[make_override] Successfully updated {}", key); + } catch (const std::exception& e) { + LOG_ERROR(EmuSettings, "[make_override] ERROR parsing {}: {}", key, e.what()); + LOG_ERROR(EmuSettings, "[make_override] Entry was: {}", entry.dump()); + LOG_ERROR(EmuSettings, "[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}; + + 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 < 60) { + m_gpu.vblank_frequency.value = 60; + } + return m_gpu.vblank_frequency.value; + } + void SetVblankFrequency(const u32& v) { + if (v < 60) { + m_gpu.vblank_frequency.value = 60; + } else { + m_gpu.vblank_frequency.value = v; + } + } + + // 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/ipc/ipc.cpp b/src/core/ipc/ipc.cpp index aab3e7de5..489c34646 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; 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/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/gnmdriver/gnmdriver.cpp b/src/core/libraries/gnmdriver/gnmdriver.cpp index 1993d8cd7..1bb034dd0 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" @@ -2874,7 +2874,7 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { sdk_version = 0; } - if (Config::copyGPUCmdBuffers()) { + if (EmulatorSettings.IsCopyGpuBuffers()) { liverpool->ReserveCopyBufferSpace(); } 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/network/net.cpp b/src/core/libraries/network/net.cpp index ca75ad394..6bf4764c4 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -1447,7 +1447,7 @@ int PS4_SYSV_ABI sceNetResolverStartNtoa(OrbisNetId resolverid, const char* host return ORBIS_NET_ERROR_EBADF; } - if (!Config::getIsConnectedToNetwork()) { + if (!EmulatorSettings.IsConnectedToNetwork()) { *sceNetErrnoLoc() = ORBIS_NET_RESOLVER_ENODNS; file->resolver->resolution_error = ORBIS_NET_ERROR_RESOLVER_ENODNS; return ORBIS_NET_ERROR_RESOLVER_ENODNS; diff --git a/src/core/libraries/network/net_ctl_obj.cpp b/src/core/libraries/network/net_ctl_obj.cpp index a4081cd11..5eb6403c2 100644 --- a/src/core/libraries/network/net_ctl_obj.cpp +++ b/src/core/libraries/network/net_ctl_obj.cpp @@ -1,9 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include -#include "common/config.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/network/net_ctl_codes.h" #include "core/libraries/network/net_ctl_obj.h" #include "core/tls.h" @@ -46,8 +46,9 @@ s32 NetCtlInternal::RegisterNpToolkitCallback(OrbisNetCtlCallbackForNpToolkit fu void NetCtlInternal::CheckCallback() { std::scoped_lock lock{m_mutex}; - const auto event = Config::getIsConnectedToNetwork() ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED - : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; + const auto event = EmulatorSettings.IsConnectedToNetwork() + ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED + : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; for (const auto [func, arg] : callbacks) { if (func != nullptr) { func(event, arg); @@ -57,8 +58,9 @@ void NetCtlInternal::CheckCallback() { void NetCtlInternal::CheckNpToolkitCallback() { std::scoped_lock lock{m_mutex}; - const auto event = Config::getIsConnectedToNetwork() ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED - : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; + const auto event = EmulatorSettings.IsConnectedToNetwork() + ? ORBIS_NET_CTL_EVENT_TYPE_IPOBTAINED + : ORBIS_NET_CTL_EVENT_TYPE_DISCONNECTED; for (const auto [func, arg] : nptool_callbacks) { if (func != nullptr) { func(event, arg); diff --git a/src/core/libraries/network/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_matching2.cpp b/src/core/libraries/np/np_matching2.cpp index 423b84257..bf9b7b7d0 100644 --- a/src/core/libraries/np/np_matching2.cpp +++ b/src/core/libraries/np/np_matching2.cpp @@ -5,7 +5,7 @@ #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_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..6ca24b6d5 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 @@ -13,9 +13,9 @@ #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 +36,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 +98,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 +284,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/system/systemservice.cpp b/src/core/libraries/system/systemservice.cpp index ce5542fc8..f03c7e7cb 100644 --- a/src/core/libraries/system/systemservice.cpp +++ b/src/core/libraries/system/systemservice.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 #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 +18,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 +1918,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/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/memory.cpp b/src/core/memory.cpp index 768fb3c90..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); 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/user_manager.cpp b/src/core/user_manager.cpp new file mode 100644 index 000000000..9d0829fc3 --- /dev/null +++ b/src/core/user_manager.cpp @@ -0,0 +1,177 @@ +// 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; +} + +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..77f612016 --- /dev/null +++ b/src/core/user_manager.h @@ -0,0 +1,58 @@ +// 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; + + 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..a2142dd9a --- /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(EmuSettings, "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(EmuSettings, "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(EmuSettings, "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(EmuSettings, "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(EmuSettings, "User settings loaded successfully"); + return true; + } catch (const std::exception& e) { + LOG_ERROR(EmuSettings, "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..5206a309d 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" @@ -197,18 +197,16 @@ 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); - + EmulatorSettings.Load(id); if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / - (id + ".toml"))) { + (id + ".json"))) { EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(true); } else { EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(false); } // 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(); @@ -227,31 +225,35 @@ void Emulator::Run(std::filesystem::path file, std::vector args, 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")); + Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".json")); LOG_INFO(Config, "Game-specific config exists: {}", has_game_config); - 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; @@ -328,8 +330,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(), controller, + window_title); g_window = window.get(); @@ -360,7 +363,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 +402,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(); 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..388c23b03 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(); 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/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/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/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/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/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..a760dd596 100644 --- a/src/video_core/buffer_cache/region_manager.h +++ b/src/video_core/buffer_cache/region_manager.h @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "common/config.h" #include "common/div_ceil.h" #include "common/logging/log.h" +#include "core/emulator_settings.h" #ifdef __linux__ #include "common/adaptive_mutex.h" @@ -95,7 +95,7 @@ public: } if constexpr (type == Type::CPU) { UpdateProtection(); - } else if (Config::getReadbacksMode() == Config::GpuReadbacksMode::Precise) { + } else if (EmulatorSettings.GetReadbacksMode() == GpuReadbacksMode::Precise) { UpdateProtection(); } } @@ -126,7 +126,7 @@ public: bits.UnsetRange(start_page, end_page); if constexpr (type == Type::CPU) { UpdateProtection(); - } else if (Config::getReadbacksMode() != Config::GpuReadbacksMode::Disabled) { + } else if (EmulatorSettings.GetReadbacksMode() != GpuReadbacksMode::Disabled) { UpdateProtection(); } } diff --git a/src/video_core/cache_storage.cpp b/src/video_core/cache_storage.cpp index 1c46a4cf5..8d6abf9b5 100644 --- a/src/video_core/cache_storage.cpp +++ b/src/video_core/cache_storage.cpp @@ -1,11 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "common/config.h" #include "common/elf_info.h" #include "common/io_file.h" #include "common/polyfill_thread.h" #include "common/thread.h" +#include "core/emulator_settings.h" #include "video_core/cache_storage.h" #include "video_core/renderer_vulkan/vk_instance.h" @@ -95,7 +95,7 @@ void DataBase::Open() { const auto& game_info = Common::ElfInfo::Instance(); using namespace Common::FS; - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { mz_zip_zero_struct(&zip_ar); cache_path = GetUserPath(PathType::CacheDir) / @@ -128,7 +128,7 @@ void DataBase::Close() { io_worker.request_stop(); io_worker.join(); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { mz_zip_writer_finalize_archive(&zip_ar); mz_zip_writer_end(&zip_ar); } @@ -142,7 +142,7 @@ bool WriteVector(const BlobType type, std::filesystem::path&& path_, std::vector auto request = std::packaged_task{[=]() { auto path{path_}; path.replace_extension(GetBlobFileExtension(type)); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { ASSERT_MSG(!ar_is_read_only, "The archive is read-only. Did you forget to call `FinishPreload`?"); if (!mz_zip_writer_add_mem(&zip_ar, path.string().c_str(), v.data(), @@ -169,7 +169,7 @@ template void LoadVector(BlobType type, std::filesystem::path& path, std::vector& v) { using namespace Common::FS; path.replace_extension(GetBlobFileExtension(type)); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { int index{-1}; index = mz_zip_reader_locate_file(&zip_ar, path.string().c_str(), nullptr, 0); if (index < 0) { @@ -192,7 +192,8 @@ bool DataBase::Save(BlobType type, const std::string& name, std::vector&& da return false; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return WriteVector(type, std::move(path), std::move(data)); } @@ -201,7 +202,8 @@ bool DataBase::Save(BlobType type, const std::string& name, std::vector&& d return false; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return WriteVector(type, std::move(path), std::move(data)); } @@ -210,7 +212,8 @@ void DataBase::Load(BlobType type, const std::string& name, std::vector& dat return; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return LoadVector(type, path, data); } @@ -219,13 +222,14 @@ void DataBase::Load(BlobType type, const std::string& name, std::vector& da return; } - auto path = Config::isPipelineCacheArchived() ? std::filesystem::path{name} : cache_path / name; + auto path = EmulatorSettings.IsPipelineCacheArchived() ? std::filesystem::path{name} + : cache_path / name; return LoadVector(type, path, data); } void DataBase::ForEachBlob(BlobType type, const std::function&& data)>& func) { const auto& ext = GetBlobFileExtension(type); - if (Config::isPipelineCacheArchived()) { + if (EmulatorSettings.IsPipelineCacheArchived()) { const auto num_files = mz_zip_reader_get_num_files(&zip_ar); for (int index = 0; index < num_files; ++index) { std::array file_name{}; @@ -255,7 +259,7 @@ void DataBase::ForEachBlob(BlobType type, const std::function @@ -31,7 +31,7 @@ void LoadRenderDoc() { // Check if we are running by RDoc GUI HMODULE mod = GetModuleHandleA("renderdoc.dll"); - if (!mod && Config::isRdocEnabled()) { + if (!mod && EmulatorSettings.IsRenderdocEnabled()) { // If enabled in config, try to load RDoc runtime in offline mode HKEY h_reg_key; LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, @@ -67,7 +67,7 @@ void LoadRenderDoc() { #endif // Check if we are running by RDoc GUI void* mod = dlopen(RENDERDOC_LIB, RTLD_NOW | RTLD_NOLOAD); - if (!mod && Config::isRdocEnabled()) { + if (!mod && EmulatorSettings.IsRenderdocEnabled()) { // If enabled in config, try to load RDoc runtime in offline mode if ((mod = dlopen(RENDERDOC_LIB, RTLD_NOW))) { const auto RENDERDOC_GetAPI = diff --git a/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp b/src/video_core/renderer_vulkan/host_passes/fsr_pass.cpp index 373ec1711..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(); } 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..f36d9c61c 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-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..7f02fcaf1 100644 --- a/src/video_core/renderer_vulkan/vk_platform.cpp +++ b/src/video_core/renderer_vulkan/vk_platform.cpp @@ -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; } From 060703f6273d11e3abe0f8581ad9161b0166ee3d Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sun, 22 Mar 2026 00:27:55 +0200 Subject: [PATCH 4/5] Initial Openal (Part 2 of 0.15.1 branch) (#4155) * using new emulator_settings * the default user is now just player one * transfer install, addon dirs * fix load custom config issue * initial openal backend * linux fix? --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> --- CMakeLists.txt | 7 + src/core/libraries/audio/audioin.h | 1 + src/core/libraries/audio/audioout.cpp | 11 +- src/core/libraries/audio/audioout_backend.h | 7 +- src/core/libraries/audio/openal_audio_out.cpp | 832 +++++++++++++++ src/core/libraries/audio/openal_manager.h | 226 ++++ src/core/libraries/audio3d/audio3d_openal.cpp | 997 ++++++++++++++++++ src/core/libraries/audio3d/audio3d_openal.h | 181 ++++ src/core/libraries/libs.cpp | 10 +- 9 files changed, 2264 insertions(+), 8 deletions(-) create mode 100644 src/core/libraries/audio/openal_audio_out.cpp create mode 100644 src/core/libraries/audio/openal_manager.h create mode 100644 src/core/libraries/audio3d/audio3d_openal.cpp create mode 100644 src/core/libraries/audio3d/audio3d_openal.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ab204b7a2..06f8cb6ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -295,8 +295,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 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/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/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); From 880445c2cea5a8c7b571f55e5bf390d011763bf8 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sun, 22 Mar 2026 09:08:44 +0200 Subject: [PATCH 5/5] Initial camera (Part 3 of 0.15.1 branch) (#4156) * using new emulator_settings * the default user is now just player one * transfer install, addon dirs * fix load custom config issue * initial openal backend * linux fix? * camera module updated --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> --- src/core/libraries/camera/camera.cpp | 238 ++++++++++++++++++++++++++- src/core/libraries/camera/camera.h | 117 +++++++++++++ src/sdl_window.cpp | 3 + 3 files changed, 350 insertions(+), 8 deletions(-) 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/sdl_window.cpp b/src/sdl_window.cpp index 838fc4db4..0c7374bfb 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -288,6 +288,9 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_ if (!SDL_Init(SDL_INIT_VIDEO)) { UNREACHABLE_MSG("Failed to initialize SDL video subsystem: {}", SDL_GetError()); } + if (!SDL_Init(SDL_INIT_CAMERA)) { + UNREACHABLE_MSG("Failed to initialize SDL camera subsystem: {}", SDL_GetError()); + } SDL_InitSubSystem(SDL_INIT_AUDIO); SDL_PropertiesID props = SDL_CreateProperties();