Merge branch 'shadps4-emu:main' into ime-fixes-again

This commit is contained in:
Valdis Bogdāns 2026-03-22 14:18:51 +02:00 committed by GitHub
commit a22d035bef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 4746 additions and 338 deletions

77
.github/ISSUE_TEMPLATE/rfc.yaml vendored Normal file
View File

@ -0,0 +1,77 @@
# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
# Docs - https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema
name: Request For Comments (RFC)
description: Ask for feedback on major architectural changes or design choices
title: "[RFC]: "
labels: ["RFC"]
body:
- type: markdown
attributes:
value: |
## Important: Read First
RFCs are for major architectural changes, design direction, or changes that benefit from structured discussion before merge.
Please make an effort to search for an existing RFC or issue before opening a new one.
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I have searched for a similar RFC or issue in this repository and did not find one.
required: true
- type: textarea
id: motivation
attributes:
label: Motivation
description: |
Explain the problem this RFC is trying to solve.
Describe why the current design is insufficient and why this change is worth discussing now.
validations:
required: true
- type: textarea
id: proposed_change
attributes:
label: Proposed Change
description: |
Describe the proposed change in enough detail for maintainers and contributors to evaluate it.
Include the high-level design, affected areas, and any important constraints.
validations:
required: true
- type: textarea
id: feedback_period
attributes:
label: Feedback Period
description: |
State the intended review window for this RFC.
Example: one week, two weeks, or until specific maintainers have reviewed it.
placeholder: "Example: 1 week"
validations:
required: false
- type: textarea
id: cc_list
attributes:
label: CC List
description: |
List any maintainers or contributors you want to explicitly notify for feedback.
validations:
required: false
- type: textarea
id: additional_context
attributes:
label: Any Other Things
description: |
Add any other relevant context, tradeoffs, diagrams, migration notes, or links to related work.
validations:
required: false

View File

@ -27,7 +27,7 @@ jobs:
continue-on-error: true
steps:
- uses: actions/checkout@v6
- uses: fsfe/reuse-action@v5
- uses: fsfe/reuse-action@v6
clang-format:
runs-on: ubuntu-24.04
@ -99,7 +99,7 @@ jobs:
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $env:NUMBER_OF_PROCESSORS
- name: Upload Windows SDL artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: shadps4-win64-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: ${{github.workspace}}/build/shadPS4.exe
@ -150,7 +150,7 @@ jobs:
mv ${{github.workspace}}/build/shadps4 upload
mv ${{github.workspace}}/build/MoltenVK_icd.json upload
mv ${{github.workspace}}/build/libMoltenVK.dylib upload
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: shadps4-macos-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: upload/
@ -200,7 +200,7 @@ jobs:
run: |
ls -la ${{ github.workspace }}/build/shadps4
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: shadps4-ubuntu64-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: ${{ github.workspace }}/build/shadps4
@ -211,7 +211,7 @@ jobs:
- name: Package and Upload Linux SDL artifact
run: |
tar cf shadps4-linux-sdl.tar.gz -C ${{github.workspace}}/build shadps4
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: shadps4-linux-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: Shadps4-sdl.AppImage

View File

@ -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
@ -873,6 +880,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")

View File

@ -6,6 +6,7 @@
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include "assert.h"
#include "bit_field.h"
@ -73,6 +74,7 @@ class ElfInfo {
std::filesystem::path splash_path{};
std::filesystem::path game_folder{};
std::vector<std::string> npCommIds{};
public:
static constexpr u32 FW_10 = 0x1000000;
@ -139,6 +141,10 @@ public:
[[nodiscard]] const std::filesystem::path& GetGameFolder() const {
return game_folder;
}
[[nodiscard]] const std::vector<std::string> GetNpCommIds() const {
return npCommIds;
}
};
} // namespace Common

View File

@ -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<Impl, decltype(&Deleter)>(
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); });

View File

@ -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...

View File

@ -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
};

View File

@ -8,7 +8,6 @@
#include <string>
#include <nlohmann/json.hpp>
#include <pugixml.hpp>
#include "common/config.h"
#include "common/elf_info.h"
#include "common/logging/log.h"
#include "common/path_util.h"

View File

@ -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()) {

View File

@ -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";

View File

@ -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 <cstddef>
#include <vector>
namespace Serialization {

View File

@ -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 <map>
#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;

View File

@ -7,10 +7,10 @@
#include <imgui.h>
#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<int>(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<int>(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();
}

View File

@ -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;

View File

@ -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 <mutex>
#include <string>
#include <vector>
#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();

View File

@ -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 <fstream>
@ -8,11 +8,11 @@
#include <imgui.h>
#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;
}

View File

@ -0,0 +1,647 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <map>
#include <common/path_util.h>
#include <common/scm_rev.h>
#include <toml.hpp>
#include "common/logging/log.h"
#include "emulator_settings.h"
#include "emulator_state.h"
#include <SDL3/SDL_messagebox.h>
using json = nlohmann::json;
// ── Singleton storage ─────────────────────────────────────────────────
std::shared_ptr<EmulatorSettingsImpl> EmulatorSettingsImpl::s_instance = nullptr;
std::mutex EmulatorSettingsImpl::s_mutex;
// ── nlohmann helpers for std::filesystem::path ───────────────────────
namespace nlohmann {
template <>
struct adl_serializer<std::filesystem::path> {
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<std::string>();
}
};
} // namespace nlohmann
namespace toml {
// why is it so hard to avoid exceptions with this library
template <typename T>
std::optional<T> 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<T, int>) {
if (it->second.is_integer()) {
return static_cast<int>(toml::get<int>(it->second));
}
} else if constexpr (std::is_same_v<T, unsigned int>) {
if (it->second.is_integer()) {
return static_cast<u32>(toml::get<unsigned int>(it->second));
}
} else if constexpr (std::is_same_v<T, double>) {
if (it->second.is_floating()) {
return toml::get<double>(it->second);
}
} else if constexpr (std::is_same_v<T, std::string>) {
if (it->second.is_string()) {
return toml::get<std::string>(it->second);
}
} else if constexpr (std::is_same_v<T, std::filesystem::path>) {
if (it->second.is_string()) {
return toml::get<std::string>(it->second);
}
} else if constexpr (std::is_same_v<T, bool>) {
if (it->second.is_boolean()) {
return toml::get<bool>(it->second);
}
} else {
static_assert([] { return false; }(), "Unsupported type in get_optional<T>");
}
return std::nullopt;
}
} // namespace toml
// ── Helpers ───────────────────────────────────────────────────────────
void EmulatorSettingsImpl::PrintChangedSummary(const std::vector<std::string>& 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> EmulatorSettingsImpl::GetInstance() {
std::lock_guard lock(s_mutex);
if (!s_instance)
s_instance = std::make_shared<EmulatorSettingsImpl>();
return s_instance;
}
void EmulatorSettingsImpl::SetInstance(std::shared_ptr<EmulatorSettingsImpl> 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<std::filesystem::path> EmulatorSettingsImpl::GetGameInstallDirs() const {
std::vector<std::filesystem::path> out;
for (const auto& d : m_general.install_dirs.value)
if (d.enabled)
out.push_back(d.path);
return out;
}
const std::vector<GameInstallDir>& EmulatorSettingsImpl::GetAllGameInstallDirs() const {
return m_general.install_dirs.value;
}
void EmulatorSettingsImpl::SetAllGameInstallDirs(const std::vector<GameInstallDir>& 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<std::filesystem::path>& 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<bool> EmulatorSettingsImpl::GetGameInstallDirsEnabled() {
std::vector<bool> 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<std::remove_reference_t<decltype(group)>>();
};
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<std::string> 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 = [&]<typename T>(Setting<T>& n, toml::value const& t, std::string k) {
n = toml::get_optional<T>(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<std::vector<std::string>>(gui, "installDirs", {});
std::vector<bool> install_dirs_enabled;
try {
install_dirs_enabled = toml::find<std::vector<bool>>(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<GameInstallDir> 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<std::string>(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<std::string> EmulatorSettingsImpl::GetAllOverrideableKeys() const {
std::vector<std::string> 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;
}

View File

@ -0,0 +1,636 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <functional>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#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 <typename T>
struct Setting {
T default_value{};
T value{};
std::optional<T> 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 <typename T>
void to_json(nlohmann::json& j, const Setting<T>& s) {
j = s.value;
}
template <typename T>
void from_json(const nlohmann::json& j, Setting<T>& s) {
s.value = j.get<T>();
}
struct OverrideItem {
const char* key;
std::function<void(void* group_ptr, const nlohmann::json& entry,
std::vector<std::string>& 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<nlohmann::json(const void* group_ptr)> get_for_save;
/// Clear game_specific_value for this field.
std::function<void(void* group_ptr)> reset_game_specific;
};
template <typename Struct, typename T>
inline OverrideItem make_override(const char* key, Setting<T> Struct::* member) {
return OverrideItem{
key,
[member, key](void* base, const nlohmann::json& entry, std::vector<std::string>& changed) {
LOG_DEBUG(EmuSettings, "[make_override] Processing key: {}", key);
LOG_DEBUG(EmuSettings, "[make_override] Entry JSON: {}", entry.dump());
Struct* obj = reinterpret_cast<Struct*>(base);
Setting<T>& dst = obj->*member;
try {
T newValue = entry.get<T>();
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<const Struct*>(base);
const Setting<T>& src = obj->*member;
return nlohmann::json(src.game_specific_value.value_or(src.value));
},
// --- reset_game_specific ------------------------------------
[member](void* base) {
Struct* obj = reinterpret_cast<Struct*>(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<std::vector<GameInstallDir>> install_dirs;
Setting<std::filesystem::path> addon_install_dir;
Setting<std::filesystem::path> home_dir;
Setting<std::filesystem::path> sys_modules_dir;
Setting<std::filesystem::path> font_dir;
Setting<int> volume_slider{100};
Setting<bool> neo_mode{false};
Setting<bool> dev_kit_mode{false};
Setting<int> extra_dmem_in_mbytes{0};
Setting<bool> psn_signed_in{false};
Setting<bool> trophy_popup_disabled{false};
Setting<double> trophy_notification_duration{6.0};
Setting<std::string> trophy_notification_side{"right"};
Setting<std::string> log_filter{""};
Setting<std::string> log_type{"sync"};
Setting<bool> show_splash{false};
Setting<bool> identical_log_grouped{true};
Setting<bool> connected_to_network{false};
Setting<bool> discord_rpc_enabled{false};
Setting<bool> show_fps_counter{false};
Setting<int> console_language{1};
// return a vector of override descriptors (runtime, but tiny)
std::vector<OverrideItem> GetOverrideableFields() const {
return std::vector<OverrideItem>{
make_override<GeneralSettings>("volume_slider", &GeneralSettings::volume_slider),
make_override<GeneralSettings>("neo_mode", &GeneralSettings::neo_mode),
make_override<GeneralSettings>("dev_kit_mode", &GeneralSettings::dev_kit_mode),
make_override<GeneralSettings>("extra_dmem_in_mbytes",
&GeneralSettings::extra_dmem_in_mbytes),
make_override<GeneralSettings>("psn_signed_in", &GeneralSettings::psn_signed_in),
make_override<GeneralSettings>("trophy_popup_disabled",
&GeneralSettings::trophy_popup_disabled),
make_override<GeneralSettings>("trophy_notification_duration",
&GeneralSettings::trophy_notification_duration),
make_override<GeneralSettings>("log_filter", &GeneralSettings::log_filter),
make_override<GeneralSettings>("log_type", &GeneralSettings::log_type),
make_override<GeneralSettings>("identical_log_grouped",
&GeneralSettings::identical_log_grouped),
make_override<GeneralSettings>("show_splash", &GeneralSettings::show_splash),
make_override<GeneralSettings>("trophy_notification_side",
&GeneralSettings::trophy_notification_side),
make_override<GeneralSettings>("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<bool> separate_logging_enabled{false}; // specific
Setting<bool> debug_dump{false}; // specific
Setting<bool> shader_collect{false}; // specific
Setting<bool> log_enabled{true}; // specific
Setting<std::string> config_version{""}; // specific
std::vector<OverrideItem> GetOverrideableFields() const {
return std::vector<OverrideItem>{
make_override<DebugSettings>("debug_dump", &DebugSettings::debug_dump),
make_override<DebugSettings>("shader_collect", &DebugSettings::shader_collect),
make_override<DebugSettings>("separate_logging_enabled",
&DebugSettings::separate_logging_enabled),
make_override<DebugSettings>("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<int> cursor_state{HideCursorState::Idle}; // specific
Setting<int> cursor_hide_timeout{5}; // specific
Setting<int> usb_device_backend{UsbBackendType::Real}; // specific
Setting<bool> use_special_pad{false};
Setting<int> special_pad_class{1};
Setting<bool> motion_controls_enabled{true}; // specific
Setting<bool> use_unified_input_config{true};
Setting<std::string> default_controller_id{""};
Setting<bool> background_controller_input{false}; // specific
Setting<s32> camera_id{-1};
std::vector<OverrideItem> GetOverrideableFields() const {
return std::vector<OverrideItem>{
make_override<InputSettings>("cursor_state", &InputSettings::cursor_state),
make_override<InputSettings>("cursor_hide_timeout",
&InputSettings::cursor_hide_timeout),
make_override<InputSettings>("usb_device_backend", &InputSettings::usb_device_backend),
make_override<InputSettings>("motion_controls_enabled",
&InputSettings::motion_controls_enabled),
make_override<InputSettings>("background_controller_input",
&InputSettings::background_controller_input),
make_override<InputSettings>("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<u32> audio_backend{AudioBackend::SDL};
Setting<std::string> sdl_mic_device{"Default Device"};
Setting<std::string> sdl_main_output_device{"Default Device"};
Setting<std::string> sdl_padSpk_output_device{"Default Device"};
Setting<std::string> openal_mic_device{"Default Device"};
Setting<std::string> openal_main_output_device{"Default Device"};
Setting<std::string> openal_padSpk_output_device{"Default Device"};
std::vector<OverrideItem> GetOverrideableFields() const {
return std::vector<OverrideItem>{
make_override<AudioSettings>("audio_backend", &AudioSettings::audio_backend),
make_override<AudioSettings>("sdl_mic_device", &AudioSettings::sdl_mic_device),
make_override<AudioSettings>("sdl_main_output_device",
&AudioSettings::sdl_main_output_device),
make_override<AudioSettings>("sdl_padSpk_output_device",
&AudioSettings::sdl_padSpk_output_device),
make_override<AudioSettings>("openal_mic_device", &AudioSettings::openal_mic_device),
make_override<AudioSettings>("openal_main_output_device",
&AudioSettings::openal_main_output_device),
make_override<AudioSettings>("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<u32> window_width{1280};
Setting<u32> window_height{720};
Setting<u32> internal_screen_width{1280};
Setting<u32> internal_screen_height{720};
Setting<bool> null_gpu{false};
Setting<bool> copy_gpu_buffers{false};
Setting<u32> readbacks_mode{GpuReadbacksMode::Disabled};
Setting<bool> readback_linear_images_enabled{false};
Setting<bool> direct_memory_access_enabled{false};
Setting<bool> dump_shaders{false};
Setting<bool> patch_shaders{false};
Setting<u32> vblank_frequency{60};
Setting<bool> full_screen{false};
Setting<std::string> full_screen_mode{"Windowed"};
Setting<std::string> present_mode{"Mailbox"};
Setting<bool> hdr_allowed{false};
Setting<bool> fsr_enabled{false};
Setting<bool> rcas_enabled{true};
Setting<int> rcas_attenuation{250};
// TODO add overrides
std::vector<OverrideItem> GetOverrideableFields() const {
return std::vector<OverrideItem>{
make_override<GPUSettings>("null_gpu", &GPUSettings::null_gpu),
make_override<GPUSettings>("copy_gpu_buffers", &GPUSettings::copy_gpu_buffers),
make_override<GPUSettings>("full_screen", &GPUSettings::full_screen),
make_override<GPUSettings>("full_screen_mode", &GPUSettings::full_screen_mode),
make_override<GPUSettings>("present_mode", &GPUSettings::present_mode),
make_override<GPUSettings>("window_height", &GPUSettings::window_height),
make_override<GPUSettings>("window_width", &GPUSettings::window_width),
make_override<GPUSettings>("hdr_allowed", &GPUSettings::hdr_allowed),
make_override<GPUSettings>("fsr_enabled", &GPUSettings::fsr_enabled),
make_override<GPUSettings>("rcas_enabled", &GPUSettings::rcas_enabled),
make_override<GPUSettings>("rcas_attenuation", &GPUSettings::rcas_attenuation),
make_override<GPUSettings>("dump_shaders", &GPUSettings::dump_shaders),
make_override<GPUSettings>("patch_shaders", &GPUSettings::patch_shaders),
make_override<GPUSettings>("readbacks_mode", &GPUSettings::readbacks_mode),
make_override<GPUSettings>("readback_linear_images_enabled",
&GPUSettings::readback_linear_images_enabled),
make_override<GPUSettings>("direct_memory_access_enabled",
&GPUSettings::direct_memory_access_enabled),
make_override<GPUSettings>("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<s32> gpu_id{-1};
Setting<bool> renderdoc_enabled{false};
Setting<bool> vkvalidation_enabled{false};
Setting<bool> vkvalidation_core_enabled{true};
Setting<bool> vkvalidation_sync_enabled{false};
Setting<bool> vkvalidation_gpu_enabled{false};
Setting<bool> vkcrash_diagnostic_enabled{false};
Setting<bool> vkhost_markers{false};
Setting<bool> vkguest_markers{false};
Setting<bool> pipeline_cache_enabled{false};
Setting<bool> pipeline_cache_archived{false};
std::vector<OverrideItem> GetOverrideableFields() const {
return std::vector<OverrideItem>{
make_override<VulkanSettings>("gpu_id", &VulkanSettings::gpu_id),
make_override<VulkanSettings>("renderdoc_enabled", &VulkanSettings::renderdoc_enabled),
make_override<VulkanSettings>("vkvalidation_enabled",
&VulkanSettings::vkvalidation_enabled),
make_override<VulkanSettings>("vkvalidation_core_enabled",
&VulkanSettings::vkvalidation_core_enabled),
make_override<VulkanSettings>("vkvalidation_sync_enabled",
&VulkanSettings::vkvalidation_sync_enabled),
make_override<VulkanSettings>("vkvalidation_gpu_enabled",
&VulkanSettings::vkvalidation_gpu_enabled),
make_override<VulkanSettings>("vkcrash_diagnostic_enabled",
&VulkanSettings::vkcrash_diagnostic_enabled),
make_override<VulkanSettings>("vkhost_markers", &VulkanSettings::vkhost_markers),
make_override<VulkanSettings>("vkguest_markers", &VulkanSettings::vkguest_markers),
make_override<VulkanSettings>("pipeline_cache_enabled",
&VulkanSettings::pipeline_cache_enabled),
make_override<VulkanSettings>("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<EmulatorSettingsImpl> GetInstance();
static void SetInstance(std::shared_ptr<EmulatorSettingsImpl> 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<std::filesystem::path> GetGameInstallDirs() const;
void SetAllGameInstallDirs(const std::vector<GameInstallDir>& dirs);
void RemoveGameInstallDir(const std::filesystem::path& dir);
void SetGameInstallDirEnabled(const std::filesystem::path& dir, bool enabled);
void SetGameInstallDirs(const std::vector<std::filesystem::path>& dirs_config);
const std::vector<bool> GetGameInstallDirsEnabled();
const std::vector<GameInstallDir>& 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<EmulatorSettingsImpl> s_instance;
static std::mutex s_mutex;
/// Apply overrideable fields from groupJson into group.game_specific_value.
template <typename Group>
void ApplyGroupOverrides(Group& group, const nlohmann::json& groupJson,
std::vector<std::string>& 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 <typename Group>
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 <typename Group>
static void ClearGroupOverrides(Group& group) {
for (auto& item : group.GetOverrideableFields())
item.reset_game_specific(&group);
}
static void PrintChangedSummary(const std::vector<std::string>& changed);
public:
// Add these getters to access overrideable fields
std::vector<OverrideItem> GetGeneralOverrideableFields() const {
return m_general.GetOverrideableFields();
}
std::vector<OverrideItem> GetDebugOverrideableFields() const {
return m_debug.GetOverrideableFields();
}
std::vector<OverrideItem> GetInputOverrideableFields() const {
return m_input.GetOverrideableFields();
}
std::vector<OverrideItem> GetAudioOverrideableFields() const {
return m_audio.GetOverrideableFields();
}
std::vector<OverrideItem> GetGPUOverrideableFields() const {
return m_gpu.GetOverrideableFields();
}
std::vector<OverrideItem> GetVulkanOverrideableFields() const {
return m_vulkan.GetOverrideableFields();
}
std::vector<std::string> 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
};

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include "common/config.h"
#include "common/string_util.h"
#include "core/file_sys/devices/logger.h"
#include "core/file_sys/devices/nop_device.h"

View File

@ -8,12 +8,12 @@
#include <SDL3/SDL.h>
#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<int>(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;

View File

@ -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<Core::FileSys::MntPoints>::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<PSF>::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 {

View File

@ -3,6 +3,7 @@
#pragma once
#include <memory>
#include <mutex>
#include <core/libraries/system/userservice.h>
#include "common/types.h"

View File

@ -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 <memory>
@ -6,10 +6,9 @@
#include <shared_mutex>
#include <stop_token>
#include <thread>
#include <core/emulator_settings.h>
#include <magic_enum/magic_enum.hpp>
#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<SDLAudioOut>();
if (EmulatorSettings.GetAudioBackend() == AudioBackend::OpenAL) {
audio = std::make_unique<OpenALAudioOut>();
} else {
audio = std::make_unique<SDLAudioOut>();
}
LOG_INFO(Lib_AudioOut, "Audio system initialized");
return ORBIS_OK;

View File

@ -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<PortBackend> Open(PortOut& port) override;
};
class OpenALAudioOut final : public AudioOutBackend {
public:
std::unique_ptr<PortBackend> Open(PortOut& port) override;
};
} // namespace Libraries::AudioOut

View File

@ -0,0 +1,832 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <atomic>
#include <cstring>
#include <memory>
#include <thread>
#include <vector>
#include <AL/al.h>
#include <AL/alc.h>
#include <alext.h>
#include <queue>
#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 <immintrin.h>
#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<int, 8>& 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<float>(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<int>(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<int>(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<float> 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<s16> 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<ALsizei>(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<s64>(current_time - next_output_time);
if (time_diff > static_cast<s64>(TIMING_RESYNC_THRESHOLD_US)) [[unlikely]] {
next_output_time = current_time + period_us;
} else if (time_diff < 0) {
const u64 time_to_wait = static_cast<u64>(-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<ALsizei>(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<s16>(scaled + (scaled >= 0 ? 0.5f : -0.5f));
}
static void ConvertS16Mono(const void* src, void* dst, u32 frames, const float*) {
const s16* s = static_cast<const s16*>(src);
s16* d = static_cast<s16*>(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<const s16*>(src);
s16* d = static_cast<s16*>(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<const s16*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
float* d = static_cast<float*>(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<const float*>(src);
float* d = static_cast<float*>(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<const float*>(src);
float* d = static_cast<float*>(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<const float*>(src);
float* d = static_cast<float*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<const float*>(src);
s16* d = static_cast<s16*>(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<int, 8> channel_layout;
alignas(64) u64 period_us{0};
alignas(64) std::atomic<u64> 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<ALuint> buffers;
std::vector<ALuint> available_buffers;
ALenum format{AL_FORMAT_STEREO16};
// Buffer management
u32 buffer_size_bytes{0};
std::vector<s16> al_buffer_s16; // For S16 formats
std::vector<float> 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<float> current_gain{1.0f};
std::string device_name;
bool device_registered;
};
std::unique_ptr<PortBackend> OpenALAudioOut::Open(PortOut& port) {
return std::make_unique<OpenALPortBackend>(port);
}
} // namespace Libraries::AudioOut

View File

@ -0,0 +1,226 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include <AL/al.h>
#include <AL/alc.h>
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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::string> GetAvailableDevices() {
std::vector<std::string> 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<std::mutex> 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<std::string, DeviceContext> devices;
mutable std::mutex mutex;
ALCcontext* current_context{nullptr}; // For thread-local tracking
};
} // namespace Libraries::AudioOut

View File

@ -3,8 +3,8 @@
#include <cstring>
#include <SDL3/SDL.h>
#include <common/config.h>
#include <common/logging/log.h>
#include <core/emulator_settings.h>
#include "audioin.h"
#include "audioin_backend.h"
@ -21,7 +21,7 @@ public:
fmt.channels = static_cast<Uint8>(port.channels_num);
fmt.freq = static_cast<int>(port.freq);
std::string micDevStr = Config::getMicDevice();
std::string micDevStr = EmulatorSettings.GetSDLMicDevice();
uint32_t devId = 0;
if (micDevStr == "None") {
nullDevice = true;

View File

@ -9,8 +9,8 @@
#include <SDL3/SDL_audio.h>
#include <SDL3/SDL_hints.h>
#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();
}
}

View File

@ -0,0 +1,997 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <vector>
#include <magic_enum/magic_enum.hpp>
#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<Audio3dState> 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<AudioOut::OrbisAudioOutPort>(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<void*>(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<AudioData>& 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<u8*>(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<uintptr_t>(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<uintptr_t>(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<Audio3dState>();
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<void*>(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<u32>(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<u32>(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<const u8*>(attribute);
obj.persistent_attributes[static_cast<u32>(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<OrbisAudio3dPcm*>(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<const u8*>(attr.value);
obj.persistent_attributes[static_cast<u32>(attr.attribute_id)].assign(
src, src + attr.value_size);
}
LOG_DEBUG(Lib_Audio3d, "Stored attribute {:#x} for object {}",
static_cast<u32>(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<float*>(std::calloc(out_samples, sizeof(float)));
if (!mix_float)
return ORBIS_AUDIO3D_ERROR_OUT_OF_MEMORY;
auto mix_in = [&](std::deque<AudioData>& 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<const s16*>(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<const float*>(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<u32>(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<s16*>(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<s16>(v * 32767.0f);
}
std::free(mix_float);
port.mixed_queue.push_back(AudioData{.sample_buffer = reinterpret_cast<u8*>(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<void*>(queue_level), static_cast<void*>(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<u32>(size);
}
if (queue_available) {
const u32 depth = port.parameters.queue_depth;
*queue_available = (size < depth) ? static_cast<u32>(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<const void*>(parameters), static_cast<void*>(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<int>(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<u64>(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<u32>(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<OrbisAudio3dPortId> 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

View File

@ -0,0 +1,181 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include <optional>
#include <vector>
#include <queue>
#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<AudioData> pcm_queue;
std::unordered_map<u32, std::vector<u8>> 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<s32> audioout_handles;
// Reserved objects and their state.
std::unordered_map<OrbisAudio3dObjectId, ObjectState> objects;
// Increasing counter for generating unique object IDs within this port.
OrbisAudio3dObjectId next_object_id{0};
// Bed audio queue.
std::deque<AudioData> bed_queue;
// Mixed stereo frames ready to be consumed by sceAudio3dPortPush.
std::deque<AudioData> mixed_queue;
};
struct Audio3dState {
std::unordered_map<OrbisAudio3dPortId, Port> 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

View File

@ -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 <utility>
#include <thread>
#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<u16> raw16_buffer1, raw16_buffer2;
static std::vector<u8> 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, &timestampNS);
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, &timestamp);
if (!frame) {
for (int i = 0; i < 1000; i++) {
frame = SDL_AcquireCameraFrame(sdl_camera, &timestamp);
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;
}

View File

@ -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,

View File

@ -1,17 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "gnm_error.h"
#include "gnmdriver.h"
#include "common/assert.h"
#include "common/config.h"
#include "common/debug.h"
#include "common/elf_info.h"
#include "common/logging/log.h"
#include "common/slot_vector.h"
#include "core/address_space.h"
#include "core/debug_state.h"
#include "core/emulator_settings.h"
#include "core/libraries/gnmdriver/gnm_error.h"
#include "core/libraries/gnmdriver/gnmdriver_init.h"
#include "core/libraries/kernel/orbis_error.h"
@ -1172,13 +1172,14 @@ bool PS4_SYSV_ABI sceGnmIsUserPaEnabled() {
}
int PS4_SYSV_ABI sceGnmLogicalCuIndexToPhysicalCuIndex() {
LOG_ERROR(Lib_GnmDriver, "(STUBBED) called");
LOG_TRACE(Lib_GnmDriver, "called");
// Not available in retail firmware
return ORBIS_OK;
}
int PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask() {
LOG_ERROR(Lib_GnmDriver, "(STUBBED) called");
return ORBIS_OK;
s32 PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask(s64, s32 logical_cu_mask) {
LOG_INFO(Lib_GnmDriver, "called, logical_cu_mask: {}", logical_cu_mask);
return logical_cu_mask;
}
int PS4_SYSV_ABI sceGnmLogicalTcaUnitToPhysical() {
@ -2874,7 +2875,7 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
sdk_version = 0;
}
if (Config::copyGPUCmdBuffers()) {
if (EmulatorSettings.IsCopyGpuBuffers()) {
liverpool->ReserveCopyBufferSpace();
}

View File

@ -121,7 +121,7 @@ s32 PS4_SYSV_ABI sceGnmInsertWaitFlipDone(u32* cmdbuf, u32 size, s32 vo_handle,
int PS4_SYSV_ABI sceGnmIsCoredumpValid();
bool PS4_SYSV_ABI sceGnmIsUserPaEnabled();
int PS4_SYSV_ABI sceGnmLogicalCuIndexToPhysicalCuIndex();
int PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask();
s32 PS4_SYSV_ABI sceGnmLogicalCuMaskToPhysicalCuMask(s64, s32 logical_cu_mask);
int PS4_SYSV_ABI sceGnmLogicalTcaUnitToPhysical();
int PS4_SYSV_ABI sceGnmMapComputeQueue(u32 pipe_id, u32 queue_id, VAddr ring_base_addr,
u32 ring_size_dw, u32* read_ptr_addr);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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 <algorithm>
#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);

View File

@ -2,9 +2,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/assert.h"
#include "common/config.h"
#include "common/singleton.h"
#include "common/types.h"
#include "core/emulator_settings.h"
#include "core/libraries/error_codes.h"
#include "net_error.h"
#include "net_resolver.h"
@ -27,7 +27,7 @@ int Resolver::ResolveAsync(const char* hostname, OrbisNetInAddr* addr, int timeo
}
void Resolver::Resolve() {
if (!Config::getIsConnectedToNetwork()) {
if (!EmulatorSettings.IsConnectedToNetwork()) {
resolution_error = ORBIS_NET_ERROR_RESOLVER_ENODNS;
return;
}

View File

@ -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 <common/singleton.h>
#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;

View File

@ -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 <mutex>
#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);

View File

@ -5,7 +5,7 @@
#include <mutex>
#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 {

View File

@ -47,3 +47,4 @@ constexpr int ORBIS_NP_TROPHY_ERROR_INCONSISTENT_TITLE_CONF = 0x80551628;
constexpr int ORBIS_NP_TROPHY_ERROR_TITLE_BACKGROUND = 0x80551629;
constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISABLED = 0x8055162B;
constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISPLAY_BUFFER_NOT_IN_USE = 0x8055162D;
constexpr int ORBIS_NP_TROPHY_ERROR_TITLE_NOT_FOUND = 0x805516C2;

View File

@ -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;
}

View File

@ -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--;

View File

@ -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 <chrono>
@ -7,15 +7,10 @@
#include <mutex>
#include <cmrc/cmrc.hpp>
#include <imgui.h>
#ifdef ENABLE_QT_GUI
#include <qt_gui/background_music_player.h>
#endif
#include "common/assert.h"
#include "common/config.h"
#include "common/path_util.h"
#include "common/singleton.h"
#include "core/emulator_settings.h"
#include "core/libraries/np/trophy_ui.h"
#include "imgui/imgui_std.h"
@ -36,9 +31,9 @@ TrophyUI::TrophyUI(const std::filesystem::path& trophyIconPath, const std::strin
const std::string_view& rarity)
: trophy_name(trophyName), trophy_type(rarity) {
side = Config::sideTrophy();
side = EmulatorSettings.GetTrophyNotificationSide();
trophy_timer = Config::getTrophyNotificationDuration();
trophy_timer = EmulatorSettings.GetTrophyNotificationDuration();
if (std::filesystem::exists(trophyIconPath)) {
trophy_icon = RefCountedTexture::DecodePngFile(trophyIconPath);
@ -98,7 +93,7 @@ TrophyUI::TrophyUI(const std::filesystem::path& trophyIconPath, const std::strin
return;
}
MIX_SetMasterGain(mixer, static_cast<float>(Config::getVolumeSlider() / 100.f));
MIX_SetMasterGain(mixer, static_cast<float>(EmulatorSettings.GetVolumeSlider() / 100.f));
auto musicPathMp3 = CustomTrophy_Dir / "trophy.mp3";
auto musicPathWav = CustomTrophy_Dir / "trophy.wav";
@ -284,7 +279,7 @@ void AddTrophyToQueue(const std::filesystem::path& trophyIconPath, const std::st
const std::string_view& rarity) {
std::lock_guard<std::mutex> lock(queueMtx);
if (Config::getisTrophyPopupDisabled()) {
if (EmulatorSettings.IsTrophyPopupDisabled()) {
return;
} else if (current_trophy_ui.has_value()) {
current_trophy_ui.reset();

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -20,3 +20,6 @@ constexpr int ORBIS_PAD_ERROR_INVALID_BUFFER_LENGTH = 0x80920102;
constexpr int ORBIS_PAD_ERROR_INVALID_REPORT_LENGTH = 0x80920103;
constexpr int ORBIS_PAD_ERROR_INVALID_REPORT_ID = 0x80920104;
constexpr int ORBIS_PAD_ERROR_SEND_AGAIN = 0x80920105;
constexpr s32 ORBIS_DEVICE_SERVICE_ERROR_INVALID_USER = 0x809b0001;
constexpr s32 ORBIS_DEVICE_SERVICE_ERROR_USER_NOT_LOGIN = 0x809b0081;

View File

@ -2,9 +2,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/assert.h"
#include "common/config.h"
#include "common/elf_info.h"
#include "common/logging/log.h"
#include "core/emulator_settings.h"
#include "core/file_sys/fs.h"
#include "core/libraries/disc_map/disc_map.h"
#include "core/libraries/font/font.h"
@ -108,7 +108,7 @@ bool validateModuleId(s32 id) {
}
// Cannot load debug modules on retail hardware.
if (isDebugModule(id) && !Config::isDevKitConsole()) {
if (isDebugModule(id) && !EmulatorSettings.IsDevKit()) {
return ORBIS_SYSMODULE_INVALID_ID;
}
@ -154,7 +154,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) {
std::string mod_name = std::string(mod.name);
// libSceGnmDriver case
if (index == 0xd && Config::isDevKitConsole()) {
if (index == 0xd && EmulatorSettings.IsDevKit()) {
// There are some other checks involved here that I am not familiar with.
// Since we're not exactly running libSceGnmDriver LLE, this shouldn't matter too much.
mod_name.append("_padebug");
@ -168,7 +168,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) {
}
// libSceVrTracker case
if (index == 0xb3 && Config::isDevKitConsole()) {
if (index == 0xb3 && EmulatorSettings.IsDevKit()) {
mod_name.append("_debug");
}
@ -178,7 +178,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) {
// PS4 Pro running in enhanced mode
mod_name.append("ForNeoMode");
} else if ((mod.flags & OrbisSysmoduleModuleInternalFlags::IsNeo) != 0 &&
Config::isNeoModeConsole()) {
EmulatorSettings.IsNeo()) {
// PS4 Pro running in base mode
mod_name.append("ForNeo");
}
@ -188,7 +188,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) {
// Now we need to check if the requested library is allowed to LLE.
// First, we allow all modules from game-specific sys_modules
const auto& sys_module_path = Config::getSysModulesPath();
const auto& sys_module_path = EmulatorSettings.GetSysModulesDir();
const auto& game_specific_module_path =
sys_module_path / game_info->GameSerial() / mod_name;
if (std::filesystem::exists(game_specific_module_path)) {
@ -223,7 +223,8 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) {
{"libSceAudiodec.sprx", nullptr},
{"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont},
{"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt},
{"libSceFreeTypeOt.sprx", nullptr}});
{"libSceFreeTypeOt.sprx", nullptr},
{"libScePadTracker.sprx", nullptr}});
// Iterate through the allowed array
const auto it = std::ranges::find_if(
@ -299,7 +300,7 @@ s32 loadModule(s32 id, s32 argc, const void* argv, s32* res_out) {
for (s64 i = requested_module.num_to_load - 1; i >= 0; i--) {
// Modules flagged as debug modules only load for devkits
u32 mod_index = requested_module.to_load[i];
if ((!Config::isDevKitConsole() &&
if ((!EmulatorSettings.IsDevKit() &&
g_modules_array[mod_index].flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0) {
continue;
}
@ -361,7 +362,7 @@ s32 unloadModule(s32 id, s32 argc, const void* argv, s32* res_out, bool is_inter
OrbisSysmoduleModuleInternal dep_mod = g_modules_array[mod.to_load[i]];
// If this is a debug module and we're not emulating a devkit, skip it.
if ((dep_mod.flags & OrbisSysmoduleModuleInternalFlags::IsDebug) != 0 &&
!Config::isDevKitConsole()) {
!EmulatorSettings.IsDevKit()) {
continue;
}
@ -398,7 +399,7 @@ s32 preloadModulesForLibkernel() {
// These are skipped unless this console is a devkit.
if ((module_index == 0x12 || module_index == 0x1e || module_index == 0x24 ||
module_index == 0x26) &&
!Config::isDevKitConsole()) {
!EmulatorSettings.IsDevKit()) {
continue;
}
@ -409,13 +410,13 @@ s32 preloadModulesForLibkernel() {
// libSceDbgAssist is skipped on non-testkit consoles.
// For now, stub check to non-devkit.
if (module_index == 0x23 && !Config::isDevKitConsole()) {
if (module_index == 0x23 && !EmulatorSettings.IsDevKit()) {
continue;
}
// libSceRazorCpu, skipped for old non-devkit consoles.
if (module_index == 0x25 && sdk_ver < Common::ElfInfo::FW_45 &&
!Config::isDevKitConsole()) {
!EmulatorSettings.IsDevKit()) {
continue;
}

View File

@ -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 <cstdlib>
#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<OrbisSystemServiceEvent> 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);

View File

@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/config.h"
#include "common/logging/log.h"
#include "common/singleton.h"
#include "core/libraries/libs.h"

View File

@ -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 <fmt/format.h>
#include <libusb.h>
#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<SkylandersPortalBackend>();
break;
case Config::InfinityBase:
case UsbBackendType::InfinityBase:
usb_backend = std::make_shared<InfinityBaseBackend>();
break;
case Config::DimensionsToypad:
case UsbBackendType::DimensionsToypad:
usb_backend = std::make_shared<DimensionsToypadBackend>();
break;
default:

View File

@ -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);

View File

@ -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<VideoOutDriver>(Config::getInternalScreenWidth(),
Config::getInternalScreenHeight());
driver = std::make_unique<VideoOutDriver>(EmulatorSettings.GetInternalScreenWidth(),
EmulatorSettings.GetInternalScreenHeight());
LIB_FUNCTION("SbU3dwp80lQ", "libSceVideoOut", 1, "libSceVideoOut", sceVideoOutGetFlipStatus);
LIB_FUNCTION("U46NwOiJpys", "libSceVideoOut", 1, "libSceVideoOut", sceVideoOutSubmitFlip);

View File

@ -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<std::string>& args) {
if (Config::debugDump()) {
if (EmulatorSettings.IsDebugDump()) {
DebugDump();
}

View File

@ -3,9 +3,9 @@
#include "common/alignment.h"
#include "common/assert.h"
#include "common/config.h"
#include "common/debug.h"
#include "common/elf_info.h"
#include "core/emulator_settings.h"
#include "core/file_sys/fs.h"
#include "core/libraries/kernel/memory.h"
#include "core/libraries/kernel/orbis_error.h"
@ -37,11 +37,11 @@ void MemoryManager::SetupMemoryRegions(u64 flexible_size, bool use_extended_mem1
bool use_extended_mem2) {
const bool is_neo = ::Libraries::Kernel::sceKernelIsNeoMode();
auto total_size = is_neo ? ORBIS_KERNEL_TOTAL_MEM_PRO : ORBIS_KERNEL_TOTAL_MEM;
if (Config::isDevKitConsole()) {
if (EmulatorSettings.IsDevKit()) {
total_size = is_neo ? ORBIS_KERNEL_TOTAL_MEM_DEV_PRO : ORBIS_KERNEL_TOTAL_MEM_DEV;
}
s32 extra_dmem = Config::getExtraDmemInMbytes();
if (Config::getExtraDmemInMbytes() != 0) {
s32 extra_dmem = EmulatorSettings.GetExtraDmemInMBytes();
if (extra_dmem != 0) {
LOG_WARNING(Kernel_Vmm,
"extraDmemInMbytes is {} MB! Old Direct Size: {:#x} -> New Direct Size: {:#x}",
extra_dmem, total_size, total_size + extra_dmem * 1_MB);
@ -1223,13 +1223,16 @@ s32 MemoryManager::SetDirectMemoryType(VAddr addr, u64 size, s32 memory_type) {
// Increment phys_handle
phys_handle++;
}
// Check if VMA can be merged with adjacent areas after physical area modifications.
vma_handle = MergeAdjacent(vma_map, vma_handle);
}
current_addr += size_in_vma;
remaining_size -= size_in_vma;
vma_handle++;
// Check if VMA can be merged with adjacent areas after modifications.
vma_handle = MergeAdjacent(vma_map, vma_handle);
if (vma_handle->second.base + vma_handle->second.size <= current_addr) {
// If we're now in the next VMA, then go to the next handle.
vma_handle++;
}
}
return ORBIS_OK;
@ -1262,10 +1265,15 @@ void MemoryManager::NameVirtualRange(VAddr virtual_addr, u64 size, std::string_v
vma.name = name;
}
}
it = MergeAdjacent(vma_map, it);
remaining_size -= size_in_vma;
current_addr += size_in_vma;
it++;
// Check if VMA can be merged with adjacent areas after modifications.
it = MergeAdjacent(vma_map, it);
if (it->second.base + it->second.size <= current_addr) {
// If we're now in the next VMA, then go to the next handle.
it++;
}
}
}

View File

@ -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 <string>
#include <vector>
#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;
}

177
src/core/user_manager.cpp Normal file
View File

@ -0,0 +1,177 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <filesystem>
#include <iostream>
#include <common/path_util.h>
#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<User>& 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<User> UserManager::GetValidUsers() const {
std::vector<User> 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();
}

58
src/core/user_manager.h Normal file
View File

@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#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> 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<User*, 4>;
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<User>& GetAllUsers() const;
Users CreateDefaultUsers();
bool SetDefaultUser(u32 user_id);
User GetDefaultUser();
void SetControllerPort(u32 user_id, int port);
std::vector<User> 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{};
};

110
src/core/user_settings.cpp Normal file
View File

@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <map>
#include <common/path_util.h>
#include <common/scm_rev.h>
#include "common/logging/log.h"
#include "user_settings.h"
using json = nlohmann::json;
// Singleton storage
std::shared_ptr<UserSettingsImpl> UserSettingsImpl::s_instance = nullptr;
std::mutex UserSettingsImpl::s_mutex;
// Singleton
UserSettingsImpl::UserSettingsImpl() = default;
UserSettingsImpl::~UserSettingsImpl() {
Save();
}
std::shared_ptr<UserSettingsImpl> UserSettingsImpl::GetInstance() {
std::lock_guard lock(s_mutex);
if (!s_instance)
s_instance = std::make_shared<UserSettingsImpl>();
return s_instance;
}
void UserSettingsImpl::SetInstance(std::shared_ptr<UserSettingsImpl> 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<Users>();
} 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;
}
}

46
src/core/user_settings.h Normal file
View File

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <functional>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#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<UserSettingsImpl> GetInstance();
static void SetInstance(std::shared_ptr<UserSettingsImpl> instance);
private:
UserManager m_userManager;
static std::shared_ptr<UserSettingsImpl> s_instance;
static std::mutex s_mutex;
};

View File

@ -10,11 +10,11 @@
#include <fmt/xchar.h>
#include <hwinfo/hwinfo.h>
#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<std::string> 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<std::string> 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<std::string> args,
Common::g_scm_branch, Common::g_scm_desc, game_title);
}
}
window = std::make_unique<Frontend::WindowSDL>(
Config::getWindowWidth(), Config::getWindowHeight(), controller, window_title);
window = std::make_unique<Frontend::WindowSDL>(EmulatorSettings.GetWindowWidth(),
EmulatorSettings.GetWindowHeight(), controller,
window_title);
g_window = window.get();
@ -360,7 +363,7 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> 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<std::string> args,
#ifdef ENABLE_DISCORD_RPC
// Discord RPC
if (Config::getEnableDiscordRPC()) {
if (EmulatorSettings.IsDiscordRPCEnabled()) {
auto* rpc = Common::Singleton<DiscordRPCHandler::RPC>::Instance();
if (rpc->getRPCEnabled() == false) {
rpc->init();

View File

@ -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 <SDL3/SDL_events.h>
#include <imgui.h>
#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();
}
}

View File

@ -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 <imgui.h>
#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();

View File

@ -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 <deque>
@ -6,11 +6,11 @@
#include <imgui.h>
#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;
}

View File

@ -9,6 +9,7 @@
#include <CLI/CLI.hpp>
#include <SDL3/SDL_messagebox.h>
#include <core/emulator_settings.h>
#include <core/emulator_state.h>
#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 <windows.h>
#endif
#include <core/user_settings.h>
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>();
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<EmulatorSettingsImpl> emu_settings = std::make_shared<EmulatorSettingsImpl>();
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;

View File

@ -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();
@ -323,6 +326,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_
window, Config::getFullscreenMode() == "Fullscreen" ? displayMode : NULL);
}
SDL_SetWindowFullscreen(window, Config::getIsFullscreen());
SDL_SyncWindow(window);
SDL_InitSubSystem(SDL_INIT_GAMEPAD);
controller->SetEngine(std::make_unique<Input::SDLInputEngine>());

View File

@ -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<u32>();
if (!Config::directMemoryAccess()) {
if (!EmulatorSettings.IsDirectMemoryAccessEnabled()) {
return ctx.EmitFlatbufferLoad(ctx.ConstU32(flatbuf_off_dw));
}
// We can only provide a fallback for immediate offsets.

View File

@ -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)) {

View File

@ -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 <unordered_map>
#include <boost/container/flat_map.hpp>
#include <xbyak/xbyak.h>
#include <xbyak/xbyak_util.h>
#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<const u8*>(info.srt_info.walker_func);
if (Config::dumpShaders()) {
if (EmulatorSettings.IsDumpShaders()) {
DumpSrtProgram(info, reinterpret_cast<const u8*>(info.srt_info.walker_func),
info.srt_info.walker_func_size);
}

View File

@ -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;
}

View File

@ -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 <map>
@ -6,9 +7,9 @@
#include <fmt/format.h>
#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;
}

View File

@ -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 <boost/preprocessor/stringize.hpp>
#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<const u32> dcb, std::span<c
ce_task = ProcessCeUpdate(ccb);
RESUME_GFX(ce_task);
}
const bool host_markers_enabled = rasterizer && Config::getVkHostMarkersEnabled();
const bool guest_markers_enabled = rasterizer && Config::getVkGuestMarkersEnabled();
const bool host_markers_enabled = rasterizer && EmulatorSettings.IsVkHostMarkersEnabled();
const bool guest_markers_enabled = rasterizer && EmulatorSettings.IsVkGuestMarkersEnabled();
const auto base_addr = reinterpret_cast<uintptr_t>(dcb.data());
while (!dcb.empty()) {
@ -899,7 +899,7 @@ template <bool is_indirect>
Liverpool::Task Liverpool::ProcessCompute(std::span<const u32> 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<const u32> dcb, std::sp
void Liverpool::SubmitGfx(std::span<const u32> dcb, std::span<const u32> ccb) {
auto& queue = mapped_queues[GfxQueueId];
if (Config::copyGPUCmdBuffers()) {
if (EmulatorSettings.IsCopyGpuBuffers()) {
std::tie(dcb, ccb) = CopyCmdBuffers(dcb, ccb);
}

View File

@ -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<Type::GPU>(offset, size)) {
return true;
}

View File

@ -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<!enable, false>();
} else if (Config::getReadbacksMode() == Config::GpuReadbacksMode::Precise) {
} else if (EmulatorSettings.GetReadbacksMode() == GpuReadbacksMode::Precise) {
UpdateProtection<enable, true>();
}
}
@ -126,7 +126,7 @@ public:
bits.UnsetRange(start_page, end_page);
if constexpr (type == Type::CPU) {
UpdateProtection<true, false>();
} else if (Config::getReadbacksMode() != Config::GpuReadbacksMode::Disabled) {
} else if (EmulatorSettings.GetReadbacksMode() != GpuReadbacksMode::Disabled) {
UpdateProtection<false, true>();
}
}

View File

@ -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<void()>{[=]() {
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 <typename T>
void LoadVector(BlobType type, std::filesystem::path& path, std::vector<T>& 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<u8>&& 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<u32>&& 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<u8>& 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<u32>& 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<void(std::vector<u8>&& 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<char, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE> file_name{};
@ -255,7 +259,7 @@ void DataBase::ForEachBlob(BlobType type, const std::function<void(std::vector<u
}
void DataBase::FinishPreload() {
if (Config::isPipelineCacheArchived()) {
if (EmulatorSettings.IsPipelineCacheArchived()) {
mz_zip_writer_init_from_reader(&zip_ar, cache_path.string().c_str());
ar_is_read_only = false;
}

View File

@ -1,8 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/assert.h"
#include "common/config.h"
#include "core/emulator_settings.h"
#include "video_core/renderdoc.h"
#include <renderdoc_app.h>
@ -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 =

View File

@ -1,8 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/assert.h"
#include "common/config.h"
#include "core/emulator_settings.h"
#include "video_core/host_shaders/fsr_comp.h"
#include "video_core/renderer_vulkan/host_passes/fsr_pass.h"
#include "video_core/renderer_vulkan/vk_platform.h"
@ -164,7 +164,7 @@ vk::ImageView FsrPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input,
CreateImages(img);
}
if (Config::getVkHostMarkersEnabled()) {
if (EmulatorSettings.IsVkHostMarkersEnabled()) {
cmdbuf.beginDebugUtilsLabelEXT(vk::DebugUtilsLabelEXT{
.pLabelName = "Host/FSR",
});
@ -387,7 +387,7 @@ vk::ImageView FsrPass::Render(vk::CommandBuffer cmdbuf, vk::ImageView input,
.pImageMemoryBarriers = return_barrier.data(),
});
if (Config::getVkHostMarkersEnabled()) {
if (EmulatorSettings.IsVkHostMarkersEnabled()) {
cmdbuf.endDebugUtilsLabelEXT();
}
@ -407,6 +407,13 @@ void FsrPass::ResizeAndInvalidate(u32 width, u32 height) {
void FsrPass::CreateImages(Img& img) const {
img.dirty = false;
// Destroy previous resources before re-creating at new size.
// Views first, then images (views reference the images).
img.intermediary_image_view.reset();
img.output_image_view.reset();
img.intermediary_image.Destroy();
img.output_image.Destroy();
vk::ImageCreateInfo image_create_info{
.imageType = vk::ImageType::e2D,
.format = vk::Format::eR16G16B16A16Sfloat,

View File

@ -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();
}
}

View File

@ -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 <ranges>
#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<const u32>{}, is_patched);
}
@ -659,7 +659,7 @@ std::string PipelineCache::GetShaderName(Shader::Stage stage, u64 hash,
void PipelineCache::DumpShader(std::span<const u32> code, u64 hash, Shader::Stage stage,
size_t perm_idx, std::string_view ext) {
if (!Config::dumpShaders()) {
if (!EmulatorSettings.IsDumpShaders()) {
return;
}

View File

@ -1,8 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/config.h"
#include "common/serdes.h"
#include "core/emulator_settings.h"
#include "shader_recompiler/frontend/fetch_shader.h"
#include "shader_recompiler/info.h"
#include "video_core/cache_storage.h"
@ -295,7 +295,7 @@ bool PipelineCache::LoadPipelineStage(Serialization::Archive& ar, size_t stage)
}
void PipelineCache::WarmUp() {
if (!Config::isPipelineCacheEnabled()) {
if (!EmulatorSettings.IsPipelineCacheEnabled()) {
return;
}

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Include the vulkan platform specific header
@ -17,9 +17,9 @@
#include <fmt/ranges.h>
#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<const char*> 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 =

View File

@ -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<Rasterizer>(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<float>(Config::getRcasAttenuation() / 1000.f);
fsr_settings.enable = EmulatorSettings.IsFsrEnabled();
fsr_settings.use_rcas = EmulatorSettings.IsRcasEnabled();
fsr_settings.rcas_attenuation =
static_cast<float>(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();
}

View File

@ -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();

View File

@ -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 <algorithm>
#include <limits>
#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;
}

View File

@ -82,6 +82,14 @@ UniqueImage::~UniqueImage() {
}
}
void UniqueImage::Destroy() {
if (image) {
vmaDestroyImage(allocator, image, allocation);
image = vk::Image{};
allocation = {};
}
}
void UniqueImage::Create(const vk::ImageCreateInfo& image_ci) {
this->image_ci = image_ci;
ASSERT(!image);
@ -239,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<bool>(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<bool>(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,
@ -275,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<bool>(dst_mask & write_flags);
constexpr auto write_flags = vk::AccessFlagBits2::eTransferWrite |
vk::AccessFlagBits2::eShaderWrite |
vk::AccessFlagBits2::eMemoryWrite;
const bool is_write = static_cast<bool>(last_state.access_mask & write_flags);
if (last_state.layout == dst_layout && last_state.access_mask == dst_mask && !is_write) {
return {};
}
@ -377,6 +382,8 @@ void Image::Upload(std::span<const vk::BufferImageCopy> upload_copies, vk::Buffe
.bufferMemoryBarrierCount = 1,
.pBufferMemoryBarriers = &post_barrier,
});
Transit(vk::ImageLayout::eGeneral,
vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {});
flags &= ~ImageFlagBits::Dirty;
}
@ -647,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, {});
}
@ -690,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,

View File

@ -59,6 +59,8 @@ struct UniqueImage {
void Create(const vk::ImageCreateInfo& image_ci);
void Destroy();
operator vk::Image() const {
return image;
}

View File

@ -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 <xxhash.h>
#include "common/assert.h"
#include "common/config.h"
#include "common/debug.h"
#include "common/scope_exit.h"
#include "core/emulator_settings.h"
#include "core/memory.h"
#include "video_core/buffer_cache/buffer_cache.h"
#include "video_core/page_manager.h"
@ -641,7 +641,7 @@ ImageView& TextureCache::FindTexture(ImageId image_id, const ImageDesc& desc) {
Image& image = slot_images[image_id];
if (desc.type == BindingType::Storage) {
image.flags |= ImageFlagBits::GpuModified;
if (Config::readbackLinearImages() && !image.info.props.is_tiled &&
if (EmulatorSettings.IsReadbackLinearImagesEnabled() && !image.info.props.is_tiled &&
image.info.guest_address != 0) {
download_images.emplace(image_id);
}
@ -653,7 +653,7 @@ ImageView& TextureCache::FindTexture(ImageId image_id, const ImageDesc& desc) {
ImageView& TextureCache::FindRenderTarget(ImageId image_id, const ImageDesc& desc) {
Image& image = slot_images[image_id];
image.flags |= ImageFlagBits::GpuModified;
if (Config::readbackLinearImages() && !image.info.props.is_tiled) {
if (EmulatorSettings.IsReadbackLinearImagesEnabled() && !image.info.props.is_tiled) {
download_images.emplace(image_id);
}
image.usage.render_target = 1u;