// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include #include #include #include #include #include #include #include "common/logging/log.h" #include "common/types.h" #define EmulatorSettings (*EmulatorSettingsImpl::GetInstance()) enum HideCursorState : int { Never, Idle, Always, }; enum UsbBackendType : int { Real, SkylandersPortal, InfinityBase, DimensionsToypad, }; enum GpuReadbacksMode : int { Disabled, Relaxed, Precise, }; enum class ConfigMode { Default, Global, Clean, }; enum AudioBackend : int { SDL, OpenAL, // Add more backends as needed }; template struct Setting { T default_value{}; T value{}; std::optional game_specific_value{}; Setting() = default; // Single-argument ctor: initialises both default_value and value so // that CleanMode can always recover the intended factory default. /*implicit*/ Setting(T init) : default_value(std::move(init)), value(default_value) {} /// Return the active value under the given mode. T get(ConfigMode mode = ConfigMode::Default) const { switch (mode) { case ConfigMode::Default: return game_specific_value.value_or(value); case ConfigMode::Global: return value; case ConfigMode::Clean: return default_value; } return value; } /// Write v to the base layer. /// Game-specific overrides are applied exclusively via Load(serial) void set(const T& v) { value = v; } /// Discard the game-specific override; subsequent get(Default) will /// fall back to the base value. void reset_game_specific() { game_specific_value = std::nullopt; } }; template void to_json(nlohmann::json& j, const Setting& s) { j = s.value; } template void from_json(const nlohmann::json& j, Setting& s) { s.value = j.get(); } struct OverrideItem { const char* key; std::function& changed)> apply; /// Return the value that should be written to the per-game config file. /// Falls back to base value if no game-specific override is set. std::function get_for_save; /// Clear game_specific_value for this field. std::function reset_game_specific; }; template inline OverrideItem make_override(const char* key, Setting Struct::* member) { return OverrideItem{ key, [member, key](void* base, const nlohmann::json& entry, std::vector& changed) { LOG_DEBUG(Config, "[make_override] Processing key: {}", key); LOG_DEBUG(Config, "[make_override] Entry JSON: {}", entry.dump()); Struct* obj = reinterpret_cast(base); Setting& dst = obj->*member; try { T newValue = entry.get(); LOG_DEBUG(Config, "[make_override] Parsed value: {}", newValue); LOG_DEBUG(Config, "[make_override] Current value: {}", dst.value); if (dst.value != newValue) { std::ostringstream oss; oss << key << " ( " << dst.value << " → " << newValue << " )"; changed.push_back(oss.str()); LOG_DEBUG(Config, "[make_override] Recorded change: {}", oss.str()); } dst.game_specific_value = newValue; LOG_DEBUG(Config, "[make_override] Successfully updated {}", key); } catch (const std::exception& e) { LOG_ERROR(Config, "[make_override] ERROR parsing {}: {}", key, e.what()); LOG_ERROR(Config, "[make_override] Entry was: {}", entry.dump()); LOG_ERROR(Config, "[make_override] Type name: {}", entry.type_name()); } }, // --- get_for_save ------------------------------------------- // Returns game_specific_value when present, otherwise base value. // This means a freshly-opened game-specific dialog still shows // useful (current-global) values rather than empty entries. [member](const void* base) -> nlohmann::json { const Struct* obj = reinterpret_cast(base); const Setting& src = obj->*member; return nlohmann::json(src.game_specific_value.value_or(src.value)); }, // --- reset_game_specific ------------------------------------ [member](void* base) { Struct* obj = reinterpret_cast(base); (obj->*member).reset_game_specific(); }}; } // ------------------------------- // Support types // ------------------------------- struct GameInstallDir { std::filesystem::path path; bool enabled; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GameInstallDir, path, enabled) // ------------------------------- // General settings // ------------------------------- struct GeneralSettings { Setting> install_dirs; Setting addon_install_dir; Setting home_dir; Setting sys_modules_dir; Setting font_dir; Setting volume_slider{100}; Setting neo_mode{false}; Setting dev_kit_mode{false}; Setting extra_dmem_in_mbytes{0}; Setting psn_signed_in{false}; Setting trophy_popup_disabled{false}; Setting trophy_notification_duration{6.0}; Setting trophy_notification_side{"right"}; Setting log_filter{""}; Setting log_type{"sync"}; Setting show_splash{false}; Setting identical_log_grouped{true}; Setting connected_to_network{false}; Setting discord_rpc_enabled{false}; Setting show_fps_counter{false}; Setting console_language{1}; // return a vector of override descriptors (runtime, but tiny) std::vector GetOverrideableFields() const { return std::vector{ make_override("volume_slider", &GeneralSettings::volume_slider), make_override("neo_mode", &GeneralSettings::neo_mode), make_override("dev_kit_mode", &GeneralSettings::dev_kit_mode), make_override("extra_dmem_in_mbytes", &GeneralSettings::extra_dmem_in_mbytes), make_override("psn_signed_in", &GeneralSettings::psn_signed_in), make_override("trophy_popup_disabled", &GeneralSettings::trophy_popup_disabled), make_override("trophy_notification_duration", &GeneralSettings::trophy_notification_duration), make_override("log_filter", &GeneralSettings::log_filter), make_override("log_type", &GeneralSettings::log_type), make_override("identical_log_grouped", &GeneralSettings::identical_log_grouped), make_override("show_splash", &GeneralSettings::show_splash), make_override("trophy_notification_side", &GeneralSettings::trophy_notification_side), make_override("connected_to_network", &GeneralSettings::connected_to_network)}; } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GeneralSettings, install_dirs, addon_install_dir, home_dir, sys_modules_dir, font_dir, volume_slider, neo_mode, dev_kit_mode, extra_dmem_in_mbytes, psn_signed_in, trophy_popup_disabled, trophy_notification_duration, log_filter, log_type, show_splash, identical_log_grouped, trophy_notification_side, connected_to_network, discord_rpc_enabled, show_fps_counter, console_language) // ------------------------------- // Debug settings // ------------------------------- struct DebugSettings { Setting separate_logging_enabled{false}; // specific Setting debug_dump{false}; // specific Setting shader_collect{false}; // specific Setting log_enabled{true}; // specific Setting config_version{""}; // specific std::vector GetOverrideableFields() const { return std::vector{ make_override("debug_dump", &DebugSettings::debug_dump), make_override("shader_collect", &DebugSettings::shader_collect), make_override("separate_logging_enabled", &DebugSettings::separate_logging_enabled), make_override("log_enabled", &DebugSettings::log_enabled)}; } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DebugSettings, separate_logging_enabled, debug_dump, shader_collect, log_enabled, config_version) // ------------------------------- // Input settings // ------------------------------- struct InputSettings { Setting cursor_state{HideCursorState::Idle}; // specific Setting cursor_hide_timeout{5}; // specific Setting usb_device_backend{UsbBackendType::Real}; // specific Setting use_special_pad{false}; Setting special_pad_class{1}; Setting motion_controls_enabled{true}; // specific Setting use_unified_input_config{true}; Setting default_controller_id{""}; Setting background_controller_input{false}; // specific Setting camera_id{-1}; std::vector GetOverrideableFields() const { return std::vector{ make_override("cursor_state", &InputSettings::cursor_state), make_override("cursor_hide_timeout", &InputSettings::cursor_hide_timeout), make_override("usb_device_backend", &InputSettings::usb_device_backend), make_override("motion_controls_enabled", &InputSettings::motion_controls_enabled), make_override("background_controller_input", &InputSettings::background_controller_input), make_override("camera_id", &InputSettings::camera_id)}; } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(InputSettings, cursor_state, cursor_hide_timeout, usb_device_backend, use_special_pad, special_pad_class, motion_controls_enabled, use_unified_input_config, default_controller_id, background_controller_input, camera_id) // ------------------------------- // Audio settings // ------------------------------- struct AudioSettings { Setting audio_backend{AudioBackend::SDL}; Setting sdl_mic_device{"Default Device"}; Setting sdl_main_output_device{"Default Device"}; Setting sdl_padSpk_output_device{"Default Device"}; Setting openal_mic_device{"Default Device"}; Setting openal_main_output_device{"Default Device"}; Setting openal_padSpk_output_device{"Default Device"}; std::vector GetOverrideableFields() const { return std::vector{ make_override("audio_backend", &AudioSettings::audio_backend), make_override("sdl_mic_device", &AudioSettings::sdl_mic_device), make_override("sdl_main_output_device", &AudioSettings::sdl_main_output_device), make_override("sdl_padSpk_output_device", &AudioSettings::sdl_padSpk_output_device), make_override("openal_mic_device", &AudioSettings::openal_mic_device), make_override("openal_main_output_device", &AudioSettings::openal_main_output_device), make_override("openal_padSpk_output_device", &AudioSettings::openal_padSpk_output_device)}; } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AudioSettings, audio_backend, sdl_mic_device, sdl_main_output_device, sdl_padSpk_output_device, openal_mic_device, openal_main_output_device, openal_padSpk_output_device) // ------------------------------- // GPU settings // ------------------------------- struct GPUSettings { Setting window_width{1280}; Setting window_height{720}; Setting internal_screen_width{1280}; Setting internal_screen_height{720}; Setting null_gpu{false}; Setting copy_gpu_buffers{false}; Setting readbacks_mode{GpuReadbacksMode::Disabled}; Setting readback_linear_images_enabled{false}; Setting direct_memory_access_enabled{false}; Setting dump_shaders{false}; Setting patch_shaders{false}; Setting vblank_frequency{60}; Setting full_screen{false}; Setting full_screen_mode{"Windowed"}; Setting present_mode{"Mailbox"}; Setting hdr_allowed{false}; Setting fsr_enabled{false}; Setting rcas_enabled{true}; Setting rcas_attenuation{250}; // TODO add overrides std::vector GetOverrideableFields() const { return std::vector{ make_override("null_gpu", &GPUSettings::null_gpu), make_override("copy_gpu_buffers", &GPUSettings::copy_gpu_buffers), make_override("full_screen", &GPUSettings::full_screen), make_override("full_screen_mode", &GPUSettings::full_screen_mode), make_override("present_mode", &GPUSettings::present_mode), make_override("window_height", &GPUSettings::window_height), make_override("window_width", &GPUSettings::window_width), make_override("hdr_allowed", &GPUSettings::hdr_allowed), make_override("fsr_enabled", &GPUSettings::fsr_enabled), make_override("rcas_enabled", &GPUSettings::rcas_enabled), make_override("rcas_attenuation", &GPUSettings::rcas_attenuation), make_override("dump_shaders", &GPUSettings::dump_shaders), make_override("patch_shaders", &GPUSettings::patch_shaders), make_override("readbacks_mode", &GPUSettings::readbacks_mode), make_override("readback_linear_images_enabled", &GPUSettings::readback_linear_images_enabled), make_override("direct_memory_access_enabled", &GPUSettings::direct_memory_access_enabled), make_override("vblank_frequency", &GPUSettings::vblank_frequency), }; } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GPUSettings, window_width, window_height, internal_screen_width, internal_screen_height, null_gpu, copy_gpu_buffers, readbacks_mode, readback_linear_images_enabled, direct_memory_access_enabled, dump_shaders, patch_shaders, vblank_frequency, full_screen, full_screen_mode, present_mode, hdr_allowed, fsr_enabled, rcas_enabled, rcas_attenuation) // ------------------------------- // Vulkan settings // ------------------------------- struct VulkanSettings { Setting gpu_id{-1}; Setting renderdoc_enabled{false}; Setting vkvalidation_enabled{false}; Setting vkvalidation_core_enabled{true}; Setting vkvalidation_sync_enabled{false}; Setting vkvalidation_gpu_enabled{false}; Setting vkcrash_diagnostic_enabled{false}; Setting vkhost_markers{false}; Setting vkguest_markers{false}; Setting pipeline_cache_enabled{false}; Setting pipeline_cache_archived{false}; std::vector GetOverrideableFields() const { return std::vector{ make_override("gpu_id", &VulkanSettings::gpu_id), make_override("renderdoc_enabled", &VulkanSettings::renderdoc_enabled), make_override("vkvalidation_enabled", &VulkanSettings::vkvalidation_enabled), make_override("vkvalidation_core_enabled", &VulkanSettings::vkvalidation_core_enabled), make_override("vkvalidation_sync_enabled", &VulkanSettings::vkvalidation_sync_enabled), make_override("vkvalidation_gpu_enabled", &VulkanSettings::vkvalidation_gpu_enabled), make_override("vkcrash_diagnostic_enabled", &VulkanSettings::vkcrash_diagnostic_enabled), make_override("vkhost_markers", &VulkanSettings::vkhost_markers), make_override("vkguest_markers", &VulkanSettings::vkguest_markers), make_override("pipeline_cache_enabled", &VulkanSettings::pipeline_cache_enabled), make_override("pipeline_cache_archived", &VulkanSettings::pipeline_cache_archived), }; } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(VulkanSettings, gpu_id, renderdoc_enabled, vkvalidation_enabled, vkvalidation_core_enabled, vkvalidation_sync_enabled, vkvalidation_gpu_enabled, vkcrash_diagnostic_enabled, vkhost_markers, vkguest_markers, pipeline_cache_enabled, pipeline_cache_archived) // ------------------------------- // Main manager // ------------------------------- class EmulatorSettingsImpl { public: EmulatorSettingsImpl(); ~EmulatorSettingsImpl(); static std::shared_ptr GetInstance(); static void SetInstance(std::shared_ptr instance); bool Save(const std::string& serial = ""); bool Load(const std::string& serial = ""); void SetDefaultValues(); bool TransferSettings(); // Config mode ConfigMode GetConfigMode() const { return m_configMode; } void SetConfigMode(ConfigMode mode) { m_configMode = mode; } // // Game-specific override management /// Clears all per-game overrides. Call this when a game exits so /// the emulator reverts to global settings. void ClearGameSpecificOverrides(); /// Reset a single field's game-specific override by its JSON ke void ResetGameSpecificValue(const std::string& key); // general accessors bool AddGameInstallDir(const std::filesystem::path& dir, bool enabled = true); std::vector GetGameInstallDirs() const; void SetAllGameInstallDirs(const std::vector& dirs); void RemoveGameInstallDir(const std::filesystem::path& dir); void SetGameInstallDirEnabled(const std::filesystem::path& dir, bool enabled); void SetGameInstallDirs(const std::vector& dirs_config); const std::vector GetGameInstallDirsEnabled(); const std::vector& GetAllGameInstallDirs() const; std::filesystem::path GetHomeDir(); void SetHomeDir(const std::filesystem::path& dir); std::filesystem::path GetSysModulesDir(); void SetSysModulesDir(const std::filesystem::path& dir); std::filesystem::path GetFontsDir(); void SetFontsDir(const std::filesystem::path& dir); private: GeneralSettings m_general{}; DebugSettings m_debug{}; InputSettings m_input{}; AudioSettings m_audio{}; GPUSettings m_gpu{}; VulkanSettings m_vulkan{}; ConfigMode m_configMode{ConfigMode::Default}; bool m_loaded{false}; static std::shared_ptr s_instance; static std::mutex s_mutex; /// Apply overrideable fields from groupJson into group.game_specific_value. template void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson, std::vector& changed) { for (auto& item : group.GetOverrideableFields()) { if (!groupJson.contains(item.key)) continue; item.apply(&group, groupJson.at(item.key), changed); } } // Write all overrideable fields from group into out (for game-specific save). template static void SaveGroupGameSpecific(const Group& group, nlohmann::json& out) { for (auto& item : group.GetOverrideableFields()) out[item.key] = item.get_for_save(&group); } // Discard every game-specific override in group. template static void ClearGroupOverrides(Group& group) { for (auto& item : group.GetOverrideableFields()) item.reset_game_specific(&group); } static void PrintChangedSummary(const std::vector& changed); public: // Add these getters to access overrideable fields std::vector GetGeneralOverrideableFields() const { return m_general.GetOverrideableFields(); } std::vector GetDebugOverrideableFields() const { return m_debug.GetOverrideableFields(); } std::vector GetInputOverrideableFields() const { return m_input.GetOverrideableFields(); } std::vector GetAudioOverrideableFields() const { return m_audio.GetOverrideableFields(); } std::vector GetGPUOverrideableFields() const { return m_gpu.GetOverrideableFields(); } std::vector GetVulkanOverrideableFields() const { return m_vulkan.GetOverrideableFields(); } std::vector GetAllOverrideableKeys() const; #define SETTING_FORWARD(group, Name, field) \ auto Get##Name() const { \ return (group).field.get(m_configMode); \ } \ void Set##Name(const decltype((group).field.value)& v) { \ (group).field.value = v; \ } #define SETTING_FORWARD_BOOL(group, Name, field) \ bool Is##Name() const { \ return (group).field.get(m_configMode); \ } \ void Set##Name(bool v) { \ (group).field.value = v; \ } #define SETTING_FORWARD_BOOL_READONLY(group, Name, field) \ bool Is##Name() const { \ return (group).field.get(m_configMode); \ } // General settings SETTING_FORWARD(m_general, VolumeSlider, volume_slider) SETTING_FORWARD_BOOL(m_general, Neo, neo_mode) SETTING_FORWARD_BOOL(m_general, DevKit, dev_kit_mode) SETTING_FORWARD(m_general, ExtraDmemInMBytes, extra_dmem_in_mbytes) SETTING_FORWARD_BOOL(m_general, PSNSignedIn, psn_signed_in) SETTING_FORWARD_BOOL(m_general, TrophyPopupDisabled, trophy_popup_disabled) SETTING_FORWARD(m_general, TrophyNotificationDuration, trophy_notification_duration) SETTING_FORWARD(m_general, TrophyNotificationSide, trophy_notification_side) SETTING_FORWARD_BOOL(m_general, ShowSplash, show_splash) SETTING_FORWARD_BOOL(m_general, IdenticalLogGrouped, identical_log_grouped) SETTING_FORWARD(m_general, AddonInstallDir, addon_install_dir) SETTING_FORWARD(m_general, LogFilter, log_filter) SETTING_FORWARD(m_general, LogType, log_type) SETTING_FORWARD_BOOL(m_general, ConnectedToNetwork, connected_to_network) SETTING_FORWARD_BOOL(m_general, DiscordRPCEnabled, discord_rpc_enabled) SETTING_FORWARD_BOOL(m_general, ShowFpsCounter, show_fps_counter) SETTING_FORWARD(m_general, ConsoleLanguage, console_language) // Audio settings SETTING_FORWARD(m_audio, AudioBackend, audio_backend) SETTING_FORWARD(m_audio, SDLMicDevice, sdl_mic_device) SETTING_FORWARD(m_audio, SDLMainOutputDevice, sdl_main_output_device) SETTING_FORWARD(m_audio, SDLPadSpkOutputDevice, sdl_padSpk_output_device) SETTING_FORWARD(m_audio, OpenALMicDevice, openal_mic_device) SETTING_FORWARD(m_audio, OpenALMainOutputDevice, openal_main_output_device) SETTING_FORWARD(m_audio, OpenALPadSpkOutputDevice, openal_padSpk_output_device) // Debug settings SETTING_FORWARD_BOOL(m_debug, SeparateLoggingEnabled, separate_logging_enabled) SETTING_FORWARD_BOOL(m_debug, DebugDump, debug_dump) SETTING_FORWARD_BOOL(m_debug, ShaderCollect, shader_collect) SETTING_FORWARD_BOOL(m_debug, LogEnabled, log_enabled) SETTING_FORWARD(m_debug, ConfigVersion, config_version) // GPU Settings SETTING_FORWARD_BOOL(m_gpu, NullGPU, null_gpu) SETTING_FORWARD_BOOL(m_gpu, DumpShaders, dump_shaders) SETTING_FORWARD_BOOL(m_gpu, CopyGpuBuffers, copy_gpu_buffers) SETTING_FORWARD_BOOL(m_gpu, FullScreen, full_screen) SETTING_FORWARD(m_gpu, FullScreenMode, full_screen_mode) SETTING_FORWARD(m_gpu, PresentMode, present_mode) SETTING_FORWARD(m_gpu, WindowHeight, window_height) SETTING_FORWARD(m_gpu, WindowWidth, window_width) SETTING_FORWARD(m_gpu, InternalScreenHeight, internal_screen_height) SETTING_FORWARD(m_gpu, InternalScreenWidth, internal_screen_width) SETTING_FORWARD_BOOL(m_gpu, HdrAllowed, hdr_allowed) SETTING_FORWARD_BOOL(m_gpu, FsrEnabled, fsr_enabled) SETTING_FORWARD_BOOL(m_gpu, RcasEnabled, rcas_enabled) SETTING_FORWARD(m_gpu, RcasAttenuation, rcas_attenuation) SETTING_FORWARD(m_gpu, ReadbacksMode, readbacks_mode) SETTING_FORWARD_BOOL(m_gpu, ReadbackLinearImagesEnabled, readback_linear_images_enabled) SETTING_FORWARD_BOOL(m_gpu, DirectMemoryAccessEnabled, direct_memory_access_enabled) SETTING_FORWARD_BOOL_READONLY(m_gpu, PatchShaders, patch_shaders) u32 GetVblankFrequency() { if (m_gpu.vblank_frequency.value < 30) { return 30; } return m_gpu.vblank_frequency.get(); } void SetVblankFrequency(const u32& v, bool is_specific = false) { u32 val = v < 30 ? 30 : v; if (is_specific) { m_gpu.vblank_frequency.game_specific_value = val; } else { m_gpu.vblank_frequency.value = val; } } // Input Settings SETTING_FORWARD(m_input, CursorState, cursor_state) SETTING_FORWARD(m_input, CursorHideTimeout, cursor_hide_timeout) SETTING_FORWARD(m_input, UsbDeviceBackend, usb_device_backend) SETTING_FORWARD_BOOL(m_input, MotionControlsEnabled, motion_controls_enabled) SETTING_FORWARD_BOOL(m_input, BackgroundControllerInput, background_controller_input) SETTING_FORWARD(m_input, DefaultControllerId, default_controller_id) SETTING_FORWARD_BOOL(m_input, UsingSpecialPad, use_special_pad) SETTING_FORWARD(m_input, SpecialPadClass, special_pad_class) SETTING_FORWARD_BOOL(m_input, UseUnifiedInputConfig, use_unified_input_config) SETTING_FORWARD(m_input, CameraId, camera_id) // Vulkan settings SETTING_FORWARD(m_vulkan, GpuId, gpu_id) SETTING_FORWARD_BOOL(m_vulkan, RenderdocEnabled, renderdoc_enabled) SETTING_FORWARD_BOOL(m_vulkan, VkValidationEnabled, vkvalidation_enabled) SETTING_FORWARD_BOOL(m_vulkan, VkValidationCoreEnabled, vkvalidation_core_enabled) SETTING_FORWARD_BOOL(m_vulkan, VkValidationSyncEnabled, vkvalidation_sync_enabled) SETTING_FORWARD_BOOL(m_vulkan, VkValidationGpuEnabled, vkvalidation_gpu_enabled) SETTING_FORWARD_BOOL(m_vulkan, VkCrashDiagnosticEnabled, vkcrash_diagnostic_enabled) SETTING_FORWARD_BOOL(m_vulkan, VkHostMarkersEnabled, vkhost_markers) SETTING_FORWARD_BOOL(m_vulkan, VkGuestMarkersEnabled, vkguest_markers) SETTING_FORWARD_BOOL(m_vulkan, PipelineCacheEnabled, pipeline_cache_enabled) SETTING_FORWARD_BOOL(m_vulkan, PipelineCacheArchived, pipeline_cache_archived) #undef SETTING_FORWARD #undef SETTING_FORWARD_BOOL #undef SETTING_FORWARD_BOOL_READONLY };