From 923d1b1ab6eb000494c16c96a36a10addb961797 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 4 Feb 2026 02:03:02 -0600 Subject: [PATCH 01/20] Libs: Miscellaneous fixes (#3993) * Fixed sceCompanionUtilGetEvent stub Previously we effectively stubbed with ORBIS_COMPANION_UTIL_INVALID_POINTER, which makes no sense and caused issues in games. * Check for null issuer_id in libSceNpAuth's GetAuthorizationCode Comes up in Mirror's Edge Catalyst, according to some debugging done by a community member. Given the library didn't have any null checks for that value, this is probably allowed. --- src/core/libraries/companion/companion_util.cpp | 5 ++--- src/core/libraries/np/np_auth.cpp | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/libraries/companion/companion_util.cpp b/src/core/libraries/companion/companion_util.cpp index 758cfface..2794c49af 100644 --- a/src/core/libraries/companion/companion_util.cpp +++ b/src/core/libraries/companion/companion_util.cpp @@ -29,10 +29,9 @@ u32 PS4_SYSV_ABI getEvent(sceCompanionUtilContext* ctx, sceCompanionUtilEvent* o } s32 PS4_SYSV_ABI sceCompanionUtilGetEvent(sceCompanionUtilEvent* outEvent) { - sceCompanionUtilContext* ctx = nullptr; - u32 ret = getEvent(ctx, outEvent, 1); + u32 ret = ORBIS_COMPANION_UTIL_NO_EVENT; - LOG_DEBUG(Lib_CompanionUtil, "(STUBBED) called ret: {}", ret); + LOG_DEBUG(Lib_CompanionUtil, "(STUBBED) called ret: {:#x}", ret); return ret; } diff --git a/src/core/libraries/np/np_auth.cpp b/src/core/libraries/np/np_auth.cpp index 0c855546c..b6091723c 100644 --- a/src/core/libraries/np/np_auth.cpp +++ b/src/core/libraries/np/np_auth.cpp @@ -123,7 +123,9 @@ s32 GetAuthorizationCode(s32 req_id, const OrbisNpAuthGetAuthorizationCodeParame // Not sure what values are expected here, so zeroing these for now. std::memset(auth_code, 0, sizeof(OrbisNpAuthorizationCode)); - *issuer_id = 0; + if (issuer_id != nullptr) { + *issuer_id = 0; + } return ORBIS_OK; } From 1fe72cfe1fe052fe2924c183a6235d9807da9ba2 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:39:37 +0100 Subject: [PATCH 02/20] filesystem: fill in timespec values for fstat (#3994) * filesystem: fill in timespec values for fstat * w*ndows * fix windows and mac --- src/core/libraries/kernel/file_system.cpp | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index 4b5a53266..bc4e2def6 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -34,6 +34,7 @@ #include #else #include +#include #endif namespace D = Core::Devices; @@ -751,6 +752,30 @@ s32 PS4_SYSV_ABI fstat(s32 fd, OrbisKernelStat* sb) { sb->st_size = file->f.GetSize(); sb->st_blksize = 512; sb->st_blocks = (sb->st_size + 511) / 512; +#if defined(__linux__) || defined(__FreeBSD__) + struct stat filestat = {}; + stat(file->f.GetPath().c_str(), &filestat); + sb->st_atim = *reinterpret_cast(&filestat.st_atim); + sb->st_mtim = *reinterpret_cast(&filestat.st_mtim); + sb->st_ctim = *reinterpret_cast(&filestat.st_ctim); +#elif defined(__APPLE__) + struct stat filestat = {}; + stat(file->f.GetPath().c_str(), &filestat); + sb->st_atim = *reinterpret_cast(&filestat.st_atimespec); + sb->st_mtim = *reinterpret_cast(&filestat.st_mtimespec); + sb->st_ctim = *reinterpret_cast(&filestat.st_ctimespec); +#else + const auto ft = std::filesystem::last_write_time(file->f.GetPath()); + const auto sctp = std::chrono::time_point_cast( + ft - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); + const auto secs = std::chrono::time_point_cast(sctp); + const auto nsecs = std::chrono::duration_cast(sctp - secs); + + sb->st_mtim.tv_sec = static_cast(secs.time_since_epoch().count()); + sb->st_mtim.tv_nsec = static_cast(nsecs.count()); + sb->st_atim = sb->st_mtim; + sb->st_ctim = sb->st_mtim; +#endif // TODO incomplete break; } From b88ec484bafcee9bf8b4a6cd7bad8520ea6f981e Mon Sep 17 00:00:00 2001 From: Vladislav Mikhalin Date: Fri, 6 Feb 2026 00:16:22 +0300 Subject: [PATCH 03/20] ajm mp3: check frame size on every frame (#3995) --- src/core/libraries/ajm/ajm_mp3.cpp | 28 ++++++++++++++-------------- src/core/libraries/ajm/ajm_mp3.h | 1 - 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/core/libraries/ajm/ajm_mp3.cpp b/src/core/libraries/ajm/ajm_mp3.cpp index f4ce22b8b..d1c9374cc 100644 --- a/src/core/libraries/ajm/ajm_mp3.cpp +++ b/src/core/libraries/ajm/ajm_mp3.cpp @@ -1,9 +1,11 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "ajm_error.h" +#include "ajm_mp3.h" +#include "ajm_result.h" + #include "common/assert.h" -#include "core/libraries/ajm/ajm_error.h" -#include "core/libraries/ajm/ajm_mp3.h" #include "core/libraries/error_codes.h" extern "C" { @@ -122,7 +124,6 @@ void AjmMp3Decoder::Reset() { avcodec_flush_buffers(m_codec_context); m_header.reset(); m_frame_samples = 0; - m_frame_size = 0; } void AjmMp3Decoder::GetInfo(void* out_info) const { @@ -141,7 +142,7 @@ void AjmMp3Decoder::GetInfo(void* out_info) const { u32 AjmMp3Decoder::GetMinimumInputSize() const { // 4 bytes is for mp3 header that contains frame_size - return std::max(m_frame_size, 4); + return 4; } DecoderResult AjmMp3Decoder::ProcessData(std::span& in_buf, SparseOutputBuffer& output, @@ -149,12 +150,11 @@ DecoderResult AjmMp3Decoder::ProcessData(std::span& in_buf, SparseOutputBuff DecoderResult result{}; AVPacket* pkt = av_packet_alloc(); - if ((!m_header.has_value() || m_frame_samples == 0) && in_buf.size() >= 4) { - m_header = std::byteswap(*reinterpret_cast(in_buf.data())); - AjmDecMp3ParseFrame info{}; - ParseMp3Header(in_buf.data(), in_buf.size(), true, &info); - m_frame_samples = info.samples_per_channel; - m_frame_size = info.frame_size; + m_header = std::byteswap(*reinterpret_cast(in_buf.data())); + AjmDecMp3ParseFrame info{}; + ParseMp3Header(in_buf.data(), in_buf.size(), true, &info); + m_frame_samples = info.samples_per_channel; + if (info.total_samples != 0 || info.encoder_delay != 0) { gapless.init = { .total_samples = info.total_samples, .skip_samples = static_cast(info.encoder_delay), @@ -163,6 +163,10 @@ DecoderResult AjmMp3Decoder::ProcessData(std::span& in_buf, SparseOutputBuff gapless.current = gapless.init; } + if (in_buf.size() < info.frame_size) { + result.result |= ORBIS_AJM_RESULT_PARTIAL_INPUT; + } + int ret = av_parser_parse2(m_parser, m_codec_context, &pkt->data, &pkt->size, in_buf.data(), in_buf.size(), AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); ASSERT_MSG(ret >= 0, "Error while parsing {}", ret); @@ -424,11 +428,7 @@ int AjmMp3Decoder::ParseMp3Header(const u8* p_begin, u32 stream_size, int parse_ frame->encoder_delay = std::byteswap(*reinterpret_cast(p_fgh + 1)); frame->total_samples = std::byteswap(*reinterpret_cast(p_fgh + 3)); frame->ofl_type = AjmDecMp3OflType::Fgh; - } else { - LOG_ERROR(Lib_Ajm, "FGH header CRC is incorrect."); } - } else { - LOG_ERROR(Lib_Ajm, "Could not find vendor header."); } } diff --git a/src/core/libraries/ajm/ajm_mp3.h b/src/core/libraries/ajm/ajm_mp3.h index ecbc77051..1113e222a 100644 --- a/src/core/libraries/ajm/ajm_mp3.h +++ b/src/core/libraries/ajm/ajm_mp3.h @@ -99,7 +99,6 @@ private: SwrContext* m_swr_context = nullptr; std::optional m_header; u32 m_frame_samples = 0; - u32 m_frame_size = 0; }; } // namespace Libraries::Ajm From f911ed23a93f79474cde6fb630f6add13a09c13c Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:38:44 -0600 Subject: [PATCH 04/20] Kernel.Vmm: Fix bug with VMA physical area tracking (#3998) * Fix bug with "phys_addr_to_search" logic By improperly updating the variable, some games would mark the same dmem area as mapped in different parts of the vma, and the dmem map wouldn't properly reflect the state of the vma's phys areas. * Clang * Oops --- src/core/memory.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 90759c6cd..561e72617 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -593,7 +593,10 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo // Tracy memory tracking breaks from merging memory areas. Disabled for now. // TRACK_ALLOC(out_addr, size_to_map, "VMEM"); + // Merge this handle with adjacent areas handle = MergeAdjacent(fmem_map, new_fmem_handle); + + // Get the next flexible area. current_addr += size_to_map; remaining_size -= size_to_map; flexible_usage += size_to_map; @@ -602,13 +605,13 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo ASSERT_MSG(remaining_size == 0, "Failed to map physical memory"); } else if (type == VMAType::Direct) { // Map the physical memory for this direct memory mapping. - auto phys_addr_to_search = phys_addr; + auto current_phys_addr = phys_addr; u64 remaining_size = size; auto dmem_area = FindDmemArea(phys_addr); while (dmem_area != dmem_map.end() && remaining_size > 0) { // Carve a new dmem area in place of this one with the appropriate type. // Ensure the carved area only covers the current dmem area. - const auto start_phys_addr = std::max(phys_addr, dmem_area->second.base); + const auto start_phys_addr = std::max(current_phys_addr, dmem_area->second.base); const auto offset_in_dma = start_phys_addr - dmem_area->second.base; const auto size_in_dma = std::min(dmem_area->second.size - offset_in_dma, remaining_size); @@ -617,17 +620,17 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo new_dmem_area.dma_type = PhysicalMemoryType::Mapped; // Add the dmem area to this vma, merge it with any similar tracked areas. - new_vma.phys_areas[phys_addr_to_search - phys_addr] = dmem_handle->second; - MergeAdjacent(new_vma.phys_areas, - new_vma.phys_areas.find(phys_addr_to_search - phys_addr)); + const u64 offset_in_vma = current_phys_addr - phys_addr; + new_vma.phys_areas[offset_in_vma] = dmem_handle->second; + MergeAdjacent(new_vma.phys_areas, new_vma.phys_areas.find(offset_in_vma)); // Merge the new dmem_area with dmem_map MergeAdjacent(dmem_map, dmem_handle); // Get the next relevant dmem area. - phys_addr_to_search = phys_addr + size_in_dma; + current_phys_addr += size_in_dma; remaining_size -= size_in_dma; - dmem_area = FindDmemArea(phys_addr_to_search); + dmem_area = FindDmemArea(current_phys_addr); } ASSERT_MSG(remaining_size == 0, "Failed to map physical memory"); } From f0d23eb2d2a897e950bb58eeed253d81167d5a68 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 7 Feb 2026 12:35:50 +0200 Subject: [PATCH 05/20] tagged 0.14.0 --- CMakeLists.txt | 8 ++++---- dist/net.shadps4.shadPS4.metainfo.xml | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e123f3d9..b7eac04a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,13 +202,13 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") -set(EMULATOR_VERSION_MINOR "13") -set(EMULATOR_VERSION_PATCH "1") +set(EMULATOR_VERSION_MINOR "14") +set(EMULATOR_VERSION_PATCH "0") set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}") -set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH} WIP") -set(APP_IS_RELEASE false) +set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") +set(APP_IS_RELEASE true) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/common/scm_rev.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp" @ONLY) message("-- end git things, remote: ${GIT_REMOTE_NAME}, branch: ${GIT_BRANCH}, link: ${GIT_REMOTE_URL}") diff --git a/dist/net.shadps4.shadPS4.metainfo.xml b/dist/net.shadps4.shadPS4.metainfo.xml index d2a6747d9..210ca1c5e 100644 --- a/dist/net.shadps4.shadPS4.metainfo.xml +++ b/dist/net.shadps4.shadPS4.metainfo.xml @@ -38,6 +38,9 @@ Game + + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.14.0 + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.13.0 From e39529cab33a660f39a7c655802d5de5d775300a Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 7 Feb 2026 12:41:55 +0200 Subject: [PATCH 06/20] started 0.14.1 WIP --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b7eac04a8..2afe3a49e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,12 +203,12 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") set(EMULATOR_VERSION_MINOR "14") -set(EMULATOR_VERSION_PATCH "0") +set(EMULATOR_VERSION_PATCH "1") set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}") -set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") -set(APP_IS_RELEASE true) +set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH} WIP") +set(APP_IS_RELEASE false) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/common/scm_rev.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp" @ONLY) message("-- end git things, remote: ${GIT_REMOTE_NAME}, branch: ${GIT_BRANCH}, link: ${GIT_REMOTE_URL}") From ef5b40c70ba61dd770a572d03a33c089da96d78d Mon Sep 17 00:00:00 2001 From: Alexandre Bouvier Date: Sat, 7 Feb 2026 14:22:32 +0000 Subject: [PATCH 07/20] cmake: prefer system cli11 library (#3999) --- CMakeLists.txt | 1 + externals/CMakeLists.txt | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2afe3a49e..dc59f1af1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -221,6 +221,7 @@ endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") find_package(Boost 1.84.0 CONFIG) +find_package(CLI11 2.6.1 CONFIG) find_package(FFmpeg 5.1.2 MODULE) find_package(fmt 10.2.0 CONFIG) find_package(glslang 15 CONFIG) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index db03e7679..80a6ff7e2 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -271,7 +271,8 @@ add_subdirectory(json) add_subdirectory(miniz) # cli11 -set(CLI11_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) - -add_subdirectory(CLI11) \ No newline at end of file +if (NOT TARGET CLI11::CLI11) + set(CLI11_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + add_subdirectory(CLI11) +endif() From 0f9cc89ae5b7eaa0d7563a9420515f0394ed613f Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 7 Feb 2026 16:38:45 +0200 Subject: [PATCH 08/20] Improved sceAudioOut and SDL3 backend (#3984) * improved sdl backend * small cleanups * misc * adjustments and new definations * cleanups * more debuging * rewrote sceAudioOut calls * fixed a trace * fixed audio3d port * small debug fixes * small additions * using shared_ptr * compile fixes * make macOS happy * using shared mutex * implemented audio input backend * fixed port construction based on decompile * implemented partially sceAudioInGetSilentState to return correct code if mic device is null * improved sdl volume handling * dynamic volume update * this one is for @UltraDaCat --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> --- CMakeLists.txt | 7 +- src/common/config.cpp | 3 + src/core/libraries/audio/audioin.cpp | 280 ++++- src/core/libraries/audio/audioin.h | 24 +- src/core/libraries/audio/audioin_backend.h | 31 + src/core/libraries/audio/audioin_error.h | 20 + src/core/libraries/audio/audioout.cpp | 1289 +++++++++++++------- src/core/libraries/audio/audioout.h | 184 +-- src/core/libraries/audio/sdl_audio.cpp | 156 --- src/core/libraries/audio/sdl_audio_in.cpp | 135 ++ src/core/libraries/audio/sdl_audio_out.cpp | 455 +++++++ src/core/libraries/audio/sdl_in.cpp | 139 --- src/core/libraries/audio/sdl_in.h | 42 - src/input/input_handler.cpp | 14 +- src/input/input_handler.h | 4 + 15 files changed, 1913 insertions(+), 870 deletions(-) create mode 100644 src/core/libraries/audio/audioin_backend.h create mode 100644 src/core/libraries/audio/audioin_error.h delete mode 100644 src/core/libraries/audio/sdl_audio.cpp create mode 100644 src/core/libraries/audio/sdl_audio_in.cpp create mode 100644 src/core/libraries/audio/sdl_audio_out.cpp delete mode 100644 src/core/libraries/audio/sdl_in.cpp delete mode 100644 src/core/libraries/audio/sdl_in.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dc59f1af1..254d7059c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -282,15 +282,16 @@ set(AJM_LIB src/core/libraries/ajm/ajm.cpp set(AUDIO_LIB src/core/libraries/audio/audioin.cpp src/core/libraries/audio/audioin.h - src/core/libraries/audio/sdl_in.h - src/core/libraries/audio/sdl_in.cpp + src/core/libraries/audio/audioin_backend.h + src/core/libraries/audio/audioin_error.h + src/core/libraries/audio/sdl_audio_in.cpp src/core/libraries/voice/voice.cpp src/core/libraries/voice/voice.h src/core/libraries/audio/audioout.cpp src/core/libraries/audio/audioout.h src/core/libraries/audio/audioout_backend.h src/core/libraries/audio/audioout_error.h - src/core/libraries/audio/sdl_audio.cpp + src/core/libraries/audio/sdl_audio_out.cpp src/core/libraries/ngs2/ngs2.cpp src/core/libraries/ngs2/ngs2.h ) diff --git a/src/common/config.cpp b/src/common/config.cpp index eac463d0a..aece6916f 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -1299,6 +1299,9 @@ hotkey_toggle_mouse_to_joystick = f7 hotkey_toggle_mouse_to_gyro = f6 hotkey_toggle_mouse_to_touchpad = delete hotkey_quit = lctrl, lshift, end + +hotkey_volume_up = kpplus +hotkey_volume_down = kpminus )"; } diff --git a/src/core/libraries/audio/audioin.cpp b/src/core/libraries/audio/audioin.cpp index 563b5ae14..55c2891b2 100644 --- a/src/core/libraries/audio/audioin.cpp +++ b/src/core/libraries/audio/audioin.cpp @@ -1,23 +1,264 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include "audioin_backend.h" +#include "audioin_error.h" #include "common/logging/log.h" #include "core/libraries/audio/audioin.h" -#include "core/libraries/audio/sdl_in.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" namespace Libraries::AudioIn { -static std::unique_ptr audio = std::make_unique(); +std::array, ORBIS_AUDIO_IN_NUM_PORTS> port_table{}; +std::shared_mutex port_table_mutex; +std::mutex port_allocation_mutex; -int PS4_SYSV_ABI sceAudioInChangeAppModuleState() { - LOG_ERROR(Lib_AudioIn, "(STUBBED) called"); - return ORBIS_OK; +static std::unique_ptr audio; + +/* + * Helper functions + **/ +static int GetPortId(s32 handle) { + int port_id = handle & 0xFF; + + if (port_id >= ORBIS_AUDIO_IN_NUM_PORTS) { + LOG_ERROR(Lib_AudioIn, "Invalid port"); + return ORBIS_AUDIO_IN_ERROR_PORT_FULL; + } + + if ((handle & 0x7f000000) != 0x30000000) { + LOG_ERROR(Lib_AudioIn, "Invalid handle format"); + return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE; + } + + return port_id; +} + +static s32 GetPortType(s32 handle) { + return (handle >> 16) & 0xFF; +} + +static int AllocatePort(OrbisAudioInType type) { + // TODO implement port type ranges if needed + for (int i = 0; i <= ORBIS_AUDIO_IN_NUM_PORTS; i++) { + std::shared_lock read_lock{port_table_mutex}; + if (!port_table[i]) { + return i; + } + } + return -1; +} +/* + * sceAudioIn implementation + **/ +static bool initOnce = false; +int PS4_SYSV_ABI sceAudioInOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type, + u32 index, u32 len, u32 freq, u32 param) { + LOG_INFO(Lib_AudioIn, "called, userId={}, type={}, index={}, len={}, freq={}, param={}", userId, + type, index, len, freq, param); + if (!initOnce) { + // sceAudioInInit doesn't seem to be called by most apps before sceAudioInOpen so we init + // here + audio = std::make_unique(); + initOnce = true; + } + + if (len == 0 || len > 2048) { + LOG_ERROR(Lib_AudioIn, "Invalid size"); + return ORBIS_AUDIO_IN_ERROR_INVALID_SIZE; + } + + // Validate parameters + OrbisAudioInType in_type = static_cast(type); + OrbisAudioInParamFormat format = static_cast(param); + + if (format != OrbisAudioInParamFormat::S16Mono && + format != OrbisAudioInParamFormat::S16Stereo) { + LOG_ERROR(Lib_AudioIn, "Invalid format"); + return ORBIS_AUDIO_IN_ERROR_INVALID_PARAM; + } + + if (freq != 16000 && freq != 48000) { + LOG_ERROR(Lib_AudioIn, "Invalid sample rate"); + return ORBIS_AUDIO_IN_ERROR_INVALID_FREQ; + } + + std::unique_lock lock{port_allocation_mutex}; + + // Allocate port + int port_id = AllocatePort(in_type); + if (port_id < 0) { + LOG_ERROR(Lib_AudioIn, "No free audio input ports available"); + return ORBIS_AUDIO_IN_ERROR_PORT_FULL; + } + + // Create port object + std::shared_ptr port; + try { + port = std::make_shared(); + + port->type = in_type; + port->format = format; + port->samples_num = len; + port->freq = freq; + + // Determine channel count and sample size based on format + switch (format) { + case OrbisAudioInParamFormat::S16Mono: + port->channels_num = 1; + port->sample_size = 2; + break; + case OrbisAudioInParamFormat::S16Stereo: + port->channels_num = 2; + port->sample_size = 2; + break; + default: + LOG_ERROR(Lib_AudioIn, "Unsupported audio format: {}", static_cast(format)); + return ORBIS_AUDIO_IN_ERROR_INVALID_PARAM; + } + + // Open backend + port->impl = audio->Open(*port); + if (!port->impl) { + throw std::runtime_error("Failed to create audio backend"); + } + + } catch (const std::bad_alloc&) { + LOG_ERROR(Lib_AudioIn, "Failed to allocate memory for audio port"); + return ORBIS_AUDIO_IN_ERROR_OUT_OF_MEMORY; + } catch (const std::exception& e) { + LOG_ERROR(Lib_AudioIn, "Failed to open audio input port: {}", e.what()); + return ORBIS_AUDIO_IN_ERROR_NOT_OPENED; + } + + // Store the port pointer with write lock + { + std::unique_lock write_lock{port_table_mutex}; + port_table[port_id] = port; + } + + // Create handle + s32 handle = (type << 16) | port_id | 0x30000000; + + LOG_INFO(Lib_AudioIn, "Opened audio input port {}: type={}, samples={}, freq={}, format={}", + handle, static_cast(in_type), len, freq, static_cast(format)); + return handle; +} + +int PS4_SYSV_ABI sceAudioInHqOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type, + u32 index, u32 len, u32 freq, u32 param) { + LOG_INFO(Lib_AudioIn, "called, userId={}, type={}, index={}, len={}, freq={}, param={}", userId, + type, index, len, freq, param); + int result = sceAudioInOpen(userId, type, index, len, freq, param); + if (result < 0) { + LOG_ERROR(Lib_AudioIn, "Error returned {:#x}", result); + } + return result; } int PS4_SYSV_ABI sceAudioInClose(s32 handle) { - audio->AudioInClose(handle); + LOG_INFO(Lib_AudioIn, "called, handle={:#x}", handle); + + int port_id = GetPortId(handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioIn, "Invalid port id"); + return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE; + } + + std::unique_lock lock{port_allocation_mutex}; + std::shared_ptr port; + + // Get and clear the port pointer with write lock + { + std::unique_lock write_lock{port_table_mutex}; + port = std::move(port_table[port_id]); + if (!port) { + LOG_ERROR(Lib_AudioIn, "Port wasn't open {}", port_id); + return ORBIS_AUDIO_IN_ERROR_NOT_OPENED; + } + port_table[port_id].reset(); + } + + // Free resources + std::scoped_lock port_lock{port->mutex}; + port->impl.reset(); + + LOG_INFO(Lib_AudioIn, "Closed audio input port {}", handle); + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceAudioInInput(s32 handle, void* dest) { + LOG_TRACE(Lib_AudioIn, "called, handle={:#x}, dest={}", handle, fmt::ptr(dest)); + + int port_id = GetPortId(handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioIn, "Invalid port id"); + return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE; + } + + if (!dest) { + LOG_ERROR(Lib_AudioIn, "Invalid output buffer pointer"); + return ORBIS_AUDIO_IN_ERROR_INVALID_POINTER; + } + + // Get port with read lock + std::shared_ptr port; + { + std::shared_lock read_lock{port_table_mutex}; + if (port_id < 0 || port_id >= static_cast(port_table.size())) { + LOG_ERROR(Lib_AudioIn, "Invalid port id: {}", port_id); + return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE; + } + port = port_table[port_id]; + } + + if (!port || !port->impl) { + LOG_ERROR(Lib_AudioIn, "Audio input port {} is not open", handle); + return ORBIS_AUDIO_IN_ERROR_NOT_OPENED; + } + + std::scoped_lock lock{port->mutex}; + return port->impl->Read(dest); +} + +int PS4_SYSV_ABI sceAudioInGetSilentState(s32 handle) { + LOG_TRACE(Lib_AudioIn, "called, handle={:#x}", handle); + int port_id = GetPortId(handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioIn, "Invalid port id"); + return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE; + } + // Get port with read lock + std::shared_ptr port; + { + std::shared_lock read_lock{port_table_mutex}; + if (port_id < 0 || port_id >= static_cast(port_table.size())) { + LOG_ERROR(Lib_AudioIn, "Invalid port id: {}", port_id); + return ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE; + } + port = port_table[port_id]; + } + + if (!port || !port->impl) { + LOG_ERROR(Lib_AudioIn, "Audio input port {} is not open", handle); + return ORBIS_AUDIO_IN_ERROR_NOT_OPENED; + } + + u32 silent_state = 0; + std::scoped_lock lock{port->mutex}; + if (!port->impl->IsAvailable()) { // if no mic exist or is not available + silent_state |= ORBIS_AUDIO_IN_SILENT_STATE_DEVICE_NONE; + } + return silent_state; +} + +/* + * Stubbed functions + **/ +int PS4_SYSV_ABI sceAudioInChangeAppModuleState() { + LOG_ERROR(Lib_AudioIn, "(STUBBED) called"); return ORBIS_OK; } @@ -91,20 +332,6 @@ int PS4_SYSV_ABI sceAudioInGetRerouteCount() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioInGetSilentState() { - LOG_ERROR(Lib_AudioIn, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioInHqOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type, - u32 index, u32 len, u32 freq, u32 param) { - int result = audio->AudioInOpen(type, len, freq, param); - if (result < 0) { - LOG_ERROR(Lib_AudioIn, "Error returned {:#x}", result); - } - return result; -} - int PS4_SYSV_ABI sceAudioInHqOpenEx() { LOG_ERROR(Lib_AudioIn, "(STUBBED) called"); return ORBIS_OK; @@ -115,10 +342,6 @@ int PS4_SYSV_ABI sceAudioInInit() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioInInput(s32 handle, void* dest) { - return audio->AudioInInput(handle, dest); -} - int PS4_SYSV_ABI sceAudioInInputs() { LOG_ERROR(Lib_AudioIn, "(STUBBED) called"); return ORBIS_OK; @@ -129,15 +352,6 @@ int PS4_SYSV_ABI sceAudioInIsSharedDevice() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioInOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type, - u32 index, u32 len, u32 freq, u32 param) { - int result = audio->AudioInOpen(type, len, freq, param); - if (result < 0) { - LOG_ERROR(Lib_AudioIn, "Error returned {:#x}", result); - } - return result; -} - int PS4_SYSV_ABI sceAudioInOpenEx() { LOG_ERROR(Lib_AudioIn, "(STUBBED) called"); return ORBIS_OK; diff --git a/src/core/libraries/audio/audioin.h b/src/core/libraries/audio/audioin.h index f528c730e..0eda2013e 100644 --- a/src/core/libraries/audio/audioin.h +++ b/src/core/libraries/audio/audioin.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "common/types.h" @@ -12,10 +13,31 @@ class SymbolsResolver; namespace Libraries::AudioIn { +class PortInBackend; + +constexpr s32 ORBIS_AUDIO_IN_NUM_PORTS = 7; + enum class OrbisAudioInParamFormat : u32 { S16Mono = 0, S16Stereo = 2 }; enum class OrbisAudioInType : u32 { VoiceChat = 0, General = 1, VoiceRecognition = 5 }; +constexpr int ORBIS_AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x00000001; +constexpr int ORBIS_AUDIO_IN_SILENT_STATE_PRIORITY_LOW = 0x00000002; +constexpr int ORBIS_AUDIO_IN_SILENT_STATE_USER_SETTING = 0x0000000; +constexpr int ORBIS_AUDIO_IN_SILENT_STATE_UNABLE_FORMAT = 0x00000008; + +struct PortIn { + std::mutex mutex; + std::unique_ptr impl{}; + OrbisAudioInType type; + OrbisAudioInParamFormat format; + + u32 samples_num = 0; + u32 freq = 0; + u32 channels_num = 0; + u32 sample_size = 0; +}; + int PS4_SYSV_ABI sceAudioInChangeAppModuleState(); int PS4_SYSV_ABI sceAudioInClose(s32 handle); int PS4_SYSV_ABI sceAudioInCountPorts(); @@ -32,7 +54,7 @@ int PS4_SYSV_ABI sceAudioInExtSetAecMode(); int PS4_SYSV_ABI sceAudioInGetGain(); int PS4_SYSV_ABI sceAudioInGetHandleStatusInfo(); int PS4_SYSV_ABI sceAudioInGetRerouteCount(); -int PS4_SYSV_ABI sceAudioInGetSilentState(); +int PS4_SYSV_ABI sceAudioInGetSilentState(s32 handle); int PS4_SYSV_ABI sceAudioInHqOpen(Libraries::UserService::OrbisUserServiceUserId userId, u32 type, u32 index, u32 len, u32 freq, u32 param); int PS4_SYSV_ABI sceAudioInHqOpenEx(); diff --git a/src/core/libraries/audio/audioin_backend.h b/src/core/libraries/audio/audioin_backend.h new file mode 100644 index 000000000..dc06bb27c --- /dev/null +++ b/src/core/libraries/audio/audioin_backend.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include + +namespace Libraries::AudioIn { + +struct PortIn; + +class PortInBackend { +public: + virtual ~PortInBackend() = default; + virtual int Read(void* out_buffer) = 0; + virtual void Clear() = 0; + virtual bool IsAvailable() = 0; +}; + +class AudioInBackend { +public: + AudioInBackend() = default; + virtual ~AudioInBackend() = default; + virtual std::unique_ptr Open(PortIn& port) = 0; +}; + +class SDLAudioIn final : public AudioInBackend { +public: + std::unique_ptr Open(PortIn& port) override; +}; + +} // namespace Libraries::AudioIn \ No newline at end of file diff --git a/src/core/libraries/audio/audioin_error.h b/src/core/libraries/audio/audioin_error.h new file mode 100644 index 000000000..c392e8194 --- /dev/null +++ b/src/core/libraries/audio/audioin_error.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/libraries/error_codes.h" + +// AudioIn library +constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_HANDLE = 0x80260101; +constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_SIZE = 0x80260102; +constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_FREQ = 0x80260103; +constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_TYPE = 0x80260104; +constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_POINTER = 0x80260105; +constexpr int ORBIS_AUDIO_IN_ERROR_INVALID_PARAM = 0x80260106; +constexpr int ORBIS_AUDIO_IN_ERROR_PORT_FULL = 0x80260107; +constexpr int ORBIS_AUDIO_IN_ERROR_OUT_OF_MEMORY = 0x80260108; +constexpr int ORBIS_AUDIO_IN_ERROR_NOT_OPENED = 0x80260109; +constexpr int ORBIS_AUDIO_IN_ERROR_BUSY = 0x8026010A; +constexpr int ORBIS_AUDIO_IN_ERROR_SYSTEM_MEMORY = 0x8026010B; +constexpr int ORBIS_AUDIO_IN_ERROR_SYSTEM_IPC = 0x8026010C; diff --git a/src/core/libraries/audio/audioout.cpp b/src/core/libraries/audio/audioout.cpp index bb1ca65fe..6c58df94f 100644 --- a/src/core/libraries/audio/audioout.cpp +++ b/src/core/libraries/audio/audioout.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -19,315 +20,238 @@ namespace Libraries::AudioOut { -std::mutex port_open_mutex{}; -std::array ports_out{}; +// Port table with shared_ptr - use std::shared_mutex for RW locking +std::array, ORBIS_AUDIO_OUT_NUM_PORTS> port_table{}; +std::shared_mutex port_table_mutex; +std::mutex port_allocation_mutex; static std::unique_ptr audio; +static std::atomic lazy_init{0}; + +// Port allocation ranges +constexpr struct PortRange { + s32 start; + s32 end; + s32 count() const { + return end - start + 1; + } +} port_ranges[] = { + {0, 7}, // MAIN + {8, 8}, // BGM + {9, 12}, // VOICE + {13, 16}, // PERSONAL + {17, 20}, // PADSPK + {21, 21}, // Type 5-8 + {22, 22}, // Audio3d (126) + {23, 23}, // AUX (127) + {24, 24}, // Type 125 +}; static AudioFormatInfo GetFormatInfo(const OrbisAudioOutParamFormat format) { static constexpr std::array format_infos = {{ // S16Mono - {false, 2, 1, {0}}, + {false, 2, 1, {0}, false}, // S16Stereo - {false, 2, 2, {0, 1}}, + {false, 2, 2, {0, 1}, false}, // S16_8CH - {false, 2, 8, {0, 1, 2, 3, 4, 5, 6, 7}}, + {false, 2, 8, {0, 1, 2, 3, 4, 5, 6, 7}, false}, // FloatMono - {true, 4, 1, {0}}, + {true, 4, 1, {0}, false}, // FloatStereo - {true, 4, 2, {0, 1}}, + {true, 4, 2, {0, 1}, false}, // Float_8CH - {true, 4, 8, {0, 1, 2, 3, 4, 5, 6, 7}}, + {true, 4, 8, {0, 1, 2, 3, 4, 5, 6, 7}, false}, // S16_8CH_Std - {false, 2, 8, {0, 1, 2, 3, 6, 7, 4, 5}}, + {false, 2, 8, {0, 1, 2, 3, 6, 7, 4, 5}, true}, // Float_8CH_Std - {true, 4, 8, {0, 1, 2, 3, 6, 7, 4, 5}}, + {true, 4, 8, {0, 1, 2, 3, 6, 7, 4, 5}, true}, }}; const auto index = static_cast(format); ASSERT_MSG(index < format_infos.size(), "Unknown audio format {}", index); return format_infos[index]; } -int PS4_SYSV_ABI sceAudioOutDeviceIdOpen() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} +/* + * Helper functions + **/ +static int GetPortRange(OrbisAudioOutPort type) { + s32 _type = static_cast(type); -int PS4_SYSV_ABI sceAudioDeviceControlGet() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioDeviceControlSet() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutA3dControl() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutA3dExit() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutA3dInit() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutAttachToApplicationByPid() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutChangeAppModuleState() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutClose(s32 handle) { - LOG_INFO(Lib_AudioOut, "handle = {}", handle); - if (audio == nullptr) { - return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; + switch (_type) { + case 0: + return 0; // MAIN + case 1: + return 1; // BGM + case 2: + return 2; // VOICE + case 3: + return 3; // PERSONAL + case 4: + return 4; // PADSPK + case 5: + return 5; // Type 5 + case 6: + return 5; // Type 6 + case 7: + return 5; // Type 7 + case 8: + return 5; // Type 8 + case 126: + return 6; // Audio3d + case 125: + return 8; // Type 125 + case 127: + return 7; // AUX + default: + return -1; } - if (handle < 1 || handle > SCE_AUDIO_OUT_NUM_PORTS) { +} + +static int GetPortId(s32 handle) { + int port_id = handle & 0xFF; + + if (port_id >= ORBIS_AUDIO_OUT_NUM_PORTS) { + LOG_ERROR(Lib_AudioOut, "Invalid port"); return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; } - std::unique_lock open_lock{port_open_mutex}; - auto& port = ports_out.at(handle - 1); - { - std::unique_lock lock{port.mutex}; - if (!port.IsOpen()) { - return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; - } - std::free(port.output_buffer); - port.output_buffer = nullptr; - port.output_ready = false; - port.impl = nullptr; - } - // Stop outside of port lock scope to prevent deadlocks. - port.output_thread.Stop(); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutDetachFromApplicationByPid() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutExConfigureOutputMode() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutExGetSystemInfo() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutExPtClose() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutExPtGetLastOutputTime() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutExPtOpen() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetFocusEnablePid() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetHandleStatusInfo() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetInfo() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetInfoOpenNum() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetLastOutputTime(s32 handle, u64* output_time) { - LOG_DEBUG(Lib_AudioOut, "called, handle: {}, output time: {}", handle, fmt::ptr(output_time)); - if (!output_time) { - return ORBIS_AUDIO_OUT_ERROR_INVALID_POINTER; - } - if (handle >= ports_out.size()) { - return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; - } - auto& port = ports_out.at(handle - 1); - if (!port.IsOpen()) { - return ORBIS_AUDIO_OUT_ERROR_NOT_OPENED; - } - *output_time = port.last_output_time; - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* state) { - if (audio == nullptr) { - return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; - } - if (handle < 1 || handle > SCE_AUDIO_OUT_NUM_PORTS) { + if ((handle & 0x3F000000) != 0x20000000) { + LOG_ERROR(Lib_AudioOut, "Invalid port"); return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; } - auto& port = ports_out.at(handle - 1); - { - std::unique_lock lock{port.mutex}; - if (!port.IsOpen()) { - return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; - } - switch (port.type) { - case OrbisAudioOutPort::Main: - case OrbisAudioOutPort::Bgm: - case OrbisAudioOutPort::Voice: - case OrbisAudioOutPort::Audio3d: - state->output = 1; - state->channel = port.format_info.num_channels > 2 ? 2 : port.format_info.num_channels; - break; - case OrbisAudioOutPort::Personal: - case OrbisAudioOutPort::PadSpk: - state->output = 4; - state->channel = 1; - break; - case OrbisAudioOutPort::Aux: - state->output = 0; - state->channel = 0; - break; - default: - UNREACHABLE(); - } - state->rerouteCounter = 0; - state->volume = 127; + return port_id; +} + +static s32 GetPortType(s32 handle) { + return (handle >> 16) & 0xFF; +} + +static int AllocatePort(OrbisAudioOutPort type) { + int range_idx = GetPortRange(type); + if (range_idx < 0) { + return -1; } - return ORBIS_OK; -} -int PS4_SYSV_ABI sceAudioOutGetSimulatedBusUsableStatusByBusType() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo2() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetSparkVss() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutGetSystemState() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutInit() { - LOG_TRACE(Lib_AudioOut, "called"); - if (audio != nullptr) { - return ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT; + const auto& range = port_ranges[range_idx]; + for (int i = range.start; i <= range.end; i++) { + std::shared_lock read_lock{port_table_mutex}; + if (!port_table[i]) { + return i; + } } - audio = std::make_unique(); - return ORBIS_OK; + return -1; } -int PS4_SYSV_ABI sceAudioOutInitIpmiGetSession() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; +void AdjustVol() { + if (lazy_init.load(std::memory_order_relaxed) == 0 && audio == nullptr) { + return; + } + + std::shared_lock read_lock{port_table_mutex}; + for (int i = 0; i < ORBIS_AUDIO_OUT_NUM_PORTS; i++) { + if (auto port = port_table[i]) { + std::unique_lock lock{port->mutex, std::try_to_lock}; + if (lock.owns_lock()) { + port->impl->SetVolume(port->volume); + } + } + } } -int PS4_SYSV_ABI sceAudioOutMasteringGetState() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutMasteringInit() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutMasteringSetParam() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutMasteringTerm() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutMbusInit() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -static void AudioOutputThread(PortOut* port, const std::stop_token& stop) { +static void AudioOutputThread(std::shared_ptr port, const std::stop_token& stop) { { - const auto thread_name = fmt::format("shadPS4:AudioOutputThread:{}", fmt::ptr(port)); + const auto thread_name = fmt::format("shadPS4:AudioOutputThread:{}", fmt::ptr(port.get())); Common::SetCurrentThreadName(thread_name.c_str()); } Common::AccurateTimer timer( std::chrono::nanoseconds(1000000000ULL * port->buffer_frames / port->sample_rate)); + while (true) { timer.Start(); + { std::unique_lock lock{port->mutex}; + if (!port->impl || stop.stop_requested()) { + break; + } + if (port->output_ready) { port->impl->Output(port->output_buffer); port->output_ready = false; + port->last_output_time = + Kernel::sceKernelGetProcessTime(); // moved from sceAudioOutOutput TOOD recheck } } + port->output_cv.notify_one(); + if (stop.stop_requested()) { break; } + timer.End(); } } +/* + * sceAudioOut implementation + **/ +s32 PS4_SYSV_ABI sceAudioOutInit() { + LOG_TRACE(Lib_AudioOut, "called"); + + int expected = 0; + if (!lazy_init.compare_exchange_strong(expected, 1, std::memory_order_acq_rel)) { + return ORBIS_AUDIO_OUT_ERROR_ALREADY_INIT; + } + + audio = std::make_unique(); + + LOG_INFO(Lib_AudioOut, "Audio system initialized"); + return ORBIS_OK; +} + s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id, OrbisAudioOutPort port_type, s32 index, u32 length, u32 sample_rate, OrbisAudioOutParamExtendedInformation param_type) { LOG_INFO(Lib_AudioOut, - "id = {} port_type = {} index = {} length = {} sample_rate = {} " - "param_type = {} attr = {}", - user_id, magic_enum::enum_name(port_type), index, length, sample_rate, - magic_enum::enum_name(param_type.data_format.Value()), - magic_enum::enum_name(param_type.attributes.Value())); - if (audio == nullptr) { + "called, user_id={}, port_type={}({}), index={}, length={}, " + "sample_rate={}, data_format={}({}), attributes={}({})", + user_id, magic_enum::enum_name(port_type), static_cast(port_type), index, length, + sample_rate, magic_enum::enum_name(param_type.data_format.Value()), + static_cast(param_type.data_format.Value()), + magic_enum::enum_name(param_type.attributes.Value()), + static_cast(param_type.attributes.Value())); + + if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { LOG_ERROR(Lib_AudioOut, "Audio out not initialized"); return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; } + + if (length == 0 || length > 2048 || (length & 0xFF) != 0) { + LOG_ERROR(Lib_AudioOut, "Invalid size"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_SIZE; + } + + s32 _type = static_cast(port_type); + u32 param_raw = param_type.Unpack(); + + // Extract attributes + bool is_restricted = (param_raw & ORBIS_AUDIO_OUT_PARAM_ATTR_RESTRICTED) != 0; + bool is_mix_to_main = (param_raw & ORBIS_AUDIO_OUT_PARAM_ATTR_MIX_TO_MAIN) != 0; + + if (_type != 3 && is_mix_to_main) { + LOG_ERROR(Lib_AudioOut, "Invalid format"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_FORMAT; + } + + if (_type != 0 && (param_raw & 0x70000000) != 0) { + LOG_ERROR(Lib_AudioOut, "Invalid format"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_FORMAT; + } + if ((port_type < OrbisAudioOutPort::Main || port_type > OrbisAudioOutPort::PadSpk) && (port_type != OrbisAudioOutPort::Audio3d && port_type != OrbisAudioOutPort::Aux)) { LOG_ERROR(Lib_AudioOut, "Invalid port type"); @@ -337,14 +261,11 @@ s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id, LOG_ERROR(Lib_AudioOut, "Invalid sample rate"); return ORBIS_AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ; } - if (length != 256 && length != 512 && length != 768 && length != 1024 && length != 1280 && - length != 1536 && length != 1792 && length != 2048) { - LOG_ERROR(Lib_AudioOut, "Invalid length"); - return ORBIS_AUDIO_OUT_ERROR_INVALID_SIZE; - } + if (index != 0) { LOG_ERROR(Lib_AudioOut, "index is not valid !=0 {}", index); } + const auto format = param_type.data_format.Value(); if (format < OrbisAudioOutParamFormat::S16Mono || format > OrbisAudioOutParamFormat::Float_8CH_Std) { @@ -358,270 +279,808 @@ s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id, return ORBIS_AUDIO_OUT_ERROR_INVALID_FORMAT; } - std::unique_lock open_lock{port_open_mutex}; - const auto port = - std::ranges::find_if(ports_out, [&](const PortOut& p) { return !p.IsOpen(); }); - if (port == ports_out.end()) { - LOG_ERROR(Lib_AudioOut, "Audio ports are full"); + std::unique_lock lock{port_allocation_mutex}; + + // Allocate port + int port_id = AllocatePort(port_type); + if (port_id < 0) { + LOG_ERROR(Lib_AudioOut, "Error allocated port"); return ORBIS_AUDIO_OUT_ERROR_PORT_FULL; } - { - std::unique_lock port_lock(port->mutex); + // Create port object + std::shared_ptr port; + try { + port = std::make_shared(); - port->type = port_type; - port->format_info = GetFormatInfo(format); + port->userId = user_id; + port->type = static_cast(_type); + port->format_info = GetFormatInfo(param_type.data_format.Value()); port->sample_rate = sample_rate; port->buffer_frames = length; - port->volume.fill(SCE_AUDIO_OUT_VOLUME_0DB); + port->volume.fill(ORBIS_AUDIO_OUT_VOLUME_0DB); + port->mixLevelPadSpk = ORBIS_AUDIO_OUT_MIXLEVEL_PADSPK_DEFAULT; + // Set attributes + port->is_restricted = is_restricted; + port->is_mix_to_main = is_mix_to_main; + + // Log attributes if present + if (is_restricted) { + LOG_INFO(Lib_AudioOut, "Audio port opened with RESTRICTED attribute"); + } + if (is_mix_to_main) { + LOG_INFO(Lib_AudioOut, "Audio port opened with MIX_TO_MAIN attribute"); + } + + // Create backend port->impl = audio->Open(*port); + if (!port->impl) { + throw std::runtime_error("Failed to create audio backend"); + } + // Allocate buffer port->output_buffer = std::malloc(port->BufferSize()); - port->output_ready = false; + if (!port->output_buffer) { + throw std::bad_alloc(); + } + + // Start output thread - pass shared_ptr by value to keep port alive port->output_thread.Run( - [port](const std::stop_token& stop) { AudioOutputThread(&*port, stop); }); + [port](const std::stop_token& stop) { AudioOutputThread(port, stop); }); + + // Set initial volume + port->impl->SetVolume(port->volume); + + } catch (const std::bad_alloc&) { + return ORBIS_AUDIO_OUT_ERROR_OUT_OF_MEMORY; + } catch (const std::exception& e) { + LOG_ERROR(Lib_AudioOut, "Failed to open audio port: {}", e.what()); + return ORBIS_AUDIO_OUT_ERROR_TRANS_EVENT; } - return std::distance(ports_out.begin(), port) + 1; + + { + std::unique_lock write_lock{port_table_mutex}; + port_table[port_id] = port; + } + + // Create handle + s32 handle = (_type << 16) | port_id | 0x20000000; + return handle; } -int PS4_SYSV_ABI sceAudioOutOpenEx() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceAudioOutClose(s32 handle) { + LOG_INFO(Lib_AudioOut, "handle = {:#x}", handle); + + if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { + LOG_ERROR(Lib_AudioOut, "Audio out not initialized"); + return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; + } + + int port_id = GetPortId(handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioOut, "Invalid port id"); + return port_id; + } + + s32 port_type = GetPortType(handle); + if (port_type >= 5 && port_type <= 13) { + LOG_ERROR(Lib_AudioOut, "Invalid port type"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT_TYPE; + } + + // Check valid types + if (!((port_type >= 0 && port_type <= 4) || port_type == 14 || port_type == 126 || + port_type == 127)) { + LOG_ERROR(Lib_AudioOut, "Invalid port type"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT_TYPE; + } + + std::unique_lock lock{port_allocation_mutex}; + + std::shared_ptr port; + { + std::unique_lock write_lock{port_table_mutex}; + port = std::move(port_table[port_id]); + port_table[port_id].reset(); + } + + if (!port) { + LOG_ERROR(Lib_AudioOut, "Port wasn't open {}", port_id); + return ORBIS_AUDIO_OUT_ERROR_NOT_OPENED; + } + + // Stop the output thread + port->output_thread.Stop(); + + std::free(port->output_buffer); + + LOG_DEBUG(Lib_AudioOut, "Closed audio port {}", port_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetLastOutputTime(s32 handle, u64* output_time) { + if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { + LOG_ERROR(Lib_AudioOut, "audio is not init"); + return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; + } + + int port_id = GetPortId(handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioOut, "Invalid port id"); + return port_id; + } + + if (!output_time) { + LOG_ERROR(Lib_AudioOut, "Invalid pointer"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_POINTER; + } + + std::shared_ptr port; + { + std::shared_lock read_lock{port_table_mutex}; + port = port_table[port_id]; + } + + if (!port) { + LOG_ERROR(Lib_AudioOut, "Port not opened {}", port_id); + return ORBIS_AUDIO_OUT_ERROR_NOT_OPENED; + } + + std::unique_lock lock{port->mutex}; + *output_time = port->last_output_time; + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* state) { + if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { + LOG_ERROR(Lib_AudioOut, "audio is not init"); + return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; + } + + int port_id = GetPortId(handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioOut, "Invalid port id"); + return port_id; + } + + if (!state) { + LOG_ERROR(Lib_AudioOut, "Invalid pointer"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_POINTER; + } + + std::shared_ptr port; + { + std::shared_lock read_lock{port_table_mutex}; + port = port_table[port_id]; + } + + if (!port) { + LOG_ERROR(Lib_AudioOut, "Port is not open {}", port_id); + return ORBIS_AUDIO_OUT_ERROR_NOT_OPENED; + } + + std::unique_lock lock{port->mutex}; + + switch (port->type) { + case OrbisAudioOutPort::Main: + case OrbisAudioOutPort::Bgm: + case OrbisAudioOutPort::Audio3d: + state->output = ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_PRIMARY; + state->channel = port->format_info.num_channels > 2 ? 2 : port->format_info.num_channels; + break; + case OrbisAudioOutPort::Voice: + case OrbisAudioOutPort::Personal: + state->output = ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_HEADPHONE; + state->channel = 1; + break; + case OrbisAudioOutPort::PadSpk: + state->output = ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_TERTIARY; + state->channel = 1; + state->volume = 127; // max + break; + case OrbisAudioOutPort::Aux: + state->output = ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_EXTERNAL; + state->channel = 0; + break; + default: + state->output = ORBIS_AUDIO_OUT_STATE_OUTPUT_UNKNOWN; + state->channel = 0; + break; + } + + if (port->type != OrbisAudioOutPort::PadSpk) { + state->volume = -1; // invalid + } + + state->rerouteCounter = 0; + state->flag = 0; + LOG_INFO(Lib_AudioOut, + "called, handle={:#x}, state={}, output={}, channel={}, volume={}, " + "rerouteCounter={}, flag={}", + handle, fmt::ptr(state), state->output, state->channel, state->volume, + state->rerouteCounter, state->flag); return ORBIS_OK; } s32 PS4_SYSV_ABI sceAudioOutOutput(s32 handle, void* ptr) { - if (audio == nullptr) { + LOG_TRACE(Lib_AudioOut, "(STUBBED) called, handle={:#x}, ptr={}", handle, fmt::ptr(ptr)); + + if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { + LOG_ERROR(Lib_AudioOut, "audio is not init"); return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; } - if (handle < 1 || handle > SCE_AUDIO_OUT_NUM_PORTS) { - return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; + + int port_id = GetPortId(handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioOut, "invalid port id"); + return port_id; } - auto samples_sent = 0; - auto& port = ports_out.at(handle - 1); + s32 port_type = GetPortType(handle); + if (port_type >= 5 && port_type <= 13) { + LOG_ERROR(Lib_AudioOut, "Invalid port type"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT_TYPE; + } + + // Check valid types + if (!((port_type >= 0 && port_type <= 4) || port_type == 14 || port_type == 126 || + port_type == 127)) { + LOG_ERROR(Lib_AudioOut, "Invalid port type"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT_TYPE; + } + + std::shared_ptr port; { - std::unique_lock lock{port.mutex}; - if (!port.IsOpen()) { - return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; - } - port.output_cv.wait(lock, [&] { return !port.output_ready; }); - if (ptr != nullptr && port.IsOpen()) { - std::memcpy(port.output_buffer, ptr, port.BufferSize()); - port.output_ready = true; - port.last_output_time = Kernel::sceKernelGetProcessTime(); - samples_sent = port.buffer_frames * port.format_info.num_channels; + std::shared_lock read_lock{port_table_mutex}; + port = port_table[port_id]; + } + + if (!port) { + LOG_ERROR(Lib_AudioOut, "Port is not opened {}", port_id); + return ORBIS_AUDIO_OUT_ERROR_NOT_OPENED; + } + + s32 samples_sent = 0; + { + std::unique_lock lock{port->mutex}; + port->output_cv.wait(lock, [&] { return !port->output_ready; }); + + if (ptr != nullptr) { + std::memcpy(port->output_buffer, ptr, port->BufferSize()); + port->output_ready = true; + samples_sent = port->buffer_frames * port->format_info.num_channels; } } + return samples_sent; } -int PS4_SYSV_ABI sceAudioOutOutputs(OrbisAudioOutOutputParam* param, u32 num) { - int ret = 0; - for (u32 i = 0; i < num; i++) { - const auto [handle, ptr] = param[i]; - if (ret = sceAudioOutOutput(handle, ptr); ret < 0) { - return ret; +s32 PS4_SYSV_ABI sceAudioOutOutputs(OrbisAudioOutOutputParam* param, u32 num) { + if (param) { + LOG_TRACE(Lib_AudioOut, "(STUBBED) called, param={}, num={}", fmt::ptr(param), num); + for (u32 i = 0; i < num; i++) { + LOG_TRACE(Lib_AudioOut, " [{}] handle={:#x}, ptr={}", i, param[i].handle, + fmt::ptr(param[i].ptr)); + } + } else { + LOG_TRACE(Lib_AudioOut, "(STUBBED) called, param=nullptr, num={}", num); + } + if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { + LOG_ERROR(Lib_AudioOut, "audio is not init"); + return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; + } + + if (num == 0 || num > 25) { + LOG_ERROR(Lib_AudioOut, "ports is full"); + return ORBIS_AUDIO_OUT_ERROR_PORT_FULL; + } + + if (!param) { + LOG_ERROR(Lib_AudioOut, "invalid pointer"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_POINTER; + } + + std::vector> ports; + std::vector> locks; + ports.reserve(num); + locks.reserve(num); + + u32 buffer_frames = 0; + + { + std::shared_lock read_lock{port_table_mutex}; + + for (u32 i = 0; i < num; i++) { + int port_id = GetPortId(param[i].handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioOut, "invalid port id"); + return port_id; + } + + s32 port_type = GetPortType(param[i].handle); + if (port_type >= 5 && port_type <= 13) { + LOG_ERROR(Lib_AudioOut, "Invalid port type"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT_TYPE; + } + + // Check valid types + if (!((port_type >= 0 && port_type <= 4) || port_type == 14 || port_type == 126 || + port_type == 127)) { + LOG_ERROR(Lib_AudioOut, "Invalid port type"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT_TYPE; + } + + // Check for duplicate handles + for (u32 j = 0; j < i; j++) { + if (param[i].handle == param[j].handle) { + LOG_ERROR(Lib_AudioOut, "Duplicate audio handles: {:#x}", param[i].handle); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; + } + } + + // Get port + auto port = port_table[port_id]; + if (!port) { + LOG_ERROR(Lib_AudioOut, "Port not opened {}", port_id); + return ORBIS_AUDIO_OUT_ERROR_NOT_OPENED; + } + + ports.push_back(port); + locks.emplace_back(port->mutex); + + // Check consistent buffer size + if (i == 0) { + buffer_frames = port->buffer_frames; + } else if (port->buffer_frames != buffer_frames) { + LOG_ERROR(Lib_AudioOut, "Invalid port size"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_SIZE; + } } } - return ret; -} -int PS4_SYSV_ABI sceAudioOutPtClose() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} + // Wait for all ports to be ready + for (u32 i = 0; i < num; i++) { + ports[i]->output_cv.wait(locks[i], [&] { return !ports[i]->output_ready; }); + } -int PS4_SYSV_ABI sceAudioOutPtGetLastOutputTime() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} + // Copy data to all ports + for (u32 i = 0; i < num; i++) { + if (param[i].ptr != nullptr) { + std::memcpy(ports[i]->output_buffer, param[i].ptr, ports[i]->BufferSize()); + ports[i]->output_ready = true; + } + } -int PS4_SYSV_ABI sceAudioOutPtOpen() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetConnections() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetConnectionsForUser() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetDevConnection() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetHeadphoneOutMode() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetJediJackVolume() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetJediSpkVolume() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetMainOutput() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetMixLevelPadSpk() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetMorpheusParam() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetMorpheusWorkingMode() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetPortConnections() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetPortStatuses() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetRecMode() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetSparkParam() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceAudioOutSetUsbVolume() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; + return buffer_frames; } s32 PS4_SYSV_ABI sceAudioOutSetVolume(s32 handle, s32 flag, s32* vol) { - if (audio == nullptr) { + if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { + LOG_ERROR(Lib_AudioOut, "audio is not init"); return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; } - if (handle < 1 || handle > SCE_AUDIO_OUT_NUM_PORTS) { - return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; + + int port_id = GetPortId(handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioOut, "invalid port_id"); + return port_id; } - auto& port = ports_out.at(handle - 1); + s32 port_type = GetPortType(handle); + if (port_type >= 5 && port_type <= 13) { + LOG_ERROR(Lib_AudioOut, "Invalid port type"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT_TYPE; + } + + // Check valid types + if (!((port_type >= 0 && port_type <= 4) || port_type == 14 || port_type == 126 || + port_type == 127)) { + LOG_ERROR(Lib_AudioOut, "Invalid port type"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT_TYPE; + } + + if (!vol) { + LOG_ERROR(Lib_AudioOut, "Invalid pointer"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_POINTER; + } + + if (*vol > ORBIS_AUDIO_OUT_VOLUME_0DB) { + LOG_ERROR(Lib_AudioOut, "Invalid volume"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_VOLUME; + } + + // Get port with shared lock (read-only access to table) + std::shared_ptr port; { - std::unique_lock lock{port.mutex}; - if (!port.IsOpen()) { - return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT; - } - for (int i = 0; i < port.format_info.num_channels; i++, flag >>= 1u) { - if (flag & 0x1u) { - port.volume[i] = vol[i]; - } - } - port.impl->SetVolume(port.volume); - } - AdjustVol(); - return ORBIS_OK; -} - -void AdjustVol() { - if (audio == nullptr) { - return; + std::shared_lock read_lock{port_table_mutex}; + port = port_table[port_id]; } - for (int i = 0; i < ports_out.size(); i++) { - std::unique_lock lock{ports_out[i].mutex}; - if (!ports_out[i].IsOpen()) { - continue; - } - ports_out[i].impl->SetVolume(ports_out[i].volume); + if (!port) { + LOG_ERROR(Lib_AudioOut, "Port not opened {}", port_id); + return ORBIS_AUDIO_OUT_ERROR_NOT_OPENED; } + + std::unique_lock lock{port->mutex}; + + // Set volumes based on flags + if (flag & ORBIS_AUDIO_VOLUME_FLAG_L_CH) + port->volume[0] = *vol; + if (flag & ORBIS_AUDIO_VOLUME_FLAG_R_CH) + port->volume[1] = *vol; + if (flag & ORBIS_AUDIO_VOLUME_FLAG_C_CH) + port->volume[2] = *vol; + if (flag & ORBIS_AUDIO_VOLUME_FLAG_LFE_CH) + port->volume[3] = *vol; + if (flag & ORBIS_AUDIO_VOLUME_FLAG_LS_CH) + port->volume[4] = *vol; + if (flag & ORBIS_AUDIO_VOLUME_FLAG_RS_CH) + port->volume[5] = *vol; + if (flag & ORBIS_AUDIO_VOLUME_FLAG_LE_CH) + port->volume[6] = *vol; + if (flag & ORBIS_AUDIO_VOLUME_FLAG_RE_CH) + port->volume[7] = *vol; + + port->impl->SetVolume(port->volume); + + return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSetVolumeDown() { +s32 PS4_SYSV_ABI sceAudioOutSetMixLevelPadSpk(s32 handle, s32 mixLevel) { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { + LOG_ERROR(Lib_AudioOut, "audio is not init"); + return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; + } + + int port_id = GetPortId(handle); + if (port_id < 0) { + LOG_ERROR(Lib_AudioOut, "Invalid port_id"); + return port_id; + } + + if (GetPortType(handle) != 4) { // PadSpk + LOG_ERROR(Lib_AudioOut, "Invalid port type"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_PORT_TYPE; + } + + if (mixLevel > ORBIS_AUDIO_OUT_VOLUME_0DB) { + LOG_ERROR(Lib_AudioOut, "Invalid mix level"); + return ORBIS_AUDIO_OUT_ERROR_INVALID_MIXLEVEL; + } + + std::shared_ptr port; + { + std::shared_lock read_lock{port_table_mutex}; + port = port_table[port_id]; + } + + if (!port) { + LOG_ERROR(Lib_AudioOut, "Port not opened"); + return ORBIS_AUDIO_OUT_ERROR_NOT_OPENED; + } + + std::unique_lock lock{port->mutex}; + port->mixLevelPadSpk = mixLevel; + // TODO: Apply mix level to backend + + return ORBIS_OK; +} + +/* + * Stubbed functions + **/ +s32 PS4_SYSV_ABI sceAudioOutDeviceIdOpen() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutStartAuxBroadcast() { +s32 PS4_SYSV_ABI sceAudioDeviceControlGet() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutStartSharePlay() { +s32 PS4_SYSV_ABI sceAudioDeviceControlSet() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutStopAuxBroadcast() { +s32 PS4_SYSV_ABI sceAudioOutA3dControl() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutStopSharePlay() { +s32 PS4_SYSV_ABI sceAudioOutA3dExit() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSuspendResume() { +s32 PS4_SYSV_ABI sceAudioOutA3dInit() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSysConfigureOutputMode() { +s32 PS4_SYSV_ABI sceAudioOutAttachToApplicationByPid() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSysGetHdmiMonitorInfo() { +s32 PS4_SYSV_ABI sceAudioOutChangeAppModuleState() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSysGetSystemInfo() { +s32 PS4_SYSV_ABI sceAudioOutDetachFromApplicationByPid() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode() { +s32 PS4_SYSV_ABI sceAudioOutExConfigureOutputMode() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSystemControlGet() { +s32 PS4_SYSV_ABI sceAudioOutExGetSystemInfo() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSystemControlSet() { +s32 PS4_SYSV_ABI sceAudioOutExPtClose() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSparkControlSetEqCoef() { +s32 PS4_SYSV_ABI sceAudioOutExPtGetLastOutputTime() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } -int PS4_SYSV_ABI sceAudioOutSetSystemDebugState() { +s32 PS4_SYSV_ABI sceAudioOutExPtOpen() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetFocusEnablePid() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetHandleStatusInfo() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetInfo() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetInfoOpenNum() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutOpenEx() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetSimulatedBusUsableStatusByBusType() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo2() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetSparkVss() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutGetSystemState() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutInitIpmiGetSession() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutMasteringGetState() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutMasteringInit(u32 flags) { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + if (flags != 0) { + return ORBIS_AUDIO_OUT_ERROR_MASTERING_INVALID_API_PARAM; + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutMasteringSetParam() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutMasteringTerm() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutMbusInit() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutPtClose() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutPtGetLastOutputTime() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutPtOpen() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetConnections() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetConnectionsForUser() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetDevConnection() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetHeadphoneOutMode() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetJediJackVolume() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetJediSpkVolume() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetMainOutput() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetMorpheusParam() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetMorpheusWorkingMode() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetPortConnections() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetPortStatuses() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetRecMode() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetSparkParam() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetUsbVolume() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetVolumeDown() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutStartAuxBroadcast() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutStartSharePlay() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutStopAuxBroadcast() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutStopSharePlay() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSuspendResume() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSysConfigureOutputMode() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSysGetHdmiMonitorInfo() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSysGetSystemInfo() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSystemControlGet() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSystemControlSet() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSparkControlSetEqCoef() { + LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceAudioOutSetSystemDebugState() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; } diff --git a/src/core/libraries/audio/audioout.h b/src/core/libraries/audio/audioout.h index c993582d3..93db8150f 100644 --- a/src/core/libraries/audio/audioout.h +++ b/src/core/libraries/audio/audioout.h @@ -17,8 +17,32 @@ class PortBackend; // Main up to 8 ports, BGM 1 port, voice up to 4 ports, // personal up to 4 ports, padspk up to 5 ports, aux 1 port -constexpr s32 SCE_AUDIO_OUT_NUM_PORTS = 22; -constexpr s32 SCE_AUDIO_OUT_VOLUME_0DB = 32768; // max volume value +constexpr s32 ORBIS_AUDIO_OUT_NUM_PORTS = 25; +constexpr s32 ORBIS_AUDIO_OUT_VOLUME_0DB = 32768; // max volume value + +constexpr s32 ORBIS_AUDIO_OUT_MIXLEVEL_PADSPK_DEFAULT = 11626; // default -9db +constexpr s32 ORBIS_AUDIO_OUT_MIXLEVEL_PADSPK_0DB = 32768; // max volume + +constexpr s32 ORBIS_AUDIO_OUT_PARAM_ATTR_RESTRICTED = 0x00010000; +constexpr s32 ORBIS_AUDIO_OUT_PARAM_ATTR_MIX_TO_MAIN = 0x00020000; + +// Volume flags +constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_L_CH = (1u << 0); +constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_R_CH = (1u << 1); +constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_C_CH = (1u << 2); +constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_LFE_CH = (1u << 3); +constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_LS_CH = (1u << 4); +constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_RS_CH = (1u << 5); +constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_LE_CH = (1u << 6); +constexpr u32 ORBIS_AUDIO_VOLUME_FLAG_RE_CH = (1u << 7); + +// Port state constants +constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_UNKNOWN = 0x00; +constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_PRIMARY = 0x01; +constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_SECONDARY = 0x02; +constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_TERTIARY = 0x04; +constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_HEADPHONE = 0x40; +constexpr u16 ORBIS_AUDIO_OUT_STATE_OUTPUT_CONNECTED_EXTERNAL = 0x80; enum class OrbisAudioOutPort { Main = 0, @@ -53,6 +77,9 @@ union OrbisAudioOutParamExtendedInformation { BitField<16, 4, OrbisAudioOutParamAttr> attributes; BitField<20, 10, u32> reserve1; BitField<31, 1, u32> unused; + u32 Unpack() const { + return *reinterpret_cast(this); + } }; struct OrbisAudioOutOutputParam { @@ -77,6 +104,7 @@ struct AudioFormatInfo { /// Layout array remapping channel indices, specified in this order: /// FL, FR, FC, LFE, BL, BR, SL, SR std::array channel_layout; + bool is_std; [[nodiscard]] u16 FrameSize() const { return sample_size * num_channels; @@ -87,100 +115,100 @@ struct PortOut { std::mutex mutex; std::unique_ptr impl{}; - void* output_buffer; + void* output_buffer = nullptr; std::condition_variable_any output_cv; - bool output_ready; + bool output_ready = false; Kernel::Thread output_thread{}; OrbisAudioOutPort type; AudioFormatInfo format_info; - u32 sample_rate; - u32 buffer_frames; - u64 last_output_time; + u32 sample_rate = 48000; + u32 buffer_frames = 1024; + u64 last_output_time = 0; std::array volume; - - [[nodiscard]] bool IsOpen() const { - return impl != nullptr; - } + s32 userId = 0; + s32 mixLevelPadSpk = ORBIS_AUDIO_OUT_MIXLEVEL_PADSPK_DEFAULT; + bool is_restricted = false; + bool is_mix_to_main = false; [[nodiscard]] u32 BufferSize() const { return buffer_frames * format_info.FrameSize(); } }; -int PS4_SYSV_ABI sceAudioOutDeviceIdOpen(); -int PS4_SYSV_ABI sceAudioDeviceControlGet(); -int PS4_SYSV_ABI sceAudioDeviceControlSet(); -int PS4_SYSV_ABI sceAudioOutA3dControl(); -int PS4_SYSV_ABI sceAudioOutA3dExit(); -int PS4_SYSV_ABI sceAudioOutA3dInit(); -int PS4_SYSV_ABI sceAudioOutAttachToApplicationByPid(); -int PS4_SYSV_ABI sceAudioOutChangeAppModuleState(); -int PS4_SYSV_ABI sceAudioOutClose(s32 handle); -int PS4_SYSV_ABI sceAudioOutDetachFromApplicationByPid(); -int PS4_SYSV_ABI sceAudioOutExConfigureOutputMode(); -int PS4_SYSV_ABI sceAudioOutExGetSystemInfo(); -int PS4_SYSV_ABI sceAudioOutExPtClose(); -int PS4_SYSV_ABI sceAudioOutExPtGetLastOutputTime(); -int PS4_SYSV_ABI sceAudioOutExPtOpen(); -int PS4_SYSV_ABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode(); -int PS4_SYSV_ABI sceAudioOutGetFocusEnablePid(); -int PS4_SYSV_ABI sceAudioOutGetHandleStatusInfo(); -int PS4_SYSV_ABI sceAudioOutGetInfo(); -int PS4_SYSV_ABI sceAudioOutGetInfoOpenNum(); -int PS4_SYSV_ABI sceAudioOutGetLastOutputTime(s32 handle, u64* output_time); -int PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* state); -int PS4_SYSV_ABI sceAudioOutGetSimulatedBusUsableStatusByBusType(); -int PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo(); -int PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo2(); -int PS4_SYSV_ABI sceAudioOutGetSparkVss(); -int PS4_SYSV_ABI sceAudioOutGetSystemState(); -int PS4_SYSV_ABI sceAudioOutInit(); -int PS4_SYSV_ABI sceAudioOutInitIpmiGetSession(); -int PS4_SYSV_ABI sceAudioOutMasteringGetState(); -int PS4_SYSV_ABI sceAudioOutMasteringInit(); -int PS4_SYSV_ABI sceAudioOutMasteringSetParam(); -int PS4_SYSV_ABI sceAudioOutMasteringTerm(); -int PS4_SYSV_ABI sceAudioOutMbusInit(); +s32 PS4_SYSV_ABI sceAudioOutDeviceIdOpen(); +s32 PS4_SYSV_ABI sceAudioDeviceControlGet(); +s32 PS4_SYSV_ABI sceAudioDeviceControlSet(); +s32 PS4_SYSV_ABI sceAudioOutA3dControl(); +s32 PS4_SYSV_ABI sceAudioOutA3dExit(); +s32 PS4_SYSV_ABI sceAudioOutA3dInit(); +s32 PS4_SYSV_ABI sceAudioOutAttachToApplicationByPid(); +s32 PS4_SYSV_ABI sceAudioOutChangeAppModuleState(); +s32 PS4_SYSV_ABI sceAudioOutClose(s32 handle); +s32 PS4_SYSV_ABI sceAudioOutDetachFromApplicationByPid(); +s32 PS4_SYSV_ABI sceAudioOutExConfigureOutputMode(); +s32 PS4_SYSV_ABI sceAudioOutExGetSystemInfo(); +s32 PS4_SYSV_ABI sceAudioOutExPtClose(); +s32 PS4_SYSV_ABI sceAudioOutExPtGetLastOutputTime(); +s32 PS4_SYSV_ABI sceAudioOutExPtOpen(); +s32 PS4_SYSV_ABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode(); +s32 PS4_SYSV_ABI sceAudioOutGetFocusEnablePid(); +s32 PS4_SYSV_ABI sceAudioOutGetHandleStatusInfo(); +s32 PS4_SYSV_ABI sceAudioOutGetInfo(); +s32 PS4_SYSV_ABI sceAudioOutGetInfoOpenNum(); +s32 PS4_SYSV_ABI sceAudioOutGetLastOutputTime(s32 handle, u64* output_time); +s32 PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* state); +s32 PS4_SYSV_ABI sceAudioOutGetSimulatedBusUsableStatusByBusType(); +s32 PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo(); +s32 PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo2(); +s32 PS4_SYSV_ABI sceAudioOutGetSparkVss(); +s32 PS4_SYSV_ABI sceAudioOutGetSystemState(); +s32 PS4_SYSV_ABI sceAudioOutInit(); +s32 PS4_SYSV_ABI sceAudioOutInitIpmiGetSession(); +s32 PS4_SYSV_ABI sceAudioOutMasteringGetState(); +s32 PS4_SYSV_ABI sceAudioOutMasteringInit(u32 flags); +s32 PS4_SYSV_ABI sceAudioOutMasteringSetParam(); +s32 PS4_SYSV_ABI sceAudioOutMasteringTerm(); +s32 PS4_SYSV_ABI sceAudioOutMbusInit(); s32 PS4_SYSV_ABI sceAudioOutOpen(UserService::OrbisUserServiceUserId user_id, OrbisAudioOutPort port_type, s32 index, u32 length, u32 sample_rate, OrbisAudioOutParamExtendedInformation param_type); -int PS4_SYSV_ABI sceAudioOutOpenEx(); +s32 PS4_SYSV_ABI sceAudioOutOpenEx(); s32 PS4_SYSV_ABI sceAudioOutOutput(s32 handle, void* ptr); s32 PS4_SYSV_ABI sceAudioOutOutputs(OrbisAudioOutOutputParam* param, u32 num); -int PS4_SYSV_ABI sceAudioOutPtClose(); -int PS4_SYSV_ABI sceAudioOutPtGetLastOutputTime(); -int PS4_SYSV_ABI sceAudioOutPtOpen(); -int PS4_SYSV_ABI sceAudioOutSetConnections(); -int PS4_SYSV_ABI sceAudioOutSetConnectionsForUser(); -int PS4_SYSV_ABI sceAudioOutSetDevConnection(); -int PS4_SYSV_ABI sceAudioOutSetHeadphoneOutMode(); -int PS4_SYSV_ABI sceAudioOutSetJediJackVolume(); -int PS4_SYSV_ABI sceAudioOutSetJediSpkVolume(); -int PS4_SYSV_ABI sceAudioOutSetMainOutput(); -int PS4_SYSV_ABI sceAudioOutSetMixLevelPadSpk(); -int PS4_SYSV_ABI sceAudioOutSetMorpheusParam(); -int PS4_SYSV_ABI sceAudioOutSetMorpheusWorkingMode(); -int PS4_SYSV_ABI sceAudioOutSetPortConnections(); -int PS4_SYSV_ABI sceAudioOutSetPortStatuses(); -int PS4_SYSV_ABI sceAudioOutSetRecMode(); -int PS4_SYSV_ABI sceAudioOutSetSparkParam(); -int PS4_SYSV_ABI sceAudioOutSetUsbVolume(); +s32 PS4_SYSV_ABI sceAudioOutPtClose(); +s32 PS4_SYSV_ABI sceAudioOutPtGetLastOutputTime(); +s32 PS4_SYSV_ABI sceAudioOutPtOpen(); +s32 PS4_SYSV_ABI sceAudioOutSetConnections(); +s32 PS4_SYSV_ABI sceAudioOutSetConnectionsForUser(); +s32 PS4_SYSV_ABI sceAudioOutSetDevConnection(); +s32 PS4_SYSV_ABI sceAudioOutSetHeadphoneOutMode(); +s32 PS4_SYSV_ABI sceAudioOutSetJediJackVolume(); +s32 PS4_SYSV_ABI sceAudioOutSetJediSpkVolume(); +s32 PS4_SYSV_ABI sceAudioOutSetMainOutput(); +s32 PS4_SYSV_ABI sceAudioOutSetMixLevelPadSpk(s32 handle, s32 mixLevel); +s32 PS4_SYSV_ABI sceAudioOutSetMorpheusParam(); +s32 PS4_SYSV_ABI sceAudioOutSetMorpheusWorkingMode(); +s32 PS4_SYSV_ABI sceAudioOutSetPortConnections(); +s32 PS4_SYSV_ABI sceAudioOutSetPortStatuses(); +s32 PS4_SYSV_ABI sceAudioOutSetRecMode(); +s32 PS4_SYSV_ABI sceAudioOutSetSparkParam(); +s32 PS4_SYSV_ABI sceAudioOutSetUsbVolume(); s32 PS4_SYSV_ABI sceAudioOutSetVolume(s32 handle, s32 flag, s32* vol); -int PS4_SYSV_ABI sceAudioOutSetVolumeDown(); -int PS4_SYSV_ABI sceAudioOutStartAuxBroadcast(); -int PS4_SYSV_ABI sceAudioOutStartSharePlay(); -int PS4_SYSV_ABI sceAudioOutStopAuxBroadcast(); -int PS4_SYSV_ABI sceAudioOutStopSharePlay(); -int PS4_SYSV_ABI sceAudioOutSuspendResume(); -int PS4_SYSV_ABI sceAudioOutSysConfigureOutputMode(); -int PS4_SYSV_ABI sceAudioOutSysGetHdmiMonitorInfo(); -int PS4_SYSV_ABI sceAudioOutSysGetSystemInfo(); -int PS4_SYSV_ABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode(); -int PS4_SYSV_ABI sceAudioOutSystemControlGet(); -int PS4_SYSV_ABI sceAudioOutSystemControlSet(); -int PS4_SYSV_ABI sceAudioOutSparkControlSetEqCoef(); -int PS4_SYSV_ABI sceAudioOutSetSystemDebugState(); +s32 PS4_SYSV_ABI sceAudioOutSetVolumeDown(); +s32 PS4_SYSV_ABI sceAudioOutStartAuxBroadcast(); +s32 PS4_SYSV_ABI sceAudioOutStartSharePlay(); +s32 PS4_SYSV_ABI sceAudioOutStopAuxBroadcast(); +s32 PS4_SYSV_ABI sceAudioOutStopSharePlay(); +s32 PS4_SYSV_ABI sceAudioOutSuspendResume(); +s32 PS4_SYSV_ABI sceAudioOutSysConfigureOutputMode(); +s32 PS4_SYSV_ABI sceAudioOutSysGetHdmiMonitorInfo(); +s32 PS4_SYSV_ABI sceAudioOutSysGetSystemInfo(); +s32 PS4_SYSV_ABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode(); +s32 PS4_SYSV_ABI sceAudioOutSystemControlGet(); +s32 PS4_SYSV_ABI sceAudioOutSystemControlSet(); +s32 PS4_SYSV_ABI sceAudioOutSparkControlSetEqCoef(); +s32 PS4_SYSV_ABI sceAudioOutSetSystemDebugState(); void AdjustVol(); void RegisterLib(Core::Loader::SymbolsResolver* sym); diff --git a/src/core/libraries/audio/sdl_audio.cpp b/src/core/libraries/audio/sdl_audio.cpp deleted file mode 100644 index 46dd33d73..000000000 --- a/src/core/libraries/audio/sdl_audio.cpp +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include -#include - -#include "common/config.h" -#include "common/logging/log.h" -#include "core/libraries/audio/audioout.h" -#include "core/libraries/audio/audioout_backend.h" - -#define SDL_INVALID_AUDIODEVICEID 0 // Defined in SDL_audio.h but not made a macro -namespace Libraries::AudioOut { - -class SDLPortBackend : public PortBackend { -public: - explicit SDLPortBackend(const PortOut& port) - : frame_size(port.format_info.FrameSize()), guest_buffer_size(port.BufferSize()) { - const SDL_AudioSpec fmt = { - .format = port.format_info.is_float ? SDL_AUDIO_F32LE : SDL_AUDIO_S16LE, - .channels = port.format_info.num_channels, - .freq = static_cast(port.sample_rate), - }; - - // Determine port type - std::string port_name = port.type == OrbisAudioOutPort::PadSpk - ? Config::getPadSpkOutputDevice() - : Config::getMainOutputDevice(); - SDL_AudioDeviceID dev_id = SDL_INVALID_AUDIODEVICEID; - if (port_name == "None") { - stream = nullptr; - return; - } else if (port_name == "Default Device") { - dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; - } else { - try { - SDL_AudioDeviceID* dev_array = SDL_GetAudioPlaybackDevices(nullptr); - for (; dev_array != 0;) { - std::string dev_name(SDL_GetAudioDeviceName(*dev_array)); - if (dev_name == port_name) { - dev_id = *dev_array; - break; - } else { - dev_array++; - } - } - if (dev_id == SDL_INVALID_AUDIODEVICEID) { - LOG_WARNING(Lib_AudioOut, "Audio device not found: {}", port_name); - dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; - } - } catch (const std::exception& e) { - LOG_ERROR(Lib_AudioOut, "Invalid audio output device: {}", port_name); - stream = nullptr; - return; - } - } - - // Open the audio stream - stream = SDL_OpenAudioDeviceStream(dev_id, &fmt, nullptr, nullptr); - if (stream == nullptr) { - LOG_ERROR(Lib_AudioOut, "Failed to create SDL audio stream: {}", SDL_GetError()); - return; - } - CalculateQueueThreshold(); - if (!SDL_SetAudioStreamInputChannelMap(stream, port.format_info.channel_layout.data(), - port.format_info.num_channels)) { - LOG_ERROR(Lib_AudioOut, "Failed to configure SDL audio stream channel map: {}", - SDL_GetError()); - SDL_DestroyAudioStream(stream); - stream = nullptr; - return; - } - if (!SDL_ResumeAudioStreamDevice(stream)) { - LOG_ERROR(Lib_AudioOut, "Failed to resume SDL audio stream: {}", SDL_GetError()); - SDL_DestroyAudioStream(stream); - stream = nullptr; - return; - } - SDL_SetAudioStreamGain(stream, Config::getVolumeSlider() / 100.0f); - } - - ~SDLPortBackend() override { - if (!stream) { - return; - } - SDL_DestroyAudioStream(stream); - stream = nullptr; - } - - void Output(void* ptr) override { - if (!stream) { - return; - } - // AudioOut library manages timing, but we still need to guard against the SDL - // audio queue stalling, which may happen during device changes, for example. - // Otherwise, latency may grow over time unbounded. - if (const auto queued = SDL_GetAudioStreamQueued(stream); queued >= queue_threshold) { - LOG_INFO(Lib_AudioOut, "SDL audio queue backed up ({} queued, {} threshold), clearing.", - queued, queue_threshold); - SDL_ClearAudioStream(stream); - // Recalculate the threshold in case this happened because of a device change. - CalculateQueueThreshold(); - } - if (!SDL_PutAudioStreamData(stream, ptr, static_cast(guest_buffer_size))) { - LOG_ERROR(Lib_AudioOut, "Failed to output to SDL audio stream: {}", SDL_GetError()); - } - } - - void SetVolume(const std::array& ch_volumes) override { - if (!stream) { - return; - } - // SDL does not have per-channel volumes, for now just take the maximum of the channels. - const auto vol = *std::ranges::max_element(ch_volumes); - if (!SDL_SetAudioStreamGain(stream, static_cast(vol) / SCE_AUDIO_OUT_VOLUME_0DB * - Config::getVolumeSlider() / 100.0f)) { - LOG_WARNING(Lib_AudioOut, "Failed to change SDL audio stream volume: {}", - SDL_GetError()); - } - } - -private: - void CalculateQueueThreshold() { - SDL_AudioSpec discard; - int sdl_buffer_frames; - if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(stream), &discard, - &sdl_buffer_frames)) { - LOG_WARNING(Lib_AudioOut, "Failed to get SDL audio stream buffer size: {}", - SDL_GetError()); - sdl_buffer_frames = 0; - } - const auto sdl_buffer_size = sdl_buffer_frames * frame_size; - const auto new_threshold = std::max(guest_buffer_size, sdl_buffer_size) * 4; - if (host_buffer_size != sdl_buffer_size || queue_threshold != new_threshold) { - host_buffer_size = sdl_buffer_size; - queue_threshold = new_threshold; - LOG_INFO(Lib_AudioOut, - "SDL audio buffers: guest = {} bytes, host = {} bytes, threshold = {} bytes", - guest_buffer_size, host_buffer_size, queue_threshold); - } - } - - u32 frame_size; - u32 guest_buffer_size; - u32 host_buffer_size{}; - u32 queue_threshold{}; - SDL_AudioStream* stream{}; -}; - -std::unique_ptr SDLAudioOut::Open(PortOut& port) { - return std::make_unique(port); -} - -} // namespace Libraries::AudioOut diff --git a/src/core/libraries/audio/sdl_audio_in.cpp b/src/core/libraries/audio/sdl_audio_in.cpp new file mode 100644 index 000000000..d36811175 --- /dev/null +++ b/src/core/libraries/audio/sdl_audio_in.cpp @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include "audioin.h" +#include "audioin_backend.h" + +namespace Libraries::AudioIn { + +class SDLInPortBackend : public PortInBackend { +public: + explicit SDLInPortBackend(const PortIn& port) : port(port) { + SDL_AudioFormat sampleFormat = SDL_AUDIO_S16; // PS4 uses S16 format + + SDL_AudioSpec fmt; + SDL_zero(fmt); + fmt.format = sampleFormat; + fmt.channels = static_cast(port.channels_num); + fmt.freq = static_cast(port.freq); + + std::string micDevStr = Config::getMicDevice(); + uint32_t devId = 0; + if (micDevStr == "None") { + nullDevice = true; + LOG_INFO(Lib_AudioIn, "Audio input disabled by configuration"); + } else if (micDevStr == "Default Device") { + devId = SDL_AUDIO_DEVICE_DEFAULT_RECORDING; + LOG_INFO(Lib_AudioIn, "Using default audio input device"); + } else { + try { + devId = static_cast(std::stoul(micDevStr)); + LOG_INFO(Lib_AudioIn, "Using audio input device ID: {}", devId); + } catch (const std::exception& e) { + nullDevice = true; + LOG_WARNING(Lib_AudioIn, "Invalid device ID '{}', disabling input", micDevStr); + } + } + + if (!nullDevice) { + stream = SDL_OpenAudioDeviceStream(devId, &fmt, nullptr, nullptr); + if (stream) { + if (SDL_ResumeAudioStreamDevice(stream)) { + LOG_INFO(Lib_AudioIn, "Audio input opened: {} Hz, {} channels, format {}", + port.freq, port.channels_num, static_cast(port.format)); + } else { + SDL_DestroyAudioStream(stream); + stream = nullptr; + LOG_ERROR(Lib_AudioIn, "Failed to resume audio input stream"); + } + } else { + LOG_ERROR(Lib_AudioIn, "Failed to open audio input device: {}", SDL_GetError()); + } + } + + // Allocate internal buffer for null device simulation + if (!stream) { + const size_t bufferSize = port.samples_num * port.sample_size * port.channels_num; + internal_buffer = std::malloc(bufferSize); + if (internal_buffer) { + // Fill with silence + std::memset(internal_buffer, 0, bufferSize); + LOG_INFO(Lib_AudioIn, "Created null audio input buffer of {} bytes", bufferSize); + } + } + } + + ~SDLInPortBackend() override { + if (stream) { + SDL_DestroyAudioStream(stream); + } + if (internal_buffer) { + std::free(internal_buffer); + internal_buffer = nullptr; + } + } + + int Read(void* out_buffer) override { + const int bytesToRead = port.samples_num * port.sample_size * port.channels_num; + + if (stream) { + // Read from actual audio device + int attempts = 0; + while (SDL_GetAudioStreamAvailable(stream) < bytesToRead) { + SDL_Delay(1); + if (++attempts > 1000) { + return 0; + } + } + + const int bytesRead = SDL_GetAudioStreamData(stream, out_buffer, bytesToRead); + if (bytesRead < 0) { + LOG_ERROR(Lib_AudioIn, "Audio input read error: {}", SDL_GetError()); + return 0; + } + + const int framesRead = bytesRead / (port.sample_size * port.channels_num); + return framesRead; + } else if (internal_buffer) { + // Return silence from null device buffer + std::memcpy(out_buffer, internal_buffer, bytesToRead); + return port.samples_num; + } else { + // No device available + return 0; + } + } + + void Clear() override { + if (stream) { + SDL_ClearAudioStream(stream); + } + } + bool IsAvailable() override { + if (nullDevice) { + return false; + } else { + return true; + } + } + +private: + const PortIn& port; + SDL_AudioStream* stream = nullptr; + void* internal_buffer = nullptr; + bool nullDevice = false; +}; + +std::unique_ptr SDLAudioIn::Open(PortIn& port) { + return std::make_unique(port); +} + +} // namespace Libraries::AudioIn \ No newline at end of file diff --git a/src/core/libraries/audio/sdl_audio_out.cpp b/src/core/libraries/audio/sdl_audio_out.cpp new file mode 100644 index 000000000..572525c85 --- /dev/null +++ b/src/core/libraries/audio/sdl_audio_out.cpp @@ -0,0 +1,455 @@ +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include + +#include "common/config.h" +#include "common/logging/log.h" +#include "core/libraries/audio/audioout.h" +#include "core/libraries/audio/audioout_backend.h" +#include "core/libraries/kernel/threads.h" + +#define SDL_INVALID_AUDIODEVICEID 0 + +namespace Libraries::AudioOut { + +// Volume constants +constexpr float VOLUME_0DB = 32768.0f; // 1 << 15 + +// Channel positions +enum ChannelPos { + 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 SDLPortBackend : public PortBackend { +public: + explicit SDLPortBackend(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) { + + // Calculate timing + period_us = (1000000ULL * buffer_frames + sample_rate / 2) / sample_rate; + last_output_time = 0; + next_output_time = 0; + + // Allocate internal buffer + internal_buffer_size = buffer_frames * sizeof(float) * num_channels; + internal_buffer = std::malloc(internal_buffer_size); + if (!internal_buffer) { + LOG_ERROR(Lib_AudioOut, "Failed to allocate internal audio buffer"); + return; + } + + // Initialize current gain + current_gain.store(Config::getVolumeSlider() / 100.0f); + + // Select converter function + SelectConverter(); + + // Open SDL device + if (!OpenDevice(port.type)) { + std::free(internal_buffer); + internal_buffer = nullptr; + return; + } + + CalculateQueueThreshold(); + } + + ~SDLPortBackend() override { + if (stream) { + SDL_DestroyAudioStream(stream); + } + if (internal_buffer) { + std::free(internal_buffer); + } + } + + void Output(void* ptr) override { + if (!stream || !internal_buffer) { + return; + } + + // Check for volume changes and update if needed + UpdateVolumeIfChanged(); + + // Get current time in microseconds + u64 current_time = Kernel::sceKernelGetProcessTime(); + + if (ptr != nullptr) { + // Simple format conversion (no volume application) + convert(ptr, internal_buffer, buffer_frames, nullptr); + + if (next_output_time == 0) { + next_output_time = current_time + period_us; + } else if (current_time > next_output_time) { + next_output_time = current_time + period_us; + } else { + u64 wait_until = next_output_time; + next_output_time += period_us; + + if (current_time < wait_until) { + u64 sleep_us = wait_until - current_time; + if (sleep_us > 10) { + sleep_us -= 10; + std::this_thread::sleep_for(std::chrono::microseconds(sleep_us)); + } + } + } + + last_output_time = current_time; + + // Check queue and clear if backed up + if (const auto queued = SDL_GetAudioStreamQueued(stream); queued >= queue_threshold) { + LOG_DEBUG(Lib_AudioOut, "Clearing backed up audio queue ({} >= {})", queued, + queue_threshold); + SDL_ClearAudioStream(stream); + CalculateQueueThreshold(); + } + + if (!SDL_PutAudioStreamData(stream, internal_buffer, internal_buffer_size)) { + LOG_ERROR(Lib_AudioOut, "Failed to output to SDL audio stream: {}", SDL_GetError()); + } + } + } + + void SetVolume(const std::array& ch_volumes) override { + if (!stream) { + return; + } + float max_channel_gain = 0.0f; + for (int i = 0; i < num_channels && i < 8; i++) { + float channel_gain = static_cast(ch_volumes[i]) / VOLUME_0DB; + max_channel_gain = std::max(max_channel_gain, channel_gain); + } + + // Combine with global volume slider + float total_gain = max_channel_gain * (Config::getVolumeSlider() / 100.0f); + + std::lock_guard lock(volume_mutex); + if (SDL_SetAudioStreamGain(stream, total_gain)) { + current_gain.store(total_gain); + LOG_DEBUG(Lib_AudioOut, + "Set combined audio gain to {:.3f} (channel: {:.3f}, slider: {:.3f})", + total_gain, max_channel_gain, Config::getVolumeSlider() / 100.0f); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to set audio stream gain: {}", SDL_GetError()); + } + } + + u64 GetLastOutputTime() const { + return last_output_time; + } + +private: + std::atomic volume_update_needed{false}; + u64 last_volume_check_time{0}; + static constexpr u64 VOLUME_CHECK_INTERVAL_US = 50000; // Check every 50ms + + void UpdateVolumeIfChanged() { + u64 current_time = Kernel::sceKernelGetProcessTime(); + + // Only check volume every 50ms to reduce overhead + if (current_time - last_volume_check_time >= VOLUME_CHECK_INTERVAL_US) { + last_volume_check_time = current_time; + + float config_volume = Config::getVolumeSlider() / 100.0f; + float stored_gain = current_gain.load(); + + if (std::abs(config_volume - stored_gain) > 0.001f) { + if (SDL_SetAudioStreamGain(stream, config_volume)) { + current_gain.store(config_volume); + LOG_DEBUG(Lib_AudioOut, "Updated audio gain to {:.3f}", config_volume); + } else { + LOG_ERROR(Lib_AudioOut, "Failed to set audio stream gain: {}", SDL_GetError()); + } + } + } + } + bool OpenDevice(OrbisAudioOutPort type) { + const SDL_AudioSpec fmt = { + .format = SDL_AUDIO_F32LE, // Always use float for internal processing + .channels = static_cast(num_channels), + .freq = static_cast(sample_rate), + }; + + // Determine device name + std::string device_name = GetDeviceName(type); + SDL_AudioDeviceID dev_id = SDL_INVALID_AUDIODEVICEID; + + if (device_name == "None") { + LOG_INFO(Lib_AudioOut, "Audio device disabled for port type {}", + static_cast(type)); + return false; + } else if (device_name.empty() || device_name == "Default Device") { + dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; + } else { + int num_devices = 0; + SDL_AudioDeviceID* dev_array = SDL_GetAudioPlaybackDevices(&num_devices); + + if (dev_array) { + bool found = false; + for (int i = 0; i < num_devices; i++) { + const char* dev_name = SDL_GetAudioDeviceName(dev_array[i]); + if (dev_name && std::string(dev_name) == device_name) { + dev_id = dev_array[i]; + found = true; + break; + } + } + SDL_free(dev_array); + + if (!found) { + LOG_WARNING(Lib_AudioOut, "Audio device '{}' not found, using default", + device_name); + dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; + } + } else { + LOG_WARNING(Lib_AudioOut, "No audio devices found, using default"); + dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; + } + } + + // Create audio stream + stream = SDL_OpenAudioDeviceStream(dev_id, &fmt, nullptr, nullptr); + if (!stream) { + LOG_ERROR(Lib_AudioOut, "Failed to create SDL audio stream: {}", SDL_GetError()); + return false; + } + + // Set channel map + if (num_channels > 0) { + std::vector channel_map(num_channels); + + if (is_std && num_channels == 8) { + // Standard 8CH layout + channel_map = {FL, FR, FC, LF, STD_SL, STD_SR, STD_BL, STD_BR}; + } else { + // Use provided channel layout + for (int i = 0; i < num_channels; i++) { + channel_map[i] = channel_layout[i]; + } + } + + if (!SDL_SetAudioStreamInputChannelMap(stream, channel_map.data(), num_channels)) { + LOG_ERROR(Lib_AudioOut, "Failed to set channel map: {}", SDL_GetError()); + SDL_DestroyAudioStream(stream); + stream = nullptr; + return false; + } + } + + // Set initial volume + float initial_gain = current_gain.load(); + if (!SDL_SetAudioStreamGain(stream, initial_gain)) { + LOG_WARNING(Lib_AudioOut, "Failed to set initial audio gain: {}", SDL_GetError()); + } + + // Start playback + if (!SDL_ResumeAudioStreamDevice(stream)) { + LOG_ERROR(Lib_AudioOut, "Failed to resume audio stream: {}", SDL_GetError()); + SDL_DestroyAudioStream(stream); + stream = nullptr; + return false; + } + + LOG_INFO(Lib_AudioOut, "Opened audio device: {} ({} Hz, {} ch, gain: {:.3f})", device_name, + sample_rate, num_channels, initial_gain); + return true; + } + + std::string GetDeviceName(OrbisAudioOutPort type) { + switch (type) { + case OrbisAudioOutPort::Main: + case OrbisAudioOutPort::Bgm: + return Config::getMainOutputDevice(); + // case OrbisAudioOutPort::Voice: + // case OrbisAudioOutPort::Personal: + // return Config::getHeadphoneOutputDevice(); + case OrbisAudioOutPort::PadSpk: + return Config::getPadSpkOutputDevice(); + // case OrbisAudioOutPort::Aux: + // return Config::getSpecialOutputDevice(); + default: + return Config::getMainOutputDevice(); + } + } + + void SelectConverter() { + if (is_float) { + switch (num_channels) { + case 1: + convert = &ConvertF32Mono; + break; + case 2: + convert = &ConvertF32Stereo; + break; + case 8: + if (is_std) { + convert = &ConvertF32Std8CH; + } else { + convert = &ConvertF32_8CH; + } + break; + default: + LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); + convert = nullptr; + } + } else { + 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); + convert = nullptr; + } + } + } + + void CalculateQueueThreshold() { + if (!stream) + return; + + SDL_AudioSpec discard; + int sdl_buffer_frames; + if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(stream), &discard, + &sdl_buffer_frames)) { + LOG_WARNING(Lib_AudioOut, "Failed to get SDL buffer size: {}", SDL_GetError()); + sdl_buffer_frames = 0; + } + + u32 sdl_buffer_size = sdl_buffer_frames * sizeof(float) * num_channels; + queue_threshold = std::max(guest_buffer_size, sdl_buffer_size) * 4; + + LOG_DEBUG(Lib_AudioOut, "Audio queue threshold: {} bytes (SDL buffer: {} frames)", + queue_threshold, sdl_buffer_frames); + } + + using ConverterFunc = void (*)(const void* src, void* dst, u32 frames, const float* volumes); + + // Remove volume parameter and application from all converters + static void ConvertS16Mono(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + constexpr float inv_scale = 1.0f / VOLUME_0DB; + + for (u32 i = 0; i < frames; i++) { + d[i] = s[i] * inv_scale; + } + } + + static void ConvertS16Stereo(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + constexpr float inv_scale = 1.0f / VOLUME_0DB; + + for (u32 i = 0; i < frames; i++) { + d[i * 2] = s[i * 2] * inv_scale; + d[i * 2 + 1] = s[i * 2 + 1] * inv_scale; + } + } + + static void ConvertS16_8CH(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + constexpr float inv_scale = 1.0f / VOLUME_0DB; + + for (u32 i = 0; i < frames; i++) { + for (int ch = 0; ch < 8; ch++) { + d[i * 8 + ch] = s[i * 8 + ch] * inv_scale; + } + } + } + + // Float converters become simple memcpy or passthrough + static void ConvertF32Mono(const void* src, void* dst, u32 frames, const float*) { + std::memcpy(dst, src, frames * sizeof(float)); + } + + static void ConvertF32Stereo(const void* src, void* dst, u32 frames, const float*) { + std::memcpy(dst, src, frames * 2 * sizeof(float)); + } + + static void ConvertF32_8CH(const void* src, void* dst, u32 frames, const float*) { + std::memcpy(dst, src, frames * 8 * sizeof(float)); + } + + static void ConvertF32Std8CH(const void* src, void* dst, u32 frames, const float*) { + const float* s = static_cast(src); + float* d = static_cast(dst); + + for (u32 i = 0; i < frames; i++) { + d[i * 8 + FL] = s[i * 8 + FL]; + d[i * 8 + FR] = s[i * 8 + FR]; + d[i * 8 + FC] = s[i * 8 + FC]; + d[i * 8 + LF] = s[i * 8 + LF]; + d[i * 8 + SL] = s[i * 8 + STD_SL]; // Channel remapping still needed + d[i * 8 + SR] = s[i * 8 + STD_SR]; + d[i * 8 + BL] = s[i * 8 + STD_BL]; + d[i * 8 + BR] = s[i * 8 + STD_BR]; + } + } + + // Member variables + u32 frame_size; + u32 guest_buffer_size; + u32 buffer_frames; + u32 sample_rate; + u32 num_channels; + bool is_float; + bool is_std; + std::array channel_layout; + + u64 period_us; + u64 last_output_time; + u64 next_output_time; + + // Buffers + u32 internal_buffer_size; + void* internal_buffer; + + // Converter function + ConverterFunc convert; + + // Volume tracking + std::atomic current_gain{1.0f}; + mutable std::mutex volume_mutex; + + // SDL + SDL_AudioStream* stream{}; + u32 queue_threshold{}; +}; + +std::unique_ptr SDLAudioOut::Open(PortOut& port) { + return std::make_unique(port); +} + +} // namespace Libraries::AudioOut \ No newline at end of file diff --git a/src/core/libraries/audio/sdl_in.cpp b/src/core/libraries/audio/sdl_in.cpp deleted file mode 100644 index 30bc0c578..000000000 --- a/src/core/libraries/audio/sdl_in.cpp +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include -#include "sdl_in.h" - -int SDLAudioIn::AudioInit() { - return SDL_InitSubSystem(SDL_INIT_AUDIO); -} - -int SDLAudioIn::AudioInOpen(int type, uint32_t samples_num, uint32_t freq, uint32_t format) { - std::scoped_lock lock{m_mutex}; - - for (int id = 0; id < static_cast(portsIn.size()); ++id) { - auto& port = portsIn[id]; - if (!port.isOpen) { - port.isOpen = true; - port.type = type; - port.samples_num = samples_num; - port.freq = freq; - port.format = format; - - SDL_AudioFormat sampleFormat; - switch (format) { - case Libraries::AudioIn::ORBIS_AUDIO_IN_PARAM_FORMAT_S16_MONO: - sampleFormat = SDL_AUDIO_S16; - port.channels_num = 1; - port.sample_size = 2; - break; - case Libraries::AudioIn::ORBIS_AUDIO_IN_PARAM_FORMAT_S16_STEREO: - sampleFormat = SDL_AUDIO_S16; - port.channels_num = 2; - port.sample_size = 2; - break; - default: - port.isOpen = false; - return ORBIS_AUDIO_IN_ERROR_INVALID_PORT; - } - - SDL_AudioSpec fmt; - SDL_zero(fmt); - fmt.format = sampleFormat; - fmt.channels = port.channels_num; - fmt.freq = port.freq; - - std::string micDevStr = Config::getMicDevice(); - uint32_t devId; - - bool nullDevice = false; - if (micDevStr == "None") { - nullDevice = true; - } else if (micDevStr == "Default Device") { - devId = SDL_AUDIO_DEVICE_DEFAULT_RECORDING; - } else { - try { - devId = static_cast(std::stoul(micDevStr)); - } catch (const std::exception& e) { - nullDevice = true; - } - } - - port.stream = - nullDevice ? nullptr : SDL_OpenAudioDeviceStream(devId, &fmt, nullptr, nullptr); - - if (!port.stream) { - // if stream is null, either due to configuration disabling the input, - // or no input devices present in the system, still return a valid id - // as some games require that (e.g. L.A. Noire) - return id + 1; - } - - if (SDL_ResumeAudioStreamDevice(port.stream) == false) { - SDL_DestroyAudioStream(port.stream); - port = {}; - return ORBIS_AUDIO_IN_ERROR_STREAM_FAIL; - } - - return id + 1; - } - } - - return ORBIS_AUDIO_IN_ERROR_INVALID_PORT; -} - -int SDLAudioIn::AudioInInput(int handle, void* out_buffer) { - std::scoped_lock lock{m_mutex}; - - if (handle < 1 || handle > static_cast(portsIn.size()) || !out_buffer) - return ORBIS_AUDIO_IN_ERROR_INVALID_PORT; - - auto& port = portsIn[handle - 1]; - if (!port.isOpen) - return ORBIS_AUDIO_IN_ERROR_INVALID_PORT; - - const int bytesToRead = port.samples_num * port.sample_size * port.channels_num; - - if (out_buffer == nullptr) { - int attempts = 0; - while (SDL_GetAudioStreamAvailable(port.stream) > 0) { - SDL_Delay(1); - if (++attempts > 1000) { - return ORBIS_AUDIO_IN_ERROR_TIMEOUT; - } - } - return 0; // done - } - - int attempts = 0; - while (SDL_GetAudioStreamAvailable(port.stream) < bytesToRead) { - SDL_Delay(1); - if (++attempts > 1000) { - return ORBIS_AUDIO_IN_ERROR_TIMEOUT; - } - } - - const int bytesRead = SDL_GetAudioStreamData(port.stream, out_buffer, bytesToRead); - if (bytesRead < 0) { - // SDL_GetAudioStreamData failed - LOG_ERROR(Lib_AudioIn, "AudioInInput error: {}", SDL_GetError()); - return ORBIS_AUDIO_IN_ERROR_STREAM_FAIL; - } - const int framesRead = bytesRead / (port.sample_size * port.channels_num); - return framesRead; -} - -void SDLAudioIn::AudioInClose(int handle) { - std::scoped_lock lock{m_mutex}; - if (handle < 1 || handle > (int)portsIn.size()) - return; - - auto& port = portsIn[handle - 1]; - if (!port.isOpen) - return; - - SDL_DestroyAudioStream(port.stream); - port = {}; -} \ No newline at end of file diff --git a/src/core/libraries/audio/sdl_in.h b/src/core/libraries/audio/sdl_in.h deleted file mode 100644 index e1b2a4682..000000000 --- a/src/core/libraries/audio/sdl_in.h +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include - -namespace Libraries::AudioIn { -enum OrbisAudioInParam { - ORBIS_AUDIO_IN_PARAM_FORMAT_S16_MONO = 0, - ORBIS_AUDIO_IN_PARAM_FORMAT_S16_STEREO = 2 -}; -} - -#define ORBIS_AUDIO_IN_ERROR_INVALID_PORT -1 -#define ORBIS_AUDIO_IN_ERROR_TIMEOUT -2 -#define ORBIS_AUDIO_IN_ERROR_STREAM_FAIL -3 - -class SDLAudioIn { -public: - int AudioInit(); - int AudioInOpen(int type, uint32_t samples_num, uint32_t freq, uint32_t format); - int AudioInInput(int handle, void* out_buffer); - void AudioInClose(int handle); - -private: - struct AudioInPort { - bool isOpen = false; - int type = 0; - uint32_t samples_num = 0; - uint32_t freq = 0; - int channels_num = 0; - int sample_size = 0; - uint32_t format = 0; - SDL_AudioStream* stream = nullptr; - }; - - std::array portsIn; - std::mutex m_mutex; -}; diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index e74569737..e6705edad 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -108,6 +108,8 @@ auto output_array = std::array{ ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_GYRO), ControllerOutput(HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD), ControllerOutput(HOTKEY_RENDERDOC), + ControllerOutput(HOTKEY_VOLUME_UP), + ControllerOutput(HOTKEY_VOLUME_DOWN), ControllerOutput(SDL_GAMEPAD_BUTTON_INVALID, SDL_GAMEPAD_AXIS_INVALID), }; @@ -562,6 +564,9 @@ void ControllerOutput::FinalizeUpdate() { case RIGHTJOYSTICK_HALFMODE: rightjoystick_halfmode = new_button_state; break; + case HOTKEY_RELOAD_INPUTS: + ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial())); + break; case HOTKEY_FULLSCREEN: PushSDLEvent(SDL_EVENT_TOGGLE_FULLSCREEN); break; @@ -571,9 +576,6 @@ void ControllerOutput::FinalizeUpdate() { case HOTKEY_SIMPLE_FPS: PushSDLEvent(SDL_EVENT_TOGGLE_SIMPLE_FPS); break; - case HOTKEY_RELOAD_INPUTS: - PushSDLEvent(SDL_EVENT_RELOAD_INPUTS); - break; case HOTKEY_TOGGLE_MOUSE_TO_JOYSTICK: PushSDLEvent(SDL_EVENT_MOUSE_TO_JOYSTICK); break; @@ -586,6 +588,12 @@ void ControllerOutput::FinalizeUpdate() { case HOTKEY_RENDERDOC: PushSDLEvent(SDL_EVENT_RDOC_CAPTURE); break; + case HOTKEY_VOLUME_UP: + Config::setVolumeSlider(Config::getVolumeSlider() + 10, true); + break; + case HOTKEY_VOLUME_DOWN: + Config::setVolumeSlider(Config::getVolumeSlider() - 10, true); + break; case HOTKEY_QUIT: PushSDLEvent(SDL_EVENT_QUIT_DIALOG); break; diff --git a/src/input/input_handler.h b/src/input/input_handler.h index eaadd164e..43c09ba55 100644 --- a/src/input/input_handler.h +++ b/src/input/input_handler.h @@ -55,6 +55,8 @@ #define HOTKEY_TOGGLE_MOUSE_TO_GYRO 0xf0000007 #define HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD 0xf0000008 #define HOTKEY_RENDERDOC 0xf0000009 +#define HOTKEY_VOLUME_UP 0xf000000a +#define HOTKEY_VOLUME_DOWN 0xf000000b #define SDL_UNMAPPED UINT32_MAX - 1 @@ -145,6 +147,8 @@ const std::map string_to_cbutton_map = { {"hotkey_toggle_mouse_to_gyro", HOTKEY_TOGGLE_MOUSE_TO_GYRO}, {"hotkey_toggle_mouse_to_touchpad", HOTKEY_TOGGLE_MOUSE_TO_TOUCHPAD}, {"hotkey_renderdoc_capture", HOTKEY_RENDERDOC}, + {"hotkey_volume_up", HOTKEY_VOLUME_UP}, + {"hotkey_volume_down", HOTKEY_VOLUME_DOWN}, }; const std::map string_to_axis_map = { From 1dc45ab6b3b18e09566bf16215b2f552e8da84d1 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 7 Feb 2026 21:05:47 +0200 Subject: [PATCH 09/20] npWebApi (#3878) * added function parameters * added logging * more logging * added error codes file * sceNpWebApiCreateExtdPushEventFilter some re * added np_web_api_internal * more np_web_api_internal definations * Initial types cleanup * Basic library context handling. Followed decomp closely, using standard library classes where possible to simplify code. * Fix params to sceNpWebApiCreateContext * Context logic fixes * User contexts * Clang * sceNpWebApiVshInitialize * Better initialization * Request creation logic * Some cleanup * sceNpWebApiAbortRequest, sceNpWebApiDeleteRequest * SendRequest functions * formatting * Update terminateContext and deleteUserContext Addressing some unimplemented bits now that I have requests and user contexts here. * Copyright * sceNpWebApiCreateHandle, sceNpWebApiDeleteHandle, sceNpWebApiAbortHandle also some bugfixing * Extended push event filter * abort handles in terminateContext * Other push event filter types * Register callbacks * unregister callbacks * oops * One final update to deleteContext * Logging changes * Bug fixes Fixes memory leaks, pretty sure these are the only places where that was an issue. * sceNpWebApiCheckTimeout * Handle and request timeouts * Oops * Push event filter parameters Tested with Assassin's Creed Unity, seems to be correct. * Service push event filter parameters Tested again with Assassin's Creed Unity, seems to work fine. Also fixed some code bugs I noticed, and removed an unnecessary part of my internal structs * Stub implementation for createUserContextWithOnlineId Might need a PSN check to be properly accurate, not sure. * added sceNpWebApiGetHttpStatusCode * opps * opss 2 * sceNpWebApiReadData * clang * Fix context ids, user context ids, and request ids Overlooked how these ids are actually calculated. * Additional PSN checks Seems creating any form of push event filter with an np service name fails when you're not connected to PSN. Not sure of the actual cause yet, but given the error code, it's related to sceNpManagerIntGetUserList. * compile fix --------- Co-authored-by: Stephen Miller Co-authored-by: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> --- CMakeLists.txt | 3 + src/common/elf_info.h | 1 + src/core/libraries/network/http.cpp | 4 +- src/core/libraries/network/http.h | 2 +- src/core/libraries/np/np_common.h | 2 +- src/core/libraries/np/np_matching2.cpp | 2 +- src/core/libraries/np/np_types.h | 18 +- src/core/libraries/np/np_web_api.cpp | 777 ++++++--- src/core/libraries/np/np_web_api.h | 214 +-- src/core/libraries/np/np_web_api_error.h | 36 + src/core/libraries/np/np_web_api_internal.cpp | 1534 +++++++++++++++++ src/core/libraries/np/np_web_api_internal.h | 301 ++++ 12 files changed, 2543 insertions(+), 351 deletions(-) create mode 100644 src/core/libraries/np/np_web_api_error.h create mode 100644 src/core/libraries/np/np_web_api_internal.cpp create mode 100644 src/core/libraries/np/np_web_api_internal.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 254d7059c..8cef2df24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -598,6 +598,9 @@ set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/trophy_ui.h src/core/libraries/np/np_web_api.cpp src/core/libraries/np/np_web_api.h + src/core/libraries/np/np_web_api_error.h + src/core/libraries/np/np_web_api_internal.cpp + src/core/libraries/np/np_web_api_internal.h src/core/libraries/np/np_web_api2.cpp src/core/libraries/np/np_web_api2.h src/core/libraries/np/np_party.cpp diff --git a/src/common/elf_info.h b/src/common/elf_info.h index 0b2589e95..0f2311cb0 100644 --- a/src/common/elf_info.h +++ b/src/common/elf_info.h @@ -75,6 +75,7 @@ class ElfInfo { std::filesystem::path game_folder{}; public: + static constexpr u32 FW_10 = 0x1000000; static constexpr u32 FW_15 = 0x1500000; static constexpr u32 FW_16 = 0x1600000; static constexpr u32 FW_17 = 0x1700000; diff --git a/src/core/libraries/network/http.cpp b/src/core/libraries/network/http.cpp index ebb10db68..8bc9b51f0 100644 --- a/src/core/libraries/network/http.cpp +++ b/src/core/libraries/network/http.cpp @@ -430,8 +430,8 @@ int PS4_SYSV_ABI sceHttpParseStatusLine(const char* statusLine, u64 lineLen, int return index + 1; } -int PS4_SYSV_ABI sceHttpReadData() { - LOG_ERROR(Lib_Http, "(STUBBED) called"); +int PS4_SYSV_ABI sceHttpReadData(s32 reqId, void* data, u64 size) { + LOG_ERROR(Lib_Http, "(STUBBED) called reqId = {} size = {}", reqId, size); return ORBIS_OK; } diff --git a/src/core/libraries/network/http.h b/src/core/libraries/network/http.h index 2ad5e171f..d373fd290 100644 --- a/src/core/libraries/network/http.h +++ b/src/core/libraries/network/http.h @@ -91,7 +91,7 @@ int PS4_SYSV_ABI sceHttpParseResponseHeader(const char* header, u64 headerLen, c int PS4_SYSV_ABI sceHttpParseStatusLine(const char* statusLine, u64 lineLen, int32_t* httpMajorVer, int32_t* httpMinorVer, int32_t* responseCode, const char** reasonPhrase, u64* phraseLen); -int PS4_SYSV_ABI sceHttpReadData(); +int PS4_SYSV_ABI sceHttpReadData(s32 reqId, void* data, u64 size); int PS4_SYSV_ABI sceHttpRedirectCacheFlush(); int PS4_SYSV_ABI sceHttpRemoveRequestHeader(); int PS4_SYSV_ABI sceHttpRequestGetAllHeaders(); diff --git a/src/core/libraries/np/np_common.h b/src/core/libraries/np/np_common.h index 2fd4ecd7c..a130f9c1d 100644 --- a/src/core/libraries/np/np_common.h +++ b/src/core/libraries/np/np_common.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/libraries/np/np_matching2.cpp b/src/core/libraries/np/np_matching2.cpp index cf4faea39..423b84257 100644 --- a/src/core/libraries/np/np_matching2.cpp +++ b/src/core/libraries/np/np_matching2.cpp @@ -234,7 +234,7 @@ int PS4_SYSV_ABI sceNpMatching2CreateJoinRoomA(OrbisNpMatching2ContextId ctxId, OrbisNpMatching2RoomMemberDataInternalA me{ nullptr, 0, - {0xace104e, Libraries::Np::OrbisNpPlatformType::ORBIS_NP_PLATFORM_TYPE_PS4}, + {0xace104e, Libraries::Np::OrbisNpPlatformType::PS4}, onlineId, {0, 0, 0, 0}, 1, diff --git a/src/core/libraries/np/np_types.h b/src/core/libraries/np/np_types.h index cc37b5a3d..58c119bec 100644 --- a/src/core/libraries/np/np_types.h +++ b/src/core/libraries/np/np_types.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -46,18 +46,20 @@ struct OrbisNpIdToken { }; using OrbisNpServiceLabel = u32; +constexpr s32 ORBIS_NP_INVALID_SERVICE_LABEL = 0xFFFFFFFF; -enum class OrbisNpPlatformType : s32 { - ORBIS_NP_PLATFORM_TYPE_NONE = 0, - ORBIS_NP_PLATFORM_TYPE_PS3 = 1, - ORBIS_NP_PLATFORM_TYPE_VITA = 2, - ORBIS_NP_PLATFORM_TYPE_PS4 = 3, +using OrbisNpAccountId = u64; +enum OrbisNpPlatformType : s32 { + None = 0, + PS3 = 1, + Vita = 2, + PS4 = 3, }; struct OrbisNpPeerAddressA { OrbisNpAccountId accountId; - OrbisNpPlatformType platformType; - u8 padding[4]; + OrbisNpPlatformType platform; + char padding[4]; }; }; // namespace Libraries::Np \ No newline at end of file diff --git a/src/core/libraries/np/np_web_api.cpp b/src/core/libraries/np/np_web_api.cpp index db9d2f42a..e51b79a3c 100644 --- a/src/core/libraries/np/np_web_api.cpp +++ b/src/core/libraries/np/np_web_api.cpp @@ -1,155 +1,293 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "common/elf_info.h" #include "common/logging/log.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_web_api.h" +#include "core/libraries/np/np_web_api_error.h" +#include "core/libraries/np/np_web_api_internal.h" + +#include namespace Libraries::Np::NpWebApi { -s32 PS4_SYSV_ABI sceNpWebApiCreateContext() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); +static bool g_is_initialized = false; +static s32 g_active_library_contexts = 0; + +s32 PS4_SYSV_ABI sceNpWebApiCreateContext(s32 libCtxId, OrbisNpOnlineId* onlineId) { + if (libCtxId >= 0x8000) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_LIB_CONTEXT_ID; + } + if (onlineId == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + return createUserContextWithOnlineId(libCtxId, onlineId); +} + +s32 PS4_SYSV_ABI sceNpWebApiCreatePushEventFilter( + s32 libCtxId, const OrbisNpWebApiPushEventFilterParameter* pFilterParam, u64 filterParamNum) { + if (pFilterParam == nullptr || filterParamNum == 0) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_WARNING(Lib_NpWebApi, "called, libCtxId = {:#x}", libCtxId); + return createPushEventFilter(libCtxId, pFilterParam, filterParamNum); +} + +s32 PS4_SYSV_ABI sceNpWebApiCreateServicePushEventFilter( + s32 libCtxId, s32 handleId, const char* pNpServiceName, OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiServicePushEventFilterParameter* pFilterParam, u64 filterParamNum) { + if (pNpServiceName == nullptr || pFilterParam == nullptr || filterParamNum == 0) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + if (getCompiledSdkVersion() >= Common::ElfInfo::FW_20 && + npServiceLabel == ORBIS_NP_INVALID_SERVICE_LABEL) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_WARNING(Lib_NpWebApi, + "called, libCtxId = {:#x}, handleId = {:#x}, pNpServiceName = '{}', " + "npServiceLabel = {:#x}", + libCtxId, handleId, pNpServiceName, npServiceLabel); + return createServicePushEventFilter(libCtxId, handleId, pNpServiceName, npServiceLabel, + pFilterParam, filterParamNum); +} + +s32 PS4_SYSV_ABI sceNpWebApiDeletePushEventFilter(s32 libCtxId, s32 filterId) { + LOG_INFO(Lib_NpWebApi, "called, libCtxId = {:#x}, filterId = {:#x}"); + return deletePushEventFilter(libCtxId, filterId); +} + +s32 PS4_SYSV_ABI sceNpWebApiDeleteServicePushEventFilter(s32 libCtxId, s32 filterId) { + LOG_INFO(Lib_NpWebApi, "called, libCtxId = {:#x}, filterId = {:#x}"); + return deleteServicePushEventFilter(libCtxId, filterId); +} + +s32 PS4_SYSV_ABI sceNpWebApiRegisterExtdPushEventCallback(s32 titleUserCtxId, s32 filterId, + OrbisNpWebApiExtdPushEventCallback cbFunc, + void* pUserArg) { + if (cbFunc == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, filterId = {:#x}, cbFunc = {}", + titleUserCtxId, filterId, fmt::ptr(cbFunc)); + return registerExtdPushEventCallback(titleUserCtxId, filterId, cbFunc, nullptr, pUserArg); +} + +s32 PS4_SYSV_ABI sceNpWebApiRegisterNotificationCallback(s32 titleUserCtxId, + OrbisNpWebApiNotificationCallback cbFunc, + void* pUserArg) { + if (cbFunc == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, cbFunc = {}", titleUserCtxId, + fmt::ptr(cbFunc)); + return registerNotificationCallback(titleUserCtxId, cbFunc, pUserArg); +} + +s32 PS4_SYSV_ABI sceNpWebApiRegisterPushEventCallback(s32 titleUserCtxId, s32 filterId, + OrbisNpWebApiPushEventCallback cbFunc, + void* pUserArg) { + if (getCompiledSdkVersion() >= Common::ElfInfo::FW_10 && cbFunc == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, filterId = {:#x}, cbFunc = {}", + titleUserCtxId, filterId, fmt::ptr(cbFunc)); + return registerPushEventCallback(titleUserCtxId, filterId, cbFunc, pUserArg); +} + +s32 PS4_SYSV_ABI sceNpWebApiRegisterServicePushEventCallback( + s32 titleUserCtxId, s32 filterId, OrbisNpWebApiServicePushEventCallback cbFunc, + void* pUserArg) { + if (getCompiledSdkVersion() >= Common::ElfInfo::FW_10 && cbFunc == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, filterId = {:#x}, cbFunc = {}", + titleUserCtxId, filterId, fmt::ptr(cbFunc)); + return registerServicePushEventCallback(titleUserCtxId, filterId, cbFunc, nullptr, nullptr, + pUserArg); +} + +s32 PS4_SYSV_ABI sceNpWebApiUnregisterNotificationCallback(s32 titleUserCtxId) { + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}", titleUserCtxId); + return unregisterNotificationCallback(titleUserCtxId); +} + +s32 PS4_SYSV_ABI sceNpWebApiUnregisterPushEventCallback(s32 titleUserCtxId, s32 callbackId) { + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, callbackId = {:#x}", titleUserCtxId, + callbackId); + return unregisterPushEventCallback(titleUserCtxId, callbackId); +} + +s32 PS4_SYSV_ABI sceNpWebApiUnregisterServicePushEventCallback(s32 titleUserCtxId, s32 callbackId) { + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, callbackId = {:#x}", titleUserCtxId, + callbackId); + return unregisterServicePushEventCallback(titleUserCtxId, callbackId); +} + +s32 PS4_SYSV_ABI sceNpWebApiAbortHandle(s32 libCtxId, s32 handleId) { + LOG_INFO(Lib_NpWebApi, "called libCtxId = {:#x}, handleId = {:#x}", libCtxId, handleId); + return abortHandle(libCtxId, handleId); +} + +s32 PS4_SYSV_ABI sceNpWebApiAbortRequest(s64 requestId) { + LOG_INFO(Lib_NpWebApi, "called requestId = {:#x}", requestId); + return abortRequest(requestId); +} + +s32 PS4_SYSV_ABI sceNpWebApiAddHttpRequestHeader(s64 requestId, const char* pFieldName, + const char* pValue) { + LOG_ERROR(Lib_NpWebApi, + "called (STUBBED) : requestId = {:#x}, " + "pFieldName = '{}', pValue = '{}'", + requestId, (pFieldName ? pFieldName : "null"), (pValue ? pValue : "null")); return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpWebApiCreatePushEventFilter() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpWebApiAddMultipartPart(s64 requestId, + const OrbisNpWebApiMultipartPartParameter* pParam, + s32* pIndex) { + LOG_INFO(Lib_NpWebApi, + "called (STUBBED) : requestId = {:#x}, " + "pParam = {}, pIndex = {}", + requestId, fmt::ptr(pParam), fmt::ptr(pIndex)); + if (pParam) { + LOG_ERROR(Lib_NpWebApi, " Part params: headerNum = {}, contentLength = {}", + pParam->headerNum, pParam->contentLength); + } return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpWebApiCreateServicePushEventFilter() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); +void PS4_SYSV_ABI sceNpWebApiCheckTimeout() { + LOG_TRACE(Lib_NpWebApi, "called"); + if (!g_is_initialized) { + return; + } + return checkTimeout(); +} + +s32 PS4_SYSV_ABI sceNpWebApiClearAllUnusedConnection(s32 userCtxId, + bool bRemainKeepAliveConnection) { + LOG_ERROR(Lib_NpWebApi, + "called (STUBBED) : userCtxId = {:#x}, " + "bRemainKeepAliveConnection = {}", + userCtxId, bRemainKeepAliveConnection); return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpWebApiDeletePushEventFilter() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpWebApiClearUnusedConnection(s32 userCtxId, const char* pApiGroup, + bool bRemainKeepAliveConnection) { + LOG_ERROR(Lib_NpWebApi, + "called (STUBBED) : userCtxId = {:#x}, " + "pApiGroup = '{}', bRemainKeepAliveConnection = {}", + userCtxId, (pApiGroup ? pApiGroup : "null"), bRemainKeepAliveConnection); return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpWebApiDeleteServicePushEventFilter() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpWebApiCreateContextA(s32 libCtxId, + Libraries::UserService::OrbisUserServiceUserId userId) { + if (libCtxId >= 0x8000) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_LIB_CONTEXT_ID; + } + if (userId == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + return createUserContext(libCtxId, userId); } -s32 PS4_SYSV_ABI sceNpWebApiRegisterExtdPushEventCallback() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpWebApiCreateExtdPushEventFilter( + s32 libCtxId, s32 handleId, const char* pNpServiceName, OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiExtdPushEventFilterParameter* pFilterParam, u64 filterParamNum) { + if ((pNpServiceName != nullptr && npServiceLabel == ORBIS_NP_INVALID_SERVICE_LABEL) || + pFilterParam == nullptr || filterParamNum == 0) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + LOG_INFO( + Lib_NpWebApi, + "called, libCtxId = {:#x}, handleId = {:#x}, pNpServiceName = '{}', npServiceLabel = {:#x}", + libCtxId, handleId, (pNpServiceName ? pNpServiceName : "null"), npServiceLabel); + return createExtendedPushEventFilter(libCtxId, handleId, pNpServiceName, npServiceLabel, + pFilterParam, filterParamNum, false); } -s32 PS4_SYSV_ABI sceNpWebApiRegisterNotificationCallback() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpWebApiCreateHandle(s32 libCtxId) { + return createHandle(libCtxId); } -s32 PS4_SYSV_ABI sceNpWebApiRegisterPushEventCallback() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpWebApiCreateMultipartRequest(s32 titleUserCtxId, const char* pApiGroup, + const char* pPath, + OrbisNpWebApiHttpMethod method, + s64* pRequestId) { + if (pApiGroup == nullptr || pPath == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + if (getCompiledSdkVersion() >= Common::ElfInfo::FW_25 && + method > OrbisNpWebApiHttpMethod::ORBIS_NP_WEBAPI_HTTP_METHOD_DELETE) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + LOG_INFO(Lib_NpWebApi, + "called titleUserCtxId = {:#x}, pApiGroup = '{}', pPath = '{}', method = {}", + titleUserCtxId, pApiGroup, pPath, magic_enum::enum_name(method)); + + return createRequest(titleUserCtxId, pApiGroup, pPath, method, nullptr, nullptr, pRequestId, + true); } -s32 PS4_SYSV_ABI sceNpWebApiRegisterServicePushEventCallback() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpWebApiCreateRequest(s32 titleUserCtxId, const char* pApiGroup, + const char* pPath, OrbisNpWebApiHttpMethod method, + const OrbisNpWebApiContentParameter* pContentParameter, + s64* pRequestId) { + if (pApiGroup == nullptr || pPath == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + if (pContentParameter != nullptr && pContentParameter->contentLength != 0 && + pContentParameter->pContentType == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_CONTENT_PARAMETER; + } + + if (getCompiledSdkVersion() >= Common::ElfInfo::FW_25 && + method > OrbisNpWebApiHttpMethod::ORBIS_NP_WEBAPI_HTTP_METHOD_DELETE) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + LOG_INFO(Lib_NpWebApi, + "called titleUserCtxId = {:#x}, pApiGroup = '{}', pPath = '{}', method = {}", + titleUserCtxId, pApiGroup, pPath, magic_enum::enum_name(method)); + + return createRequest(titleUserCtxId, pApiGroup, pPath, method, pContentParameter, nullptr, + pRequestId, false); } -s32 PS4_SYSV_ABI sceNpWebApiUnregisterNotificationCallback() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpWebApiDeleteContext(s32 titleUserCtxId) { + LOG_INFO(Lib_NpWebApi, "called titleUserCtxId = {:#x}", titleUserCtxId); + return deleteUserContext(titleUserCtxId); } -s32 PS4_SYSV_ABI sceNpWebApiUnregisterPushEventCallback() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpWebApiDeleteExtdPushEventFilter(s32 libCtxId, s32 filterId) { + LOG_INFO(Lib_NpWebApi, "called libCtxId = {:#x}, filterId = {:#x}", libCtxId, filterId); + return deleteExtendedPushEventFilter(libCtxId, filterId); } -s32 PS4_SYSV_ABI sceNpWebApiUnregisterServicePushEventCallback() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpWebApiDeleteHandle(s32 libCtxId, s32 handleId) { + LOG_INFO(Lib_NpWebApi, "called libCtxId = {:#x}, handleId = {:#x}", libCtxId, handleId); + return deleteHandle(libCtxId, handleId); } -s32 PS4_SYSV_ABI sceNpWebApiAbortHandle() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpWebApiDeleteRequest(s64 requestId) { + LOG_INFO(Lib_NpWebApi, "called requestId = {:#x}", requestId); + return deleteRequest(requestId); } -s32 PS4_SYSV_ABI sceNpWebApiAbortRequest() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiAddHttpRequestHeader() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiAddMultipartPart() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiCheckTimeout() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiClearAllUnusedConnection() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiClearUnusedConnection() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiCreateContextA() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiCreateExtdPushEventFilter() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiCreateHandle() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiCreateMultipartRequest() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiCreateRequest() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiDeleteContext() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiDeleteExtdPushEventFilter() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiDeleteHandle() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiDeleteRequest() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiGetConnectionStats() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpWebApiGetConnectionStats(s32 userCtxId, const char* pApiGroup, + OrbisNpWebApiConnectionStats* pStats) { + LOG_ERROR(Lib_NpWebApi, + "called (STUBBED) : userCtxId = {:#x}, " + "pApiGroup = '{}', pStats = {}", + userCtxId, (pApiGroup ? pApiGroup : "null"), fmt::ptr(pStats)); return ORBIS_OK; } @@ -158,135 +296,300 @@ s32 PS4_SYSV_ABI sceNpWebApiGetErrorCode() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValue() { +s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValue(s64 requestId, const char* pFieldName, + char* pValue, u64 valueSize) { + LOG_ERROR(Lib_NpWebApi, + "called (STUBBED) : requestId = {:#x}, " + "pFieldName = '{}', pValue = {}, valueSize = {}", + requestId, (pFieldName ? pFieldName : "null"), fmt::ptr(pValue), valueSize); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValueLength(s64 requestId, const char* pFieldName, + u64* pValueLength) { + LOG_ERROR(Lib_NpWebApi, + "called (STUBBED) : requestId = {:#x}, " + "pFieldName = '{}', pValueLength = {}", + requestId, (pFieldName ? pFieldName : "null"), fmt::ptr(pValueLength)); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApiGetHttpStatusCode(s64 requestId, s32* out_status_code) { + LOG_ERROR(Lib_NpWebApi, "called : requestId = {:#x}", requestId); + // On newer SDKs, NULL output pointer is invalid + if (getCompiledSdkVersion() > Common::ElfInfo::FW_10 && out_status_code == nullptr) + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + s32 returncode = getHttpStatusCodeInternal(requestId, out_status_code); + return returncode; +} + +s32 PS4_SYSV_ABI sceNpWebApiGetMemoryPoolStats(s32 libCtxId, + OrbisNpWebApiMemoryPoolStats* pCurrentStat) { + LOG_ERROR(Lib_NpWebApi, "called (STUBBED) : libCtxId = {:#x}, pCurrentStat = {}", libCtxId, + fmt::ptr(pCurrentStat)); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApiInitialize(s32 libHttpCtxId, u64 poolSize) { + LOG_INFO(Lib_NpWebApi, "called libHttpCtxId = {:#x}, poolSize = {:#x} bytes", libHttpCtxId, + poolSize); + if (!g_is_initialized) { + g_is_initialized = true; + s32 result = initializeLibrary(); + if (result < ORBIS_OK) { + return result; + } + } + + s32 result = createLibraryContext(libHttpCtxId, poolSize, nullptr, 0); + if (result >= ORBIS_OK) { + g_active_library_contexts++; + } + return result; +} + +s32 PS4_SYSV_ABI sceNpWebApiInitializeForPresence(s32 libHttpCtxId, u64 poolSize) { + LOG_INFO(Lib_NpWebApi, "called libHttpCtxId = {:#x}, poolSize = {:#x} bytes", libHttpCtxId, + poolSize); + if (!g_is_initialized) { + g_is_initialized = true; + s32 result = initializeLibrary(); + if (result < ORBIS_OK) { + return result; + } + } + + s32 result = createLibraryContext(libHttpCtxId, poolSize, nullptr, 3); + if (result >= ORBIS_OK) { + g_active_library_contexts++; + } + return result; +} + +s32 PS4_SYSV_ABI sceNpWebApiIntCreateCtxIndExtdPushEventFilter( + s32 libCtxId, s32 handleId, const OrbisNpWebApiExtdPushEventFilterParameter* pFilterParam, + u64 filterParamNum) { + if (pFilterParam == nullptr || filterParamNum == 0) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + LOG_INFO(Lib_NpWebApi, "called, libCtxId = {:#x}, handleId = {:#x}", libCtxId, handleId); + return createExtendedPushEventFilter(libCtxId, handleId, nullptr, + ORBIS_NP_INVALID_SERVICE_LABEL, pFilterParam, + filterParamNum, true); +} + +s32 PS4_SYSV_ABI sceNpWebApiIntCreateRequest( + s32 titleUserCtxId, const char* pApiGroup, const char* pPath, OrbisNpWebApiHttpMethod method, + const OrbisNpWebApiContentParameter* pContentParameter, + const OrbisNpWebApiIntCreateRequestExtraArgs* pInternalArgs, s64* pRequestId) { + LOG_INFO(Lib_NpWebApi, "called"); + if (pApiGroup == nullptr || pPath == nullptr || + method > OrbisNpWebApiHttpMethod::ORBIS_NP_WEBAPI_HTTP_METHOD_PATCH) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + if (pContentParameter != nullptr && pContentParameter->contentLength != 0 && + pContentParameter->pContentType == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_CONTENT_PARAMETER; + } + + LOG_INFO(Lib_NpWebApi, + "called titleUserCtxId = {:#x}, pApiGroup = '{}', pPath = '{}', method = {}", + titleUserCtxId, pApiGroup, pPath, magic_enum::enum_name(method)); + + return createRequest(titleUserCtxId, pApiGroup, pPath, method, pContentParameter, pInternalArgs, + pRequestId, false); +} + +s32 PS4_SYSV_ABI sceNpWebApiIntCreateServicePushEventFilter( + s32 libCtxId, s32 handleId, const char* pNpServiceName, OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiServicePushEventFilterParameter* pFilterParam, u64 filterParamNum) { + if (pFilterParam == nullptr || filterParamNum == 0) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_WARNING(Lib_NpWebApi, + "called, libCtxId = {:#x}, handleId = {:#x}, pNpServiceName = '{}', " + "npServiceLabel = {:#x}", + libCtxId, handleId, (pNpServiceName ? pNpServiceName : "null"), npServiceLabel); + return createServicePushEventFilter(libCtxId, handleId, pNpServiceName, npServiceLabel, + pFilterParam, filterParamNum); +} + +s32 PS4_SYSV_ABI sceNpWebApiIntInitialize(const OrbisNpWebApiIntInitializeArgs* args) { + LOG_INFO(Lib_NpWebApi, "called"); + if (args == nullptr || args->structSize != sizeof(OrbisNpWebApiIntInitializeArgs)) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + if (!g_is_initialized) { + g_is_initialized = true; + s32 result = initializeLibrary(); + if (result < ORBIS_OK) { + return result; + } + } + + s32 result = createLibraryContext(args->libHttpCtxId, args->poolSize, args->name, 2); + if (result >= ORBIS_OK) { + g_active_library_contexts++; + } + return result; +} + +s32 PS4_SYSV_ABI sceNpWebApiIntRegisterServicePushEventCallback( + s32 titleUserCtxId, s32 filterId, OrbisNpWebApiInternalServicePushEventCallback cbFunc, + void* pUserArg) { + if (cbFunc == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, cbFunc = {}", titleUserCtxId, + fmt::ptr(cbFunc)); + return registerServicePushEventCallback(titleUserCtxId, filterId, nullptr, cbFunc, nullptr, + pUserArg); +} + +s32 PS4_SYSV_ABI sceNpWebApiIntRegisterServicePushEventCallbackA( + s32 titleUserCtxId, s32 filterId, OrbisNpWebApiInternalServicePushEventCallbackA cbFunc, + void* pUserArg) { + if (cbFunc == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, cbFunc = {}", titleUserCtxId, + fmt::ptr(cbFunc)); + return registerServicePushEventCallback(titleUserCtxId, filterId, nullptr, nullptr, cbFunc, + pUserArg); +} + +s32 PS4_SYSV_ABI sceNpWebApiReadData(s64 requestId, void* pData, u64 size) { + LOG_ERROR(Lib_NpWebApi, "called : requestId = {:#x}, pData = {}, size = {:#x}", requestId, + fmt::ptr(pData), size); + if (pData == nullptr || size == 0) + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + + return readDataInternal(requestId, pData, size); +} + +s32 PS4_SYSV_ABI sceNpWebApiRegisterExtdPushEventCallbackA( + s32 titleUserCtxId, s32 filterId, OrbisNpWebApiExtdPushEventCallbackA cbFunc, void* pUserArg) { + if (cbFunc == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, cbFunc = {}", titleUserCtxId, + fmt::ptr(cbFunc)); + return registerExtdPushEventCallbackA(titleUserCtxId, filterId, cbFunc, pUserArg); +} + +s32 PS4_SYSV_ABI sceNpWebApiSendMultipartRequest(s64 requestId, s32 partIndex, const void* pData, + u64 dataSize) { + if (partIndex <= 0 || pData == nullptr || dataSize == 0) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + LOG_INFO(Lib_NpWebApi, + "called, requestId = {:#x}, " + "partIndex = {:#x}, pData = {}, dataSize = {:#x}", + requestId, partIndex, fmt::ptr(pData), dataSize); + return sendRequest(requestId, partIndex, pData, dataSize, 0, nullptr); +} + +s32 PS4_SYSV_ABI +sceNpWebApiSendMultipartRequest2(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, + OrbisNpWebApiResponseInformationOption* pRespInfoOption) { + if (partIndex <= 0 || pData == nullptr || dataSize == 0) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + LOG_INFO(Lib_NpWebApi, + "called, requestId = {:#x}, " + "partIndex = {:#x}, pData = {}, dataSize = {:#x}, pRespInfoOption = {}", + requestId, partIndex, fmt::ptr(pData), dataSize, fmt::ptr(pRespInfoOption)); + return sendRequest(requestId, partIndex, pData, dataSize, 1, pRespInfoOption); +} + +s32 PS4_SYSV_ABI sceNpWebApiSendRequest(s64 requestId, const void* pData, u64 dataSize) { + LOG_INFO(Lib_NpWebApi, "called, requestId = {:#x}, pData = {}, dataSize = {:#x}", requestId, + fmt::ptr(pData), dataSize); + return sendRequest(requestId, 0, pData, dataSize, 0, nullptr); +} + +s32 PS4_SYSV_ABI sceNpWebApiSendRequest2(s64 requestId, const void* pData, u64 dataSize, + OrbisNpWebApiResponseInformationOption* pRespInfoOption) { + LOG_INFO(Lib_NpWebApi, + "called, requestId = {:#x}, " + "pData = {}, dataSize = {:#x}, pRespInfoOption = {}", + requestId, fmt::ptr(pData), dataSize, fmt::ptr(pRespInfoOption)); + return sendRequest(requestId, 0, pData, dataSize, 1, pRespInfoOption); +} + +s32 PS4_SYSV_ABI sceNpWebApiSetHandleTimeout(s32 libCtxId, s32 handleId, u32 timeout) { + LOG_INFO(Lib_NpWebApi, "called, libCtxId = {:#x}, handleId = {:#x}, timeout = {} ms", libCtxId, + handleId, timeout); + return setHandleTimeout(libCtxId, handleId, timeout); +} + +s32 PS4_SYSV_ABI sceNpWebApiSetMaxConnection(s32 libCtxId, s32 maxConnection) { + LOG_ERROR(Lib_NpWebApi, "called (STUBBED) : libCtxId = {:#x}, maxConnection = {}", libCtxId, + maxConnection); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApiSetMultipartContentType(s64 requestId, const char* pTypeName, + const char* pBoundary) { + LOG_ERROR(Lib_NpWebApi, + "called (STUBBED) : requestId = {:#x}, " + "pTypeName = '{}', pBoundary = '{}'", + requestId, (pTypeName ? pTypeName : "null"), (pBoundary ? pBoundary : "null")); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApiSetRequestTimeout(s64 requestId, u32 timeout) { + LOG_INFO(Lib_NpWebApi, "called requestId = {:#x}, timeout = {} ms", requestId, timeout); + return setRequestTimeout(requestId, timeout); +} + +s32 PS4_SYSV_ABI sceNpWebApiTerminate(s32 libCtxId) { + LOG_INFO(Lib_NpWebApi, "called libCtxId = {:#x}", libCtxId); + s32 result = terminateContext(libCtxId); + if (result != ORBIS_OK) { + return result; + } + + g_active_library_contexts--; + if (g_active_library_contexts == 0) { + g_is_initialized = false; + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApiUnregisterExtdPushEventCallback(s32 titleUserCtxId, s32 callbackId) { + LOG_INFO(Lib_NpWebApi, "called, titleUserCtxId = {:#x}, callbackId = {:#x}", titleUserCtxId, + callbackId); + return unregisterExtdPushEventCallback(titleUserCtxId, callbackId); +} + +s32 PS4_SYSV_ABI sceNpWebApiUtilityParseNpId(const char* pJsonNpId, + Libraries::Np::OrbisNpId* pNpId) { LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValueLength() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} +s32 PS4_SYSV_ABI sceNpWebApiVshInitialize(s32 libHttpCtxId, u64 poolSize) { + LOG_INFO(Lib_NpWebApi, "called libHttpCtxId = {:#x}, poolSize = {:#x} bytes", libHttpCtxId, + poolSize); + if (!g_is_initialized) { + g_is_initialized = true; + s32 result = initializeLibrary(); + if (result < ORBIS_OK) { + return result; + } + } -s32 PS4_SYSV_ABI sceNpWebApiGetHttpStatusCode() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiGetMemoryPoolStats() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiInitialize() { - LOG_ERROR(Lib_NpWebApi, "(DUMMY) called"); - static s32 id = 0; - return ++id; -} - -s32 PS4_SYSV_ABI sceNpWebApiInitializeForPresence() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiIntCreateCtxIndExtdPushEventFilter() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiIntCreateRequest() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiIntCreateServicePushEventFilter() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiIntInitialize() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiIntRegisterServicePushEventCallback() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiIntRegisterServicePushEventCallbackA() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiReadData() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiRegisterExtdPushEventCallbackA() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiSendMultipartRequest() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiSendMultipartRequest2() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiSendRequest() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiSendRequest2() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiSetHandleTimeout() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiSetMaxConnection() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiSetMultipartContentType() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiSetRequestTimeout() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiTerminate() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiUnregisterExtdPushEventCallback() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiUtilityParseNpId() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpWebApiVshInitialize() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; + s32 result = createLibraryContext(libHttpCtxId, poolSize, nullptr, 4); + if (result >= ORBIS_OK) { + g_active_library_contexts++; + } + return result; } s32 PS4_SYSV_ABI Func_064C4ED1EDBEB9E8() { diff --git a/src/core/libraries/np/np_web_api.h b/src/core/libraries/np/np_web_api.h index 6679662cb..8dd9441e0 100644 --- a/src/core/libraries/np/np_web_api.h +++ b/src/core/libraries/np/np_web_api.h @@ -1,9 +1,12 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "common/types.h" +#include "core/libraries/np/np_common.h" +#include "core/libraries/np/np_types.h" +#include "core/libraries/system/userservice.h" namespace Core::Loader { class SymbolsResolver; @@ -11,106 +14,115 @@ class SymbolsResolver; namespace Libraries::Np::NpWebApi { -s32 PS4_SYSV_ABI sceNpWebApiCreateContext(); -s32 PS4_SYSV_ABI sceNpWebApiCreatePushEventFilter(); -s32 PS4_SYSV_ABI sceNpWebApiCreateServicePushEventFilter(); -s32 PS4_SYSV_ABI sceNpWebApiDeletePushEventFilter(); -s32 PS4_SYSV_ABI sceNpWebApiDeleteServicePushEventFilter(); -s32 PS4_SYSV_ABI sceNpWebApiRegisterExtdPushEventCallback(); -s32 PS4_SYSV_ABI sceNpWebApiRegisterNotificationCallback(); -s32 PS4_SYSV_ABI sceNpWebApiRegisterPushEventCallback(); -s32 PS4_SYSV_ABI sceNpWebApiRegisterServicePushEventCallback(); -s32 PS4_SYSV_ABI sceNpWebApiUnregisterNotificationCallback(); -s32 PS4_SYSV_ABI sceNpWebApiUnregisterPushEventCallback(); -s32 PS4_SYSV_ABI sceNpWebApiUnregisterServicePushEventCallback(); -s32 PS4_SYSV_ABI sceNpWebApiAbortHandle(); -s32 PS4_SYSV_ABI sceNpWebApiAbortRequest(); -s32 PS4_SYSV_ABI sceNpWebApiAddHttpRequestHeader(); -s32 PS4_SYSV_ABI sceNpWebApiAddMultipartPart(); -s32 PS4_SYSV_ABI sceNpWebApiCheckTimeout(); -s32 PS4_SYSV_ABI sceNpWebApiClearAllUnusedConnection(); -s32 PS4_SYSV_ABI sceNpWebApiClearUnusedConnection(); -s32 PS4_SYSV_ABI sceNpWebApiCreateContextA(); -s32 PS4_SYSV_ABI sceNpWebApiCreateExtdPushEventFilter(); -s32 PS4_SYSV_ABI sceNpWebApiCreateHandle(); -s32 PS4_SYSV_ABI sceNpWebApiCreateMultipartRequest(); -s32 PS4_SYSV_ABI sceNpWebApiCreateRequest(); -s32 PS4_SYSV_ABI sceNpWebApiDeleteContext(); -s32 PS4_SYSV_ABI sceNpWebApiDeleteExtdPushEventFilter(); -s32 PS4_SYSV_ABI sceNpWebApiDeleteHandle(); -s32 PS4_SYSV_ABI sceNpWebApiDeleteRequest(); -s32 PS4_SYSV_ABI sceNpWebApiGetConnectionStats(); -s32 PS4_SYSV_ABI sceNpWebApiGetErrorCode(); -s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValue(); -s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValueLength(); -s32 PS4_SYSV_ABI sceNpWebApiGetHttpStatusCode(); -s32 PS4_SYSV_ABI sceNpWebApiGetMemoryPoolStats(); -s32 PS4_SYSV_ABI sceNpWebApiInitialize(); -s32 PS4_SYSV_ABI sceNpWebApiInitializeForPresence(); -s32 PS4_SYSV_ABI sceNpWebApiIntCreateCtxIndExtdPushEventFilter(); -s32 PS4_SYSV_ABI sceNpWebApiIntCreateRequest(); -s32 PS4_SYSV_ABI sceNpWebApiIntCreateServicePushEventFilter(); -s32 PS4_SYSV_ABI sceNpWebApiIntInitialize(); -s32 PS4_SYSV_ABI sceNpWebApiIntRegisterServicePushEventCallback(); -s32 PS4_SYSV_ABI sceNpWebApiIntRegisterServicePushEventCallbackA(); -s32 PS4_SYSV_ABI sceNpWebApiReadData(); -s32 PS4_SYSV_ABI sceNpWebApiRegisterExtdPushEventCallbackA(); -s32 PS4_SYSV_ABI sceNpWebApiSendMultipartRequest(); -s32 PS4_SYSV_ABI sceNpWebApiSendMultipartRequest2(); -s32 PS4_SYSV_ABI sceNpWebApiSendRequest(); -s32 PS4_SYSV_ABI sceNpWebApiSendRequest2(); -s32 PS4_SYSV_ABI sceNpWebApiSetHandleTimeout(); -s32 PS4_SYSV_ABI sceNpWebApiSetMaxConnection(); -s32 PS4_SYSV_ABI sceNpWebApiSetMultipartContentType(); -s32 PS4_SYSV_ABI sceNpWebApiSetRequestTimeout(); -s32 PS4_SYSV_ABI sceNpWebApiTerminate(); -s32 PS4_SYSV_ABI sceNpWebApiUnregisterExtdPushEventCallback(); -s32 PS4_SYSV_ABI sceNpWebApiUtilityParseNpId(); -s32 PS4_SYSV_ABI sceNpWebApiVshInitialize(); -s32 PS4_SYSV_ABI Func_064C4ED1EDBEB9E8(); -s32 PS4_SYSV_ABI Func_0783955D4E9563DA(); -s32 PS4_SYSV_ABI Func_1A6D77F3FD8323A8(); -s32 PS4_SYSV_ABI Func_1E0693A26FE0F954(); -s32 PS4_SYSV_ABI Func_24A9B5F1D77000CF(); -s32 PS4_SYSV_ABI Func_24AAA6F50E4C2361(); -s32 PS4_SYSV_ABI Func_24D8853D6B47FC79(); -s32 PS4_SYSV_ABI Func_279B3E9C7C4A9DC5(); -s32 PS4_SYSV_ABI Func_28461E29E9F8D697(); -s32 PS4_SYSV_ABI Func_3C29624704FAB9E0(); -s32 PS4_SYSV_ABI Func_3F027804ED2EC11E(); -s32 PS4_SYSV_ABI Func_4066C94E782997CD(); -s32 PS4_SYSV_ABI Func_47C85356815DBE90(); -s32 PS4_SYSV_ABI Func_4FCE8065437E3B87(); -s32 PS4_SYSV_ABI Func_536280BE3DABB521(); -s32 PS4_SYSV_ABI Func_57A0E1BC724219F3(); -s32 PS4_SYSV_ABI Func_5819749C040B6637(); -s32 PS4_SYSV_ABI Func_6198D0C825E86319(); -s32 PS4_SYSV_ABI Func_61F2B9E8AB093743(); -s32 PS4_SYSV_ABI Func_6BC388E6113F0D44(); -s32 PS4_SYSV_ABI Func_7500F0C4F8DC2D16(); -s32 PS4_SYSV_ABI Func_75A03814C7E9039F(); -s32 PS4_SYSV_ABI Func_789D6026C521416E(); -s32 PS4_SYSV_ABI Func_7DED63D06399EFFF(); -s32 PS4_SYSV_ABI Func_7E55A2DCC03D395A(); -s32 PS4_SYSV_ABI Func_7E6C8F9FB86967F4(); -s32 PS4_SYSV_ABI Func_7F04B7D4A7D41E80(); -s32 PS4_SYSV_ABI Func_8E167252DFA5C957(); -s32 PS4_SYSV_ABI Func_95D0046E504E3B09(); -s32 PS4_SYSV_ABI Func_97284BFDA4F18FDF(); -s32 PS4_SYSV_ABI Func_99E32C1F4737EAB4(); -s32 PS4_SYSV_ABI Func_9CFF661EA0BCBF83(); -s32 PS4_SYSV_ABI Func_9EB0E1F467AC3B29(); -s32 PS4_SYSV_ABI Func_A2318FE6FBABFAA3(); -s32 PS4_SYSV_ABI Func_BA07A2E1BF7B3971(); -s32 PS4_SYSV_ABI Func_BD0803EEE0CC29A0(); -s32 PS4_SYSV_ABI Func_BE6F4E5524BB135F(); -s32 PS4_SYSV_ABI Func_C0D490EB481EA4D0(); -s32 PS4_SYSV_ABI Func_C175D392CA6D084A(); -s32 PS4_SYSV_ABI Func_CD0136AF165D2F2F(); -s32 PS4_SYSV_ABI Func_D1C0ADB7B52FEAB5(); -s32 PS4_SYSV_ABI Func_E324765D18EE4D12(); -s32 PS4_SYSV_ABI Func_E789F980D907B653(); -s32 PS4_SYSV_ABI Func_F9A32E8685627436(); +#define ORBIS_NP_WEBAPI_DEFAULT_CONNECTION_NUM 1 +#define ORBIS_NP_WEBAPI_MAX_CONNECTION_NUM 16 +#define ORBIS_NP_WEBAPI_PUSH_EVENT_DATA_TYPE_LEN_MAX 64 +#define ORBIS_NP_WEBAPI_EXTD_PUSH_EVENT_EXTD_DATA_KEY_LEN_MAX 32 + +struct OrbisNpWebApiPushEventDataType { + char val[ORBIS_NP_WEBAPI_PUSH_EVENT_DATA_TYPE_LEN_MAX + 1]; +}; + +struct OrbisNpWebApiExtdPushEventExtdDataKey { + char val[ORBIS_NP_WEBAPI_EXTD_PUSH_EVENT_EXTD_DATA_KEY_LEN_MAX + 1]; +}; + +struct OrbisNpWebApiPushEventFilterParameter { + OrbisNpWebApiPushEventDataType dataType; +}; + +struct OrbisNpWebApiServicePushEventFilterParameter { + OrbisNpWebApiPushEventDataType dataType; +}; + +struct OrbisNpWebApiExtdPushEventFilterParameter { + OrbisNpWebApiPushEventDataType dataType; + OrbisNpWebApiExtdPushEventExtdDataKey* pExtdDataKey; + u64 extdDataKeyNum; +}; + +struct OrbisNpWebApiExtdPushEventExtdData { + OrbisNpWebApiExtdPushEventExtdDataKey extdDataKey; + char* pData; + u64 dataLen; +}; + +struct OrbisNpWebApiHttpHeader { + char* pName; + char* pValue; +}; + +struct OrbisNpWebApiMultipartPartParameter { + OrbisNpWebApiHttpHeader* pHeaders; + u64 headerNum; + u64 contentLength; +}; + +enum OrbisNpWebApiHttpMethod : s32 { + ORBIS_NP_WEBAPI_HTTP_METHOD_GET, + ORBIS_NP_WEBAPI_HTTP_METHOD_POST, + ORBIS_NP_WEBAPI_HTTP_METHOD_PUT, + ORBIS_NP_WEBAPI_HTTP_METHOD_DELETE, + ORBIS_NP_WEBAPI_HTTP_METHOD_PATCH +}; + +struct OrbisNpWebApiContentParameter { + u64 contentLength; + const char* pContentType; + u8 reserved[16]; +}; + +struct OrbisNpWebApiResponseInformationOption { + s32 httpStatus; + char* pErrorObject; + u64 errorObjectSize; + u64 responseDataSize; +}; + +struct OrbisNpWebApiMemoryPoolStats { + u64 poolSize; + u64 maxInuseSize; + u64 currentInuseSize; + s32 reserved; +}; + +struct OrbisNpWebApiConnectionStats { + u32 max; + u32 used; + u32 unused; + u32 keepAlive; + u64 reserved; +}; + +struct OrbisNpWebApiIntInitializeArgs { + u32 libHttpCtxId; + u8 reserved[4]; + u64 poolSize; + const char* name; + u64 structSize; +}; + +struct OrbisNpWebApiIntCreateRequestExtraArgs { + void* unk_0; + void* unk_1; + void* unk_2; +}; + +using OrbisNpWebApiPushEventCallback = PS4_SYSV_ABI void (*)(); // dummy + +using OrbisNpWebApiExtdPushEventCallback = PS4_SYSV_ABI void (*)(); // dummy +using OrbisNpWebApiExtdPushEventCallbackA = PS4_SYSV_ABI void (*)( + s32 userCtxId, s32 callbackId, const char* pNpServiceName, OrbisNpServiceLabel npServiceLabel, + const OrbisNpPeerAddressA* pTo, const OrbisNpOnlineId* pToOnlineId, + const OrbisNpPeerAddressA* pFrom, const OrbisNpOnlineId* pFromOnlineId, + const OrbisNpWebApiPushEventDataType* pDataType, const char* pData, u64 dataLen, + const OrbisNpWebApiExtdPushEventExtdData* pExtdData, u64 extdDataNum, void* pUserArg); + +using OrbisNpWebApiServicePushEventCallback = PS4_SYSV_ABI void (*)(); // dummy +using OrbisNpWebApiInternalServicePushEventCallback = PS4_SYSV_ABI void (*)(); // dummy +using OrbisNpWebApiInternalServicePushEventCallbackA = PS4_SYSV_ABI void (*)(); // dummy + +using OrbisNpWebApiNotificationCallback = PS4_SYSV_ABI void (*)(); // dummy void RegisterLib(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::Np::NpWebApi \ No newline at end of file diff --git a/src/core/libraries/np/np_web_api_error.h b/src/core/libraries/np/np_web_api_error.h new file mode 100644 index 000000000..c7f08224f --- /dev/null +++ b/src/core/libraries/np/np_web_api_error.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +constexpr int ORBIS_NP_WEBAPI_ERROR_OUT_OF_MEMORY = 0x80552901; +constexpr int ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT = 0x80552902; +constexpr int ORBIS_NP_WEBAPI_ERROR_INVALID_LIB_CONTEXT_ID = 0x80552903; +constexpr int ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND = 0x80552904; +constexpr int ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND = 0x80552905; +constexpr int ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND = 0x80552906; +constexpr int ORBIS_NP_WEBAPI_ERROR_NOT_SIGNED_IN = 0x80552907; +constexpr int ORBIS_NP_WEBAPI_ERROR_INVALID_CONTENT_PARAMETER = 0x80552908; +constexpr int ORBIS_NP_WEBAPI_ERROR_ABORTED = 0x80552909; +constexpr int ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_ALREADY_EXIST = 0x8055290a; +constexpr int ORBIS_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND = 0x8055290b; +constexpr int ORBIS_NP_WEBAPI_ERROR_PUSH_EVENT_CALLBACK_NOT_FOUND = 0x8055290c; +constexpr int ORBIS_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND = 0x8055290d; +constexpr int ORBIS_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND = 0x8055290e; +constexpr int ORBIS_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_CALLBACK_NOT_FOUND = 0x8055290f; +constexpr int ORBIS_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND = 0x80552910; +constexpr int ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_BUSY = 0x80552911; +constexpr int ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_BUSY = 0x80552912; +constexpr int ORBIS_NP_WEBAPI_ERROR_REQUEST_BUSY = 0x80552913; +constexpr int ORBIS_NP_WEBAPI_ERROR_INVALID_HTTP_STATUS_CODE = 0x80552914; +constexpr int ORBIS_NP_WEBAPI_ERROR_PROHIBITED_HTTP_HEADER = 0x80552915; +constexpr int ORBIS_NP_WEBAPI_ERROR_PROHIBITED_FUNCTION_CALL = 0x80552916; +constexpr int ORBIS_NP_WEBAPI_ERROR_MULTIPART_PART_NOT_FOUND = 0x80552917; +constexpr int ORBIS_NP_WEBAPI_ERROR_PARAMETER_TOO_LONG = 0x80552918; +constexpr int ORBIS_NP_WEBAPI_ERROR_HANDLE_BUSY = 0x80552919; +constexpr int ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX = 0x8055291a; +constexpr int ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_MAX = 0x8055291b; +constexpr int ORBIS_NP_WEBAPI_ERROR_EXTD_PUSH_EVENT_FILTER_NOT_FOUND = 0x8055291c; +constexpr int ORBIS_NP_WEBAPI_ERROR_EXTD_PUSH_EVENT_CALLBACK_NOT_FOUND = 0x8055291d; +constexpr int ORBIS_NP_WEBAPI_ERROR_AFTER_SEND = 0x8055291e; +constexpr int ORBIS_NP_WEBAPI_ERROR_TIMEOUT = 0x8055291f; diff --git a/src/core/libraries/np/np_web_api_internal.cpp b/src/core/libraries/np/np_web_api_internal.cpp new file mode 100644 index 000000000..3c7557b72 --- /dev/null +++ b/src/core/libraries/np/np_web_api_internal.cpp @@ -0,0 +1,1534 @@ +// 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/libraries/kernel/process.h" +#include "core/libraries/kernel/time.h" +#include "core/libraries/network/http.h" +#include "np_web_api_internal.h" + +#include + +namespace Libraries::Np::NpWebApi { + +static std::mutex g_global_mutex; +static std::map g_contexts; +static s32 g_library_context_count = 0; +static s32 g_user_context_count = 0; +static s32 g_handle_count = 0; +static s32 g_push_event_filter_count = 0; +static s32 g_service_push_event_filter_count = 0; +static s32 g_extended_push_event_filter_count = 0; +static s32 g_registered_callback_count = 0; +static s64 g_request_count = 0; +static u64 g_last_timeout_check = 0; +static s32 g_sdk_ver = 0; + +s32 initializeLibrary() { + return Kernel::sceKernelGetCompiledSdkVersion(&g_sdk_ver); +} + +s32 getCompiledSdkVersion() { + return g_sdk_ver; +} + +s32 createLibraryContext(s32 libHttpCtxId, u64 poolSize, const char* name, s32 type) { + std::scoped_lock lk{g_global_mutex}; + + g_library_context_count++; + if (g_library_context_count >= 0x8000) { + g_library_context_count = 1; + } + s32 ctx_id = g_library_context_count; + while (g_contexts.contains(ctx_id)) { + ctx_id--; + } + if (ctx_id <= 0) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX; + } + + // Create new context + g_contexts[ctx_id] = new OrbisNpWebApiContext{}; + auto& new_context = g_contexts.at(ctx_id); + new_context->libCtxId = ctx_id; + new_context->libHttpCtxId = libHttpCtxId; + new_context->type = type; + new_context->userCount = 0; + new_context->terminated = false; + if (name != nullptr) { + new_context->name = std::string(name); + } + + return ctx_id; +} + +OrbisNpWebApiContext* findAndValidateContext(s32 libCtxId, s32 flag) { + std::scoped_lock lk{g_global_mutex}; + if (libCtxId < 1 || libCtxId >= 0x8000) { + return nullptr; + } + auto& context = g_contexts[libCtxId]; + std::scoped_lock lk2{context->contextLock}; + if (flag == 0 && context->terminated) { + return nullptr; + } + context->userCount++; + return context; +} + +void releaseContext(OrbisNpWebApiContext* context) { + std::scoped_lock lk{context->contextLock}; + context->userCount--; +} + +bool isContextTerminated(OrbisNpWebApiContext* context) { + std::scoped_lock lk{context->contextLock}; + return context->terminated; +} + +bool isContextBusy(OrbisNpWebApiContext* context) { + std::scoped_lock lk{context->contextLock}; + return context->userCount > 1; +} + +bool areContextHandlesBusy(OrbisNpWebApiContext* context) { + std::scoped_lock lk{context->contextLock}; + bool is_busy = false; + for (auto& handle : context->handles) { + if (handle.second->userCount > 0) { + return true; + } + } + return false; +} + +void lockContext(OrbisNpWebApiContext* context) { + context->contextLock.lock(); +} + +void unlockContext(OrbisNpWebApiContext* context) { + context->contextLock.unlock(); +} + +void markContextAsTerminated(OrbisNpWebApiContext* context) { + std::scoped_lock lk{context->contextLock}; + context->terminated = true; +} + +void checkContextTimeout(OrbisNpWebApiContext* context) { + u64 time = Kernel::sceKernelGetProcessTime(); + std::scoped_lock lk{context->contextLock}; + + for (auto& user_context : context->userContexts) { + checkUserContextTimeout(user_context.second); + } + + for (auto& value : context->timerHandles) { + auto& timer_handle = value.second; + if (!timer_handle->timedOut && timer_handle->handleTimeout != 0 && + timer_handle->handleEndTime < time) { + timer_handle->timedOut = true; + abortHandle(context->libCtxId, timer_handle->handleId); + } + } +} + +void checkTimeout() { + u64 time = Kernel::sceKernelGetProcessTime(); + if (time < g_last_timeout_check + 1000) { + return; + } + g_last_timeout_check = time; + std::scoped_lock lk{g_global_mutex}; + + for (auto& context : g_contexts) { + checkContextTimeout(context.second); + } +} + +s32 deleteContext(s32 libCtxId) { + std::scoped_lock lk{g_global_mutex}; + if (!g_contexts.contains(libCtxId)) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + auto& context = g_contexts[libCtxId]; + context->handles.clear(); + context->timerHandles.clear(); + context->pushEventFilters.clear(); + context->servicePushEventFilters.clear(); + context->extendedPushEventFilters.clear(); + + g_contexts.erase(libCtxId); + return ORBIS_OK; +} + +s32 terminateContext(s32 libCtxId) { + OrbisNpWebApiContext* ctx = findAndValidateContext(libCtxId); + if (ctx == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + if (g_sdk_ver < Common::ElfInfo::FW_40 && isContextBusy(ctx)) { + releaseContext(ctx); + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_BUSY; + } + + std::vector user_context_ids; + for (auto& user_context : ctx->userContexts) { + user_context_ids.emplace_back(user_context.first); + } + for (s32 user_context_id : user_context_ids) { + s32 result = deleteUserContext(user_context_id); + if (result != ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND || + g_sdk_ver < Common::ElfInfo::FW_40) { + return result; + } + } + + lockContext(ctx); + if (g_sdk_ver >= Common::ElfInfo::FW_40) { + for (auto& handle : ctx->handles) { + abortHandle(libCtxId, handle.first); + } + if (isContextTerminated(ctx)) { + unlockContext(ctx); + releaseContext(ctx); + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + markContextAsTerminated(ctx); + while (isContextBusy(ctx) || areContextHandlesBusy(ctx)) { + unlockContext(ctx); + Kernel::sceKernelUsleep(50000); + lockContext(ctx); + } + } + + unlockContext(ctx); + releaseContext(ctx); + return deleteContext(libCtxId); +} + +OrbisNpWebApiUserContext* findUserContextByUserId( + OrbisNpWebApiContext* context, Libraries::UserService::OrbisUserServiceUserId userId) { + if (userId == Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { + return nullptr; + } + + std::scoped_lock lk{context->contextLock}; + for (auto& user_context : context->userContexts) { + if (user_context.second->userId == userId) { + user_context.second->userCount++; + return user_context.second; + } + } + return nullptr; +} + +OrbisNpWebApiUserContext* findUserContext(OrbisNpWebApiContext* context, s32 titleUserCtxId) { + std::scoped_lock lk{context->contextLock}; + if (!context->userContexts.contains(titleUserCtxId)) { + return nullptr; + } + OrbisNpWebApiUserContext* user_context = context->userContexts[titleUserCtxId]; + if (user_context->deleted) { + return nullptr; + } + user_context->userCount++; + return user_context; +} + +s32 createUserContextWithOnlineId(s32 libCtxId, OrbisNpOnlineId* onlineId) { + LOG_WARNING(Lib_NpWebApi, "called libCtxId = {}", libCtxId); + + Libraries::UserService::OrbisUserServiceUserId user_id = 0; + Libraries::UserService::sceUserServiceGetInitialUser(&user_id); + return createUserContext(libCtxId, user_id); +} + +s32 createUserContext(s32 libCtxId, Libraries::UserService::OrbisUserServiceUserId userId) { + LOG_INFO(Lib_NpWebApi, "libCtxId = {}, userId = {}", libCtxId, userId); + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContextByUserId(context, userId); + if (user_context != nullptr) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_ALREADY_EXIST; + } + + std::scoped_lock lk{context->contextLock}; + + // Create new user context + g_user_context_count++; + if (g_user_context_count >= 0x10000) { + g_user_context_count = 1; + } + s32 user_ctx_id = (libCtxId << 0x10) | g_user_context_count; + while (context->userContexts.contains(user_ctx_id)) { + user_ctx_id--; + } + if (user_ctx_id <= (libCtxId << 0x10)) { + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_MAX; + } + + context->userContexts[user_ctx_id] = new OrbisNpWebApiUserContext{}; + user_context = context->userContexts.at(user_ctx_id); + user_context->userCount = 0; + user_context->parentContext = context; + user_context->userId = userId; + user_context->userCtxId = user_ctx_id; + user_context->deleted = false; + + // TODO: Internal structs related to libSceHttp use are initialized here. + releaseContext(context); + return user_ctx_id; +} + +s32 registerNotificationCallback(s32 titleUserCtxId, OrbisNpWebApiNotificationCallback cbFunc, + void* pUserArg) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + lockContext(context); + user_context->notificationCallbackFunction = cbFunc; + user_context->pNotificationCallbackUserArgs = pUserArg; + unlockContext(context); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +s32 unregisterNotificationCallback(s32 titleUserCtxId) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + lockContext(context); + user_context->notificationCallbackFunction = nullptr; + user_context->pNotificationCallbackUserArgs = nullptr; + unlockContext(context); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +bool isUserContextBusy(OrbisNpWebApiUserContext* userContext) { + std::scoped_lock lk{userContext->parentContext->contextLock}; + return userContext->userCount > 1; +} + +bool areUserContextRequestsBusy(OrbisNpWebApiUserContext* userContext) { + std::scoped_lock lk{userContext->parentContext->contextLock}; + bool is_busy = false; + for (auto& request : userContext->requests) { + request.second->userCount++; + bool req_busy = isRequestBusy(request.second); + request.second->userCount--; + if (req_busy) { + return true; + } + } + return false; +} + +void releaseUserContext(OrbisNpWebApiUserContext* userContext) { + std::scoped_lock lk{userContext->parentContext->contextLock}; + userContext->userCount--; +} + +void checkUserContextTimeout(OrbisNpWebApiUserContext* userContext) { + std::scoped_lock lk{userContext->parentContext->contextLock}; + for (auto& request : userContext->requests) { + checkRequestTimeout(request.second); + } +} + +s32 deleteUserContext(s32 titleUserCtxId) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + lockContext(context); + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + unlockContext(context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + if (g_sdk_ver < Common::ElfInfo::FW_40) { + if (isUserContextBusy(user_context)) { + releaseUserContext(user_context); + unlockContext(context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_BUSY; + } + + if (areUserContextRequestsBusy(user_context)) { + releaseUserContext(user_context); + unlockContext(context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_BUSY; + } + } else { + for (auto& request : user_context->requests) { + abortRequestInternal(context, user_context, request.second); + } + + if (user_context->deleted) { + releaseUserContext(user_context); + unlockContext(context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + user_context->deleted = true; + while (isUserContextBusy(user_context) || areUserContextRequestsBusy(user_context)) { + unlockContext(context); + Kernel::sceKernelUsleep(50000); + lockContext(context); + } + } + + user_context->extendedPushEventCallbacks.clear(); + user_context->servicePushEventCallbacks.clear(); + user_context->pushEventCallbacks.clear(); + user_context->requests.clear(); + context->userContexts.erase(titleUserCtxId); + + unlockContext(context); + releaseContext(context); + return ORBIS_OK; +} + +s32 createRequest(s32 titleUserCtxId, const char* pApiGroup, const char* pPath, + OrbisNpWebApiHttpMethod method, + const OrbisNpWebApiContentParameter* pContentParameter, + const OrbisNpWebApiIntCreateRequestExtraArgs* pInternalArgs, s64* pRequestId, + bool isMultipart) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + lockContext(user_context->parentContext); + if (g_sdk_ver >= Common::ElfInfo::FW_40 && user_context->deleted) { + unlockContext(user_context->parentContext); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + g_request_count++; + if (g_request_count >> 0x20 != 0) { + g_request_count = 1; + } + + s64 user_ctx_id = static_cast(titleUserCtxId); + s32 request_id = (user_ctx_id << 0x20) | g_request_count; + while (user_context->requests.contains(request_id)) { + request_id--; + } + // Real library would hang if this assert fails. + ASSERT_MSG(request_id > user_ctx_id << 0x20, "Too many requests!"); + user_context->requests[request_id] = new OrbisNpWebApiRequest{}; + + auto& request = user_context->requests[request_id]; + request->parentContext = context; + request->userCount = 0; + request->requestId = request_id; + request->userMethod = method; + request->multipart = isMultipart; + request->aborted = false; + + if (pApiGroup != nullptr) { + request->userApiGroup = std::string(pApiGroup); + } + + if (pPath != nullptr) { + request->userPath = std::string(pPath); + } + + if (pContentParameter != nullptr) { + request->userContentLength = pContentParameter->contentLength; + if (pContentParameter->pContentType != nullptr) { + request->userContentType = std::string(pContentParameter->pContentType); + } + } + + if (pInternalArgs != nullptr) { + ASSERT_MSG(pInternalArgs->unk_0 == nullptr && pInternalArgs->unk_1 == nullptr && + pInternalArgs->unk_2 == nullptr, + "Internal arguments for requests not supported"); + } + + unlockContext(user_context->parentContext); + + if (pRequestId != nullptr) { + *pRequestId = request->requestId; + } + + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +OrbisNpWebApiRequest* findRequest(OrbisNpWebApiUserContext* userContext, s64 requestId) { + std::scoped_lock lk{userContext->parentContext->contextLock}; + if (userContext->requests.contains(requestId)) { + return userContext->requests[requestId]; + } + + return nullptr; +} + +OrbisNpWebApiRequest* findRequestAndMarkBusy(OrbisNpWebApiUserContext* userContext, s64 requestId) { + std::scoped_lock lk{userContext->parentContext->contextLock}; + if (userContext->requests.contains(requestId)) { + auto& request = userContext->requests[requestId]; + request->userCount++; + return request; + } + + return nullptr; +} + +bool isRequestBusy(OrbisNpWebApiRequest* request) { + std::scoped_lock lk{request->parentContext->contextLock}; + return request->userCount > 1; +} + +s32 setRequestTimeout(s64 requestId, u32 timeout) { + OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiRequest* request = findRequestAndMarkBusy(user_context, requestId); + if (request == nullptr) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND; + } + + request->requestTimeout = timeout; + + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +void startRequestTimer(OrbisNpWebApiRequest* request) { + if (request->requestTimeout != 0 && request->requestEndTime == 0) { + request->requestEndTime = Kernel::sceKernelGetProcessTime() + request->requestTimeout; + } +} + +void checkRequestTimeout(OrbisNpWebApiRequest* request) { + u64 time = Kernel::sceKernelGetProcessTime(); + if (!request->timedOut && request->requestEndTime != 0 && request->requestEndTime < time) { + request->timedOut = true; + abortRequest(request->requestId); + } +} + +s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s8 flag, + const OrbisNpWebApiResponseInformationOption* pRespInfoOption) { + OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiRequest* request = findRequestAndMarkBusy(user_context, requestId); + if (request == nullptr) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND; + } + + startRequestTimer(request); + + // TODO: multipart logic + + if (g_sdk_ver >= Common::ElfInfo::FW_25 && !request->sent) { + request->sent = true; + } + + lockContext(context); + if (!request->timedOut && request->aborted) { + unlockContext(context); + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_ABORTED; + } + + unlockContext(context); + + // Stubbing sceNpManagerIntGetSigninState call with a config check. + if (!Config::getPSNSignedIn()) { + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_NOT_SIGNED_IN; + } + + LOG_ERROR(Lib_NpWebApi, + "(STUBBED) called, requestId = {:#x}, pApiGroup = '{}', pPath = '{}', pContentType = " + "'{}', method = {}, multipart = {}", + requestId, request->userApiGroup, request->userPath, request->userContentType, + magic_enum::enum_name(request->userMethod), request->multipart); + + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +s32 abortRequestInternal(OrbisNpWebApiContext* context, OrbisNpWebApiUserContext* userContext, + OrbisNpWebApiRequest* request) { + if (context == nullptr || userContext == nullptr || request == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + std::scoped_lock lk{context->contextLock}; + if (request->aborted) { + return ORBIS_OK; + } + + request->aborted = true; + + // TODO: Should also abort any Np requests and Http requests tied to this request. + + return ORBIS_OK; +} + +s32 abortRequest(s64 requestId) { + OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiRequest* request = findRequest(user_context, requestId); + if (request == nullptr) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND; + } + + s32 result = abortRequestInternal(context, user_context, request); + + releaseUserContext(user_context); + releaseContext(context); + return result; +} + +void releaseRequest(OrbisNpWebApiRequest* request) { + std::scoped_lock lk{request->parentContext->contextLock}; + request->userCount--; +} + +s32 deleteRequest(s64 requestId) { + OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + lockContext(context); + OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiRequest* request = findRequestAndMarkBusy(user_context, requestId); + if (request == nullptr) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND; + } + + if (g_sdk_ver < Common::ElfInfo::FW_40 && isRequestBusy(request)) { + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_REQUEST_BUSY; + } + + abortRequestInternal(context, user_context, request); + while (isRequestBusy(request)) { + unlockContext(context); + Kernel::sceKernelUsleep(50000); + lockContext(context); + } + + releaseRequest(request); + user_context->requests.erase(request->requestId); + + releaseUserContext(user_context); + unlockContext(context); + releaseContext(context); + return ORBIS_OK; +} + +s32 createHandleInternal(OrbisNpWebApiContext* context) { + g_handle_count++; + if (g_handle_count >= 0xf0000000) { + g_handle_count = 1; + } + + std::scoped_lock lk{context->contextLock}; + + s32 handle_id = g_handle_count; + context->handles[handle_id] = new OrbisNpWebApiHandle{}; + auto& handle = context->handles[handle_id]; + handle->handleId = handle_id; + handle->userCount = 0; + handle->aborted = false; + handle->deleted = false; + + if (g_sdk_ver >= Common::ElfInfo::FW_30) { + context->timerHandles[handle_id] = new OrbisNpWebApiTimerHandle{}; + auto& timer_handle = context->timerHandles[handle_id]; + timer_handle->handleId = handle_id; + timer_handle->timedOut = false; + timer_handle->handleTimeout = 0; + timer_handle->handleEndTime = 0; + } + + return handle_id; +} + +s32 createHandle(s32 libCtxId) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + s32 result = createHandleInternal(context); + releaseContext(context); + return result; +} + +s32 setHandleTimeoutInternal(OrbisNpWebApiContext* context, s32 handleId, u32 timeout) { + std::scoped_lock lk{context->contextLock}; + if (!context->timerHandles.contains(handleId)) { + return ORBIS_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + } + auto& handle = context->handles[handleId]; + handle->userCount++; + + auto& timer_handle = context->timerHandles[handleId]; + timer_handle->handleTimeout = timeout; + + handle->userCount--; + return ORBIS_OK; +} + +s32 setHandleTimeout(s32 libCtxId, s32 handleId, u32 timeout) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + s32 result = setHandleTimeoutInternal(context, handleId, timeout); + releaseContext(context); + return result; +} + +void startHandleTimer(OrbisNpWebApiContext* context, s32 handleId) { + std::scoped_lock lk{context->contextLock}; + if (!context->timerHandles.contains(handleId)) { + return; + } + auto& timer_handle = context->timerHandles[handleId]; + if (timer_handle->handleTimeout == 0) { + return; + } + timer_handle->handleEndTime = Kernel::sceKernelGetProcessTime() + timer_handle->handleTimeout; +} + +void releaseHandle(OrbisNpWebApiContext* context, OrbisNpWebApiHandle* handle) { + if (handle != nullptr) { + std::scoped_lock lk{context->contextLock}; + handle->userCount--; + } +} + +s32 getHandle(OrbisNpWebApiContext* context, s32 handleId, OrbisNpWebApiHandle** handleOut) { + std::scoped_lock lk{context->contextLock}; + if (!context->handles.contains(handleId)) { + return ORBIS_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + } + auto& handle = context->handles[handleId]; + handle->userCount++; + if (handleOut != nullptr) { + *handleOut = handle; + } + return ORBIS_OK; +} + +s32 abortHandle(s32 libCtxId, s32 handleId) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiHandle* handle; + s32 result = getHandle(context, handleId, &handle); + if (result == ORBIS_OK) { + std::scoped_lock lk{context->contextLock}; + handle->aborted = true; + // TODO: sceNpAsmClientAbortRequest call + releaseHandle(context, handle); + } + + releaseContext(context); + return result; +} + +s32 deleteHandleInternal(OrbisNpWebApiContext* context, s32 handleId) { + lockContext(context); + if (!context->handles.contains(handleId)) { + return ORBIS_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + } + + auto& handle = context->handles[handleId]; + if (g_sdk_ver >= Common::ElfInfo::FW_40) { + if (handle->deleted) { + unlockContext(context); + return ORBIS_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + } + handle->deleted = true; + unlockContext(context); + abortHandle(context->libCtxId, handleId); + lockContext(context); + handle->userCount++; + while (handle->userCount > 1) { + handle->userCount--; + unlockContext(context); + Kernel::sceKernelUsleep(50000); + lockContext(context); + handle->userCount++; + } + handle->userCount--; + } else if (handle->userCount > 0) { + unlockContext(context); + return ORBIS_NP_WEBAPI_ERROR_HANDLE_BUSY; + } + + context->handles.erase(handleId); + + if (g_sdk_ver >= Common::ElfInfo::FW_30 && context->timerHandles.contains(handleId)) { + auto& timer_handle = context->timerHandles[handleId]; + context->timerHandles.erase(handleId); + } + + unlockContext(context); + return ORBIS_OK; +} + +s32 deleteHandle(s32 libCtxId, s32 handleId) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + s32 result = deleteHandleInternal(context, handleId); + releaseContext(context); + return result; +} + +s32 createPushEventFilterInternal(OrbisNpWebApiContext* context, + const OrbisNpWebApiPushEventFilterParameter* pFilterParam, + u64 filterParamNum) { + std::scoped_lock lk{context->contextLock}; + g_push_event_filter_count++; + if (g_push_event_filter_count >= 0xf0000000) { + g_push_event_filter_count = 1; + } + s32 filterId = g_push_event_filter_count; + + context->pushEventFilters[filterId] = new OrbisNpWebApiPushEventFilter{}; + auto& filter = context->pushEventFilters[filterId]; + filter->parentContext = context; + filter->filterId = filterId; + + if (pFilterParam != nullptr && filterParamNum != 0) { + for (u64 param_idx = 0; param_idx < filterParamNum; param_idx++) { + OrbisNpWebApiPushEventFilterParameter copy = OrbisNpWebApiPushEventFilterParameter{}; + memcpy(©, &pFilterParam[param_idx], sizeof(OrbisNpWebApiPushEventFilterParameter)); + filter->filterParams.emplace_back(copy); + } + } + return filterId; +} + +s32 createPushEventFilter(s32 libCtxId, const OrbisNpWebApiPushEventFilterParameter* pFilterParam, + u64 filterParamNum) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + s32 result = createPushEventFilterInternal(context, pFilterParam, filterParamNum); + releaseContext(context); + return result; +} + +s32 deletePushEventFilterInternal(OrbisNpWebApiContext* context, s32 filterId) { + std::scoped_lock lk{context->contextLock}; + if (!context->pushEventFilters.contains(filterId)) { + return ORBIS_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND; + } + + context->pushEventFilters[filterId]->filterParams.clear(); + context->pushEventFilters.erase(filterId); + return ORBIS_OK; +} + +s32 deletePushEventFilter(s32 libCtxId, s32 filterId) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + s32 result = deletePushEventFilterInternal(context, filterId); + releaseContext(context); + return result; +} + +s32 registerPushEventCallbackInternal(OrbisNpWebApiUserContext* userContext, s32 filterId, + OrbisNpWebApiPushEventCallback cbFunc, void* pUserArg) { + std::scoped_lock lk{userContext->parentContext->contextLock}; + g_registered_callback_count++; + if (g_registered_callback_count >= 0xf0000000) { + g_registered_callback_count = 1; + } + s32 cbId = g_registered_callback_count; + + userContext->pushEventCallbacks[cbId] = new OrbisNpWebApiRegisteredPushEventCallback{}; + auto& cb = userContext->pushEventCallbacks[cbId]; + cb->callbackId = cbId; + cb->filterId = filterId; + cb->cbFunc = cbFunc; + cb->pUserArg = pUserArg; + + return cbId; +} + +s32 registerPushEventCallback(s32 titleUserCtxId, s32 filterId, + OrbisNpWebApiPushEventCallback cbFunc, void* pUserArg) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + if (g_sdk_ver >= Common::ElfInfo::FW_25 && !context->pushEventFilters.contains(filterId)) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND; + } + + s32 result = registerPushEventCallbackInternal(user_context, filterId, cbFunc, pUserArg); + releaseUserContext(user_context); + releaseContext(context); + return result; +} + +s32 unregisterPushEventCallback(s32 titleUserCtxId, s32 callbackId) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + if (!user_context->pushEventCallbacks.contains(callbackId)) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_PUSH_EVENT_CALLBACK_NOT_FOUND; + } + + lockContext(context); + user_context->pushEventCallbacks.erase(callbackId); + unlockContext(context); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +s32 createServicePushEventFilterInternal( + OrbisNpWebApiContext* context, s32 handleId, const char* pNpServiceName, + OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiServicePushEventFilterParameter* pFilterParam, u64 filterParamNum) { + std::scoped_lock lk{context->contextLock}; + if (!context->handles.contains(handleId)) { + return ORBIS_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + } + auto& handle = context->handles[handleId]; + handle->userCount++; + + if (pNpServiceName != nullptr && !Config::getPSNSignedIn()) { + // Seems sceNpManagerIntGetUserList fails? + LOG_DEBUG(Lib_NpWebApi, "Cannot create service push event while PSN is disabled"); + handle->userCount--; + return ORBIS_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND; + } + + g_service_push_event_filter_count++; + if (g_service_push_event_filter_count >= 0xf0000000) { + g_service_push_event_filter_count = 1; + } + s32 filterId = g_service_push_event_filter_count; + + context->servicePushEventFilters[filterId] = new OrbisNpWebApiServicePushEventFilter{}; + auto& filter = context->servicePushEventFilters[filterId]; + filter->parentContext = context; + filter->filterId = filterId; + + if (pNpServiceName == nullptr) { + filter->internal = true; + } else { + // TODO: if pNpServiceName is non-null, create an np request for this filter. + LOG_ERROR(Lib_NpWebApi, "Np behavior not handled"); + filter->npServiceName = std::string(pNpServiceName); + } + + filter->npServiceLabel = npServiceLabel; + + if (pFilterParam != nullptr && filterParamNum != 0) { + for (u64 param_idx = 0; param_idx < filterParamNum; param_idx++) { + OrbisNpWebApiServicePushEventFilterParameter copy = + OrbisNpWebApiServicePushEventFilterParameter{}; + memcpy(©, &pFilterParam[param_idx], + sizeof(OrbisNpWebApiServicePushEventFilterParameter)); + filter->filterParams.emplace_back(copy); + } + } + + handle->userCount--; + return filterId; +} + +s32 createServicePushEventFilter(s32 libCtxId, s32 handleId, const char* pNpServiceName, + OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiServicePushEventFilterParameter* pFilterParam, + u64 filterParamNum) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + startHandleTimer(context, handleId); + s32 result = createServicePushEventFilterInternal(context, handleId, pNpServiceName, + npServiceLabel, pFilterParam, filterParamNum); + releaseContext(context); + return result; +} + +s32 deleteServicePushEventFilterInternal(OrbisNpWebApiContext* context, s32 filterId) { + std::scoped_lock lk{context->contextLock}; + if (!context->servicePushEventFilters.contains(filterId)) { + return ORBIS_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND; + } + + context->servicePushEventFilters[filterId]->filterParams.clear(); + context->servicePushEventFilters.erase(filterId); + return ORBIS_OK; +} + +s32 deleteServicePushEventFilter(s32 libCtxId, s32 filterId) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + s32 result = deleteServicePushEventFilterInternal(context, filterId); + releaseContext(context); + return result; +} + +s32 registerServicePushEventCallbackInternal( + OrbisNpWebApiUserContext* userContext, s32 filterId, + OrbisNpWebApiServicePushEventCallback cbFunc, + OrbisNpWebApiInternalServicePushEventCallback intCbFunc, + OrbisNpWebApiInternalServicePushEventCallbackA intCbFuncA, void* pUserArg) { + std::scoped_lock lk{userContext->parentContext->contextLock}; + if (cbFunc == nullptr && intCbFunc == nullptr && intCbFuncA == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + + g_registered_callback_count++; + if (g_registered_callback_count >= 0xf0000000) { + g_registered_callback_count = 1; + } + s32 cbId = g_registered_callback_count; + + userContext->servicePushEventCallbacks[cbId] = + new OrbisNpWebApiRegisteredServicePushEventCallback{}; + auto& cb = userContext->servicePushEventCallbacks[cbId]; + cb->callbackId = cbId; + cb->filterId = filterId; + cb->cbFunc = cbFunc; + cb->internalCbFunc = intCbFunc; + cb->internalCbFuncA = intCbFuncA; + cb->pUserArg = pUserArg; + + return cbId; +} + +s32 registerServicePushEventCallback(s32 titleUserCtxId, s32 filterId, + OrbisNpWebApiServicePushEventCallback cbFunc, + OrbisNpWebApiInternalServicePushEventCallback intCbFunc, + OrbisNpWebApiInternalServicePushEventCallbackA intCbFuncA, + void* pUserArg) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + if (g_sdk_ver >= Common::ElfInfo::FW_25 && + !context->servicePushEventFilters.contains(filterId)) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND; + } + + s32 result = registerServicePushEventCallbackInternal(user_context, filterId, cbFunc, intCbFunc, + intCbFuncA, pUserArg); + releaseUserContext(user_context); + releaseContext(context); + return result; +} + +s32 unregisterServicePushEventCallback(s32 titleUserCtxId, s32 callbackId) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + if (!user_context->servicePushEventCallbacks.contains(callbackId)) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_CALLBACK_NOT_FOUND; + } + + lockContext(context); + user_context->servicePushEventCallbacks.erase(callbackId); + unlockContext(context); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +s32 createExtendedPushEventFilterInternal( + OrbisNpWebApiContext* context, s32 handleId, const char* pNpServiceName, + OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiExtdPushEventFilterParameter* pFilterParam, u64 filterParamNum, + bool internal) { + std::scoped_lock lk{context->contextLock}; + if (!context->handles.contains(handleId)) { + return ORBIS_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + } + auto& handle = context->handles[handleId]; + handle->userCount++; + + if (pNpServiceName != nullptr && !Config::getPSNSignedIn()) { + // Seems sceNpManagerIntGetUserList fails? + LOG_DEBUG(Lib_NpWebApi, "Cannot create extended push event while PSN is disabled"); + handle->userCount--; + return ORBIS_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND; + } + + g_extended_push_event_filter_count++; + if (g_extended_push_event_filter_count >= 0xf0000000) { + g_extended_push_event_filter_count = 1; + } + s32 filterId = g_extended_push_event_filter_count; + + context->extendedPushEventFilters[filterId] = new OrbisNpWebApiExtendedPushEventFilter{}; + auto& filter = context->extendedPushEventFilters[filterId]; + filter->internal = internal; + filter->parentContext = context; + filter->filterId = filterId; + + if (pNpServiceName == nullptr) { + npServiceLabel = ORBIS_NP_INVALID_SERVICE_LABEL; + } else { + // TODO: if pNpServiceName is non-null, create an np request for this filter. + LOG_ERROR(Lib_NpWebApi, "Np behavior not handled"); + filter->npServiceName = std::string(pNpServiceName); + } + + filter->npServiceLabel = npServiceLabel; + + if (pFilterParam != nullptr && filterParamNum != 0) { + for (u64 param_idx = 0; param_idx < filterParamNum; param_idx++) { + OrbisNpWebApiExtdPushEventFilterParameter copy = + OrbisNpWebApiExtdPushEventFilterParameter{}; + memcpy(©, &pFilterParam[param_idx], + sizeof(OrbisNpWebApiExtdPushEventFilterParameter)); + filter->filterParams.emplace_back(copy); + // TODO: Every parameter is registered with an extended data filter through + // sceNpPushRegisterExtendedDataFilter + } + } + + handle->userCount--; + return filterId; +} + +s32 createExtendedPushEventFilter(s32 libCtxId, s32 handleId, const char* pNpServiceName, + OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiExtdPushEventFilterParameter* pFilterParam, + u64 filterParamNum, bool internal) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + startHandleTimer(context, handleId); + s32 result = createExtendedPushEventFilterInternal( + context, handleId, pNpServiceName, npServiceLabel, pFilterParam, filterParamNum, internal); + releaseContext(context); + return result; +} + +s32 deleteExtendedPushEventFilterInternal(OrbisNpWebApiContext* context, s32 filterId) { + std::scoped_lock lk{context->contextLock}; + if (!context->extendedPushEventFilters.contains(filterId)) { + return ORBIS_NP_WEBAPI_ERROR_EXTD_PUSH_EVENT_FILTER_NOT_FOUND; + } + + context->extendedPushEventFilters[filterId]->filterParams.clear(); + context->extendedPushEventFilters.erase(filterId); + return ORBIS_OK; +} + +s32 deleteExtendedPushEventFilter(s32 libCtxId, s32 filterId) { + OrbisNpWebApiContext* context = findAndValidateContext(libCtxId); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + s32 result = deleteExtendedPushEventFilterInternal(context, filterId); + releaseContext(context); + return result; +} + +s32 registerExtdPushEventCallbackInternal(OrbisNpWebApiUserContext* userContext, s32 filterId, + OrbisNpWebApiExtdPushEventCallback cbFunc, + OrbisNpWebApiExtdPushEventCallbackA cbFuncA, + void* pUserArg) { + std::scoped_lock lk{userContext->parentContext->contextLock}; + + g_registered_callback_count++; + if (g_registered_callback_count >= 0xf0000000) { + g_registered_callback_count = 1; + } + + if (cbFunc == nullptr && cbFuncA == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + s32 cbId = g_registered_callback_count; + + userContext->extendedPushEventCallbacks[cbId] = + new OrbisNpWebApiRegisteredExtendedPushEventCallback{}; + auto& cb = userContext->extendedPushEventCallbacks[cbId]; + cb->callbackId = cbId; + cb->filterId = filterId; + cb->cbFunc = cbFunc; + cb->cbFuncA = cbFuncA; + cb->pUserArg = pUserArg; + + return cbId; +} + +s32 registerExtdPushEventCallback(s32 titleUserCtxId, s32 filterId, + OrbisNpWebApiExtdPushEventCallback cbFunc, + OrbisNpWebApiExtdPushEventCallbackA cbFuncA, void* pUserArg) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + if (!context->extendedPushEventFilters.contains(filterId)) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_EXTD_PUSH_EVENT_FILTER_NOT_FOUND; + } + + s32 result = + registerExtdPushEventCallbackInternal(user_context, filterId, cbFunc, cbFuncA, pUserArg); + releaseUserContext(user_context); + releaseContext(context); + return result; +} + +s32 registerExtdPushEventCallbackA(s32 titleUserCtxId, s32 filterId, + OrbisNpWebApiExtdPushEventCallbackA cbFunc, void* pUserArg) { + return registerExtdPushEventCallback(titleUserCtxId, filterId, nullptr, cbFunc, pUserArg); +} + +s32 unregisterExtdPushEventCallback(s32 titleUserCtxId, s32 callbackId) { + OrbisNpWebApiContext* context = findAndValidateContext(titleUserCtxId >> 0x10); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, titleUserCtxId); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + if (!user_context->extendedPushEventCallbacks.contains(callbackId)) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_EXTD_PUSH_EVENT_CALLBACK_NOT_FOUND; + } + + lockContext(context); + user_context->extendedPushEventCallbacks.erase(callbackId); + unlockContext(context); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI getHttpRequestIdFromRequest(OrbisNpWebApiRequest* request) + +{ + return request->requestId; +} + +s32 PS4_SYSV_ABI getHttpStatusCodeInternal(s64 requestId, s32* out_status_code) { + s32 status_code; + OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiRequest* request = findRequest(user_context, requestId); + if (request == nullptr) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND; + } + + // Query HTTP layer + { + int32_t httpReqId = getHttpRequestIdFromRequest(request); + s32 err = Libraries::Http::sceHttpGetStatusCode(httpReqId, &status_code); + + if (out_status_code != nullptr) + *out_status_code = status_code; + + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + + return err; + } +} + +void PS4_SYSV_ABI setRequestEndTime(OrbisNpWebApiRequest* request) { + u64 time; + if ((request->requestTimeout != 0) && (request->requestEndTime == 0)) { + time = Libraries::Kernel::sceKernelGetProcessTime(); + request->requestEndTime = (u64)request->requestTimeout + time; + } +} + +void PS4_SYSV_ABI clearRequestEndTime(OrbisNpWebApiRequest* req) { + req->requestEndTime = 0; + return; +} + +bool PS4_SYSV_ABI hasRequestTimedOut(OrbisNpWebApiRequest* request) { + return request->timedOut; +} + +bool PS4_SYSV_ABI isRequestAborted(OrbisNpWebApiRequest* request) { + return request->aborted; +} + +void PS4_SYSV_ABI setRequestState(OrbisNpWebApiRequest* request, u8 state) { + request->requestState = state; +} + +u64 PS4_SYSV_ABI copyRequestData(OrbisNpWebApiRequest* request, void* data, u64 size) { + u64 readSize = 0; + + if (request->remainingData != 0) { + u64 remainingSize = request->remainingData - request->readOffset; + + if (remainingSize != 0) { + if (remainingSize < size) { + size = remainingSize; + } + memcpy(data, request->data + request->readOffset, size); + request->readOffset += static_cast(size); + readSize = size; + } + } + return readSize; +} + +s32 PS4_SYSV_ABI readDataInternal(s64 requestId, void* pData, u64 size) { + u32 offset; + s32 result; + u64 remainingSize; + u64 bytesCopied; + + OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + + OrbisNpWebApiRequest* request = findRequest(user_context, requestId); + if (request == nullptr) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND; + } + + setRequestEndTime(request); + + bytesCopied = copyRequestData(request, pData, size); + offset = (u32)bytesCopied; + remainingSize = size - offset; + + // If caller wants more data than buffered + if (remainingSize != 0) { + lockContext(context); + setRequestState(request, 5); // TODO add request states? + + if (!hasRequestTimedOut(request) && isRequestAborted(request)) { + unlockContext(context); + offset = ORBIS_NP_WEBAPI_ERROR_ABORTED; + } else { + unlockContext(context); + + int32_t httpReqId = getHttpRequestIdFromRequest(request); + int32_t httpRead = + Libraries::Http::sceHttpReadData(httpReqId, (u8*)pData + offset, remainingSize); + + if (httpRead < 0) + httpRead = 0; + + offset += httpRead; + } + } + + // Final state resolution + lockContext(context); + setRequestState(request, 0); + + if (hasRequestTimedOut(request)) { + result = ORBIS_NP_WEBAPI_ERROR_TIMEOUT; + } else if (isRequestAborted(request)) { + result = ORBIS_NP_WEBAPI_ERROR_ABORTED; + } else { + result = offset; + } + + unlockContext(context); + + // Cleanup + clearRequestEndTime(request); + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + + return result; +} + +}; // namespace Libraries::Np::NpWebApi \ No newline at end of file diff --git a/src/core/libraries/np/np_web_api_internal.h b/src/core/libraries/np/np_web_api_internal.h new file mode 100644 index 000000000..571df0ab9 --- /dev/null +++ b/src/core/libraries/np/np_web_api_internal.h @@ -0,0 +1,301 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" +#include "core/libraries/np/np_web_api.h" +#include "core/libraries/np/np_web_api_error.h" + +namespace Libraries::Np::NpWebApi { + +// Structs reference each other, so declare them before their contents. +struct OrbisNpWebApiContext; +struct OrbisNpWebApiUserContext; +struct OrbisNpWebApiRequest; +struct OrbisNpWebApiHandle; +struct OrbisNpWebApiTimerHandle; +struct OrbisNpWebApiPushEventFilter; +struct OrbisNpWebApiServicePushEventFilter; +struct OrbisNpWebApiExtendedPushEventFilter; +struct OrbisNpWebApiRegisteredPushEventCallback; +struct OrbisNpWebApiRegisteredServicePushEventCallback; +struct OrbisNpWebApiRegisteredExtendedPushEventCallback; + +struct OrbisNpWebApiContext { + s32 type; + s32 userCount; + s32 libCtxId; + s32 libHttpCtxId; + std::recursive_mutex contextLock; + std::map userContexts; + std::map handles; + std::map timerHandles; + std::map pushEventFilters; + std::map servicePushEventFilters; + std::map extendedPushEventFilters; + std::string name; + bool terminated; +}; + +struct OrbisNpWebApiUserContext { + OrbisNpWebApiContext* parentContext; + s32 userCount; + s32 userCtxId; + Libraries::UserService::OrbisUserServiceUserId userId; + std::map requests; + std::map pushEventCallbacks; + std::map servicePushEventCallbacks; + std::map extendedPushEventCallbacks; + bool deleted; + OrbisNpWebApiNotificationCallback notificationCallbackFunction; + void* pNotificationCallbackUserArgs; +}; + +struct OrbisNpWebApiRequest { + OrbisNpWebApiContext* parentContext; + s32 userCount; + s64 requestId; + std::string userApiGroup; + std::string userPath; + OrbisNpWebApiHttpMethod userMethod; + u64 userContentLength; + std::string userContentType; + bool multipart; + bool aborted; + bool sent; + u32 requestTimeout; + u64 requestEndTime; + bool timedOut; + // not sure Stephen + u8 requestState; + u64 remainingData; + u32 readOffset; + char data[64]; +}; + +struct OrbisNpWebApiHandle { + s32 handleId; + bool aborted; + bool deleted; + s32 userCount; +}; + +struct OrbisNpWebApiTimerHandle { + s32 handleId; + u32 handleTimeout; + u64 handleEndTime; + bool timedOut; +}; + +struct OrbisNpWebApiPushEventFilter { + s32 filterId; + std::vector filterParams; + OrbisNpWebApiContext* parentContext; +}; + +struct OrbisNpWebApiServicePushEventFilter { + s32 filterId; + bool internal; + std::vector filterParams; + std::string npServiceName; + OrbisNpServiceLabel npServiceLabel; + OrbisNpWebApiContext* parentContext; +}; + +struct OrbisNpWebApiExtendedPushEventFilter { + s32 filterId; + bool internal; + std::vector filterParams; + std::string npServiceName; + OrbisNpServiceLabel npServiceLabel; + OrbisNpWebApiContext* parentContext; +}; + +struct OrbisNpWebApiRegisteredPushEventCallback { + s32 callbackId; + s32 filterId; + OrbisNpWebApiPushEventCallback cbFunc; + void* pUserArg; +}; + +struct OrbisNpWebApiRegisteredServicePushEventCallback { + s32 callbackId; + s32 filterId; + OrbisNpWebApiServicePushEventCallback cbFunc; + OrbisNpWebApiInternalServicePushEventCallback internalCbFunc; + // Note: real struct stores both internal callbacks in one field + OrbisNpWebApiInternalServicePushEventCallbackA internalCbFuncA; + void* pUserArg; +}; + +struct OrbisNpWebApiRegisteredExtendedPushEventCallback { + s32 callbackId; + s32 filterId; + OrbisNpWebApiExtdPushEventCallback cbFunc; + // Note: real struct stores both callbacks in one field + OrbisNpWebApiExtdPushEventCallbackA cbFuncA; + void* pUserArg; +}; + +// General functions +s32 initializeLibrary(); // FUN_01001450 +s32 getCompiledSdkVersion(); // FUN_01001440 + +// Library context functions +s32 createLibraryContext(s32 libHttpCtxId, u64 poolSize, const char* name, + s32 type); // FUN_01006970 +OrbisNpWebApiContext* findAndValidateContext(s32 libCtxId, s32 flag = 0); // FUN_01006860 +void releaseContext(OrbisNpWebApiContext* context); // FUN_01006fc0 +bool isContextTerminated(OrbisNpWebApiContext* context); // FUN_01006910 +bool isContextBusy(OrbisNpWebApiContext* context); // FUN_01008a50 +bool areContextHandlesBusy(OrbisNpWebApiContext* context); // FUN_01008c20 +void lockContext(OrbisNpWebApiContext* context); // FUN_010072e0 +void unlockContext(OrbisNpWebApiContext* context); // FUN_010072f0 +void markContextAsTerminated(OrbisNpWebApiContext* context); // FUN_01008bf0 +void checkContextTimeout(OrbisNpWebApiContext* context); // FUN_01008ad0 +void checkTimeout(); // FUN_01003700 +s32 deleteContext(s32 libCtxId); // FUN_01006c70 +s32 terminateContext(s32 libCtxId); // FUN_010014b0 + +// User context functions +OrbisNpWebApiUserContext* findUserContextByUserId( + OrbisNpWebApiContext* context, + Libraries::UserService::OrbisUserServiceUserId userId); // FUN_010075c0 +OrbisNpWebApiUserContext* findUserContext(OrbisNpWebApiContext* context, + s32 userCtxId); // FUN_01007530 +s32 createUserContextWithOnlineId(s32 libCtxId, OrbisNpOnlineId* onlineId); // FUN_010016a0 +s32 createUserContext(s32 libCtxId, + Libraries::UserService::OrbisUserServiceUserId userId); // FUN_010015c0 +s32 registerNotificationCallback(s32 titleUserCtxId, OrbisNpWebApiNotificationCallback cbFunc, + void* pUserArg); // FUN_01003770 +s32 unregisterNotificationCallback(s32 titleUserCtxId); // FUN_01003800 +bool isUserContextBusy(OrbisNpWebApiUserContext* userContext); // FUN_0100ea40 +bool areUserContextRequestsBusy(OrbisNpWebApiUserContext* userContext); // FUN_0100d1f0 +void releaseUserContext(OrbisNpWebApiUserContext* userContext); // FUN_0100caa0 +void checkUserContextTimeout(OrbisNpWebApiUserContext* userContext); // FUN_0100ea90 +s32 deleteUserContext(s32 userCtxId); // FUN_01001710 + +// Request functions +s32 createRequest(s32 titleUserCtxId, const char* pApiGroup, const char* pPath, + OrbisNpWebApiHttpMethod method, + const OrbisNpWebApiContentParameter* pContentParameter, + const OrbisNpWebApiIntCreateRequestExtraArgs* pInternalArgs, s64* pRequestId, + bool isMultipart); // FUN_01001850 +OrbisNpWebApiRequest* findRequest(OrbisNpWebApiUserContext* userContext, + s64 requestId); // FUN_0100d3a0 +OrbisNpWebApiRequest* findRequestAndMarkBusy(OrbisNpWebApiUserContext* userContext, + s64 requestId); // FUN_0100d330 +bool isRequestBusy(OrbisNpWebApiRequest* request); // FUN_0100c1b0 +s32 setRequestTimeout(s64 requestId, u32 timeout); // FUN_01003610 +void startRequestTimer(OrbisNpWebApiRequest* request); // FUN_0100c0d0 +void checkRequestTimeout(OrbisNpWebApiRequest* request); // FUN_0100c130 +s32 sendRequest( + s64 requestId, s32 partIndex, const void* data, u64 dataSize, s8 flag, + const OrbisNpWebApiResponseInformationOption* pResponseInformationOption); // FUN_01001c50 +s32 abortRequestInternal(OrbisNpWebApiContext* context, OrbisNpWebApiUserContext* userContext, + OrbisNpWebApiRequest* request); // FUN_01001b70 +s32 abortRequest(s64 requestId); // FUN_01002c70 +void releaseRequest(OrbisNpWebApiRequest* request); // FUN_01009fb0 +s32 deleteRequest(s64 requestId); // FUN_010019a0 + +// Handle functions +s32 createHandleInternal(OrbisNpWebApiContext* context); // FUN_01007730 +s32 createHandle(s32 libCtxId); // FUN_01002ee0 +s32 setHandleTimeoutInternal(OrbisNpWebApiContext* context, s32 handleId, + u32 timeout); // FUN_01007ed0 +s32 setHandleTimeout(s32 libCtxId, s32 handleId, u32 timeout); // FUN_010036b0 +void startHandleTimer(OrbisNpWebApiContext* context, s32 handleId); // FUN_01007fd0 +void releaseHandle(OrbisNpWebApiContext* context, OrbisNpWebApiHandle* handle); // FUN_01007ea0 +s32 getHandle(OrbisNpWebApiContext* context, s32 handleId, + OrbisNpWebApiHandle** handleOut); // FUN_01007e20 +s32 abortHandle(s32 libCtxId, s32 handleId); // FUN_01003390 +s32 deleteHandleInternal(OrbisNpWebApiContext* context, s32 handleId); // FUN_01007a00 +s32 deleteHandle(s32 libCtxId, s32 handleId); // FUN_01002f20 + +// Push event filter functions +s32 createPushEventFilterInternal(OrbisNpWebApiContext* context, + const OrbisNpWebApiPushEventFilterParameter* pFilterParam, + u64 filterParamNum); // FUN_01008040 +s32 createPushEventFilter(s32 libCtxId, const OrbisNpWebApiPushEventFilterParameter* pFilterParam, + u64 filterParamNum); // FUN_01002d10 +s32 deletePushEventFilterInternal(OrbisNpWebApiContext* context, s32 filterId); // FUN_01008180 +s32 deletePushEventFilter(s32 libCtxId, s32 filterId); // FUN_01002d60 + +// Push event callback functions +s32 registerPushEventCallbackInternal(OrbisNpWebApiUserContext* userContext, s32 filterId, + OrbisNpWebApiPushEventCallback cbFunc, + void* userArg); // FUN_0100d450 +s32 registerPushEventCallback(s32 titleUserCtxId, s32 filterId, + OrbisNpWebApiPushEventCallback cbFunc, + void* pUserArg); // FUN_01002da0 +s32 unregisterPushEventCallback(s32 titleUserCtxId, s32 callbackId); // FUN_01002e50 + +// Service push event filter functions +s32 createServicePushEventFilterInternal( + OrbisNpWebApiContext* context, s32 handleId, const char* pNpServiceName, + OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiServicePushEventFilterParameter* pFilterParam, + u64 filterParamNum); // FUN_010082f0 +s32 createServicePushEventFilter(s32 libCtxId, s32 handleId, const char* pNpServiceName, + OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiServicePushEventFilterParameter* pFilterParam, + u64 filterParamNum); // FUN_01002f60 +s32 deleteServicePushEventFilterInternal(OrbisNpWebApiContext* context, + s32 filterId); // FUN_010084f0 +s32 deleteServicePushEventFilter(s32 libCtxId, s32 filterId); // FUN_01002fe0 + +// Service push event callback functions +s32 registerServicePushEventCallbackInternal( + OrbisNpWebApiUserContext* userContext, s32 filterId, + OrbisNpWebApiServicePushEventCallback cbFunc, + OrbisNpWebApiInternalServicePushEventCallback intCbFunc, + OrbisNpWebApiInternalServicePushEventCallbackA intCbFuncA, void* pUserArg); // FUN_0100d8c0 +s32 registerServicePushEventCallback(s32 titleUserCtxId, s32 filterId, + OrbisNpWebApiServicePushEventCallback cbFunc, + OrbisNpWebApiInternalServicePushEventCallback intCbFunc, + OrbisNpWebApiInternalServicePushEventCallbackA intCbFuncA, + void* pUserArg); // FUN_01003030 +s32 unregisterServicePushEventCallback(s32 titleUserCtxId, s32 callbackId); // FUN_010030f0 + +// Extended push event filter functions +s32 createExtendedPushEventFilterInternal( + OrbisNpWebApiContext* context, s32 handleId, const char* pNpServiceName, + OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiExtdPushEventFilterParameter* pFilterParam, u64 filterParamNum, + bool internal); // FUN_01008680 +s32 createExtendedPushEventFilter(s32 libCtxId, s32 handleId, const char* pNpServiceName, + OrbisNpServiceLabel npServiceLabel, + const OrbisNpWebApiExtdPushEventFilterParameter* pFilterParam, + u64 filterParamNum, bool internal); // FUN_01003180 +s32 deleteExtendedPushEventFilterInternal(OrbisNpWebApiContext* context, + s32 filterId); // FUN_01008880 +s32 deleteExtendedPushEventFilter(s32 libCtxId, s32 filterId); // FUN_01003200 + +// Extended push event callback functions +s32 registerExtdPushEventCallbackInternal(OrbisNpWebApiUserContext* userContext, s32 filterId, + OrbisNpWebApiExtdPushEventCallback cbFunc, + OrbisNpWebApiExtdPushEventCallbackA cbFuncA, + void* pUserArg); // FUN_0100df60 +s32 registerExtdPushEventCallback(s32 userCtxId, s32 filterId, + OrbisNpWebApiExtdPushEventCallback cbFunc, + OrbisNpWebApiExtdPushEventCallbackA cbFuncA, + void* pUserArg); // FUN_01003250 +s32 registerExtdPushEventCallbackA(s32 userCtxId, s32 filterId, + OrbisNpWebApiExtdPushEventCallbackA cbFunc, + void* pUserArg); // FUN_01003240 +s32 unregisterExtdPushEventCallback(s32 titleUserCtxId, s32 callbackId); // FUN_01003300 + +s32 PS4_SYSV_ABI getHttpStatusCodeInternal(s64 requestId, s32* out_status_code); +s32 PS4_SYSV_ABI getHttpRequestIdFromRequest(OrbisNpWebApiRequest* request); +s32 PS4_SYSV_ABI readDataInternal(s64 requestId, void* pData, u64 size); +void PS4_SYSV_ABI setRequestEndTime(OrbisNpWebApiRequest* request); +void PS4_SYSV_ABI clearRequestEndTime(OrbisNpWebApiRequest* req); +bool PS4_SYSV_ABI hasRequestTimedOut(OrbisNpWebApiRequest* request); +void PS4_SYSV_ABI setRequestState(OrbisNpWebApiRequest* request, u8 state); +u64 PS4_SYSV_ABI copyRequestData(OrbisNpWebApiRequest* request, void* data, u64 size); + +}; // namespace Libraries::Np::NpWebApi \ No newline at end of file From 88cdfb4b1f22d97d80fd3e3e6435c507a57f5319 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:55:40 -0600 Subject: [PATCH 10/20] Fix assert (#4002) Just a typical day of me pushing something a month ago, nobody testing/reviewing it, then finding out it's broken when that code inevitably makes it into production. --- src/core/libraries/np/np_web_api_internal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/np/np_web_api_internal.cpp b/src/core/libraries/np/np_web_api_internal.cpp index 3c7557b72..3d6c7de86 100644 --- a/src/core/libraries/np/np_web_api_internal.cpp +++ b/src/core/libraries/np/np_web_api_internal.cpp @@ -457,7 +457,7 @@ s32 createRequest(s32 titleUserCtxId, const char* pApiGroup, const char* pPath, request_id--; } // Real library would hang if this assert fails. - ASSERT_MSG(request_id > user_ctx_id << 0x20, "Too many requests!"); + ASSERT_MSG(request_id <= (user_ctx_id << 0x20), "Too many requests!"); user_context->requests[request_id] = new OrbisNpWebApiRequest{}; auto& request = user_context->requests[request_id]; From 2e789649ecc34373238a31e8388b14ffccd46db8 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Sat, 7 Feb 2026 21:39:08 +0100 Subject: [PATCH 11/20] Automatically add missing hotkeys to the global input config (#4003) --- src/common/config.cpp | 53 ++++++++++++++++++++++++++----------- src/common/config.h | 4 +-- src/input/input_handler.cpp | 13 ++++++--- src/input/input_handler.h | 4 ++- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/common/config.cpp b/src/common/config.cpp index aece6916f..8fe624c42 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -1,7 +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 +#include #include #include #include @@ -14,6 +15,8 @@ #include "common/path_util.h" #include "common/scm_rev.h" +#include "input/input_handler.h" + using std::nullopt; using std::optional; using std::string; @@ -1289,19 +1292,6 @@ void setDefaultValues(bool is_game_specific) { constexpr std::string_view GetDefaultGlobalConfig() { return R"(# Anything put here will be loaded for all games, # alongside the game's config or default.ini depending on your preference. - -hotkey_renderdoc_capture = f12 -hotkey_fullscreen = f11 -hotkey_show_fps = f10 -hotkey_pause = f9 -hotkey_reload_inputs = f8 -hotkey_toggle_mouse_to_joystick = f7 -hotkey_toggle_mouse_to_gyro = f6 -hotkey_toggle_mouse_to_touchpad = delete -hotkey_quit = lctrl, lshift, end - -hotkey_volume_up = kpplus -hotkey_volume_down = kpminus )"; } @@ -1379,7 +1369,7 @@ analog_deadzone = rightjoystick, 2, 127 override_controller_color = false, 0, 0, 255 )"; } -std::filesystem::path GetFoolproofInputConfigFile(const string& game_id) { +std::filesystem::path GetInputConfigFile(const string& game_id) { // Read configuration file of the game, and if it doesn't exist, generate it from default // If that doesn't exist either, generate that from getDefaultConfig() and try again // If even the folder is missing, we start with that. @@ -1418,6 +1408,39 @@ std::filesystem::path GetFoolproofInputConfigFile(const string& game_id) { } } } + if (game_id == "global") { + std::map default_bindings_to_add = { + {"hotkey_renderdoc_capture", "f12"}, + {"hotkey_fullscreen", "f11"}, + {"hotkey_show_fps", "f10"}, + {"hotkey_pause", "f9"}, + {"hotkey_reload_inputs", "f8"}, + {"hotkey_toggle_mouse_to_joystick", "f7"}, + {"hotkey_toggle_mouse_to_gyro", "f6"}, + {"hotkey_toggle_mouse_to_touchpad", "delete"}, + {"hotkey_quit", "lctrl, lshift, end"}, + {"hotkey_volume_up", "kpplus"}, + {"hotkey_volume_down", "kpminus"}, + }; + std::ifstream global_in(config_file); + string line; + while (std::getline(global_in, line)) { + line.erase(std::remove_if(line.begin(), line.end(), + [](unsigned char c) { return std::isspace(c); }), + line.end()); + std::size_t equal_pos = line.find('='); + if (equal_pos == std::string::npos) { + continue; + } + std::string output_string = line.substr(0, equal_pos); + default_bindings_to_add.erase(output_string); + } + global_in.close(); + std::ofstream global_out(config_file, std::ios::app); + for (auto const& b : default_bindings_to_add) { + global_out << b.first << " = " << b.second << "\n"; + } + } // If game-specific config doesn't exist, create it from the default config if (!std::filesystem::exists(config_file)) { diff --git a/src/common/config.h b/src/common/config.h index 2a95e6cf0..036d04c99 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -183,6 +183,6 @@ std::filesystem::path getAddonInstallDir(); void setDefaultValues(bool is_game_specific = false); constexpr std::string_view GetDefaultGlobalConfig(); -std::filesystem::path GetFoolproofInputConfigFile(const std::string& game_id = ""); +std::filesystem::path GetInputConfigFile(const std::string& game_id = ""); }; // namespace Config diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index e6705edad..17a82b9cb 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "input_handler.h" @@ -200,6 +200,8 @@ InputBinding GetBindingFromString(std::string& line) { input = InputID(InputType::Axis, string_to_axis_map.at(t).axis); } else if (string_to_cbutton_map.find(t) != string_to_cbutton_map.end()) { input = InputID(InputType::Controller, string_to_cbutton_map.at(t)); + } else if (string_to_hotkey_map.find(t) != string_to_hotkey_map.end()) { + input = InputID(InputType::Controller, string_to_hotkey_map.at(t)); } else { // Invalid token found; return default binding LOG_DEBUG(Input, "Invalid token found: {}", t); @@ -220,8 +222,8 @@ InputBinding GetBindingFromString(std::string& line) { void ParseInputConfig(const std::string game_id = "") { std::string game_id_or_default = Config::GetUseUnifiedInputConfig() ? "default" : game_id; - const auto config_file = Config::GetFoolproofInputConfigFile(game_id_or_default); - const auto global_config_file = Config::GetFoolproofInputConfigFile("global"); + const auto config_file = Config::GetInputConfigFile(game_id_or_default); + const auto global_config_file = Config::GetInputConfigFile("global"); // we reset these here so in case the user fucks up or doesn't include some of these, // we can fall back to default @@ -396,13 +398,16 @@ void ParseInputConfig(const std::string game_id = "") { InputBinding binding = GetBindingFromString(input_string); BindingConnection connection(InputID(), nullptr); auto button_it = string_to_cbutton_map.find(output_string); + if (button_it == string_to_cbutton_map.end()) { + button_it = string_to_hotkey_map.find(output_string); + } auto axis_it = string_to_axis_map.find(output_string); if (binding.IsEmpty()) { LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.", lineCount, line); return; } - if (button_it != string_to_cbutton_map.end()) { + if (button_it != string_to_hotkey_map.end()) { connection = BindingConnection( binding, &*std::ranges::find(output_array, ControllerOutput(button_it->second))); connections.insert(connections.end(), connection); diff --git a/src/input/input_handler.h b/src/input/input_handler.h index 43c09ba55..844870b5d 100644 --- a/src/input/input_handler.h +++ b/src/input/input_handler.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -138,6 +138,8 @@ const std::map string_to_cbutton_map = { {"rpaddle_high", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1}, {"rpaddle_low", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2}, {"mouse_gyro_roll_mode", MOUSE_GYRO_ROLL_MODE}, +}; +const std::map string_to_hotkey_map = { {"hotkey_pause", HOTKEY_PAUSE}, {"hotkey_fullscreen", HOTKEY_FULLSCREEN}, {"hotkey_show_fps", HOTKEY_SIMPLE_FPS}, From 341de9aa176440e68861880c04c253cd05816f46 Mon Sep 17 00:00:00 2001 From: Kyoskii <125975056+Kyoskii@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:03:45 -0600 Subject: [PATCH 12/20] Update config.cpp (#4004) InternalScreenWidth would get internalScreenHeight instead of InternalScreenWidth --- src/common/config.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/config.cpp b/src/common/config.cpp index 8fe624c42..657943c95 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -364,7 +364,7 @@ u32 getWindowHeight() { } u32 getInternalScreenWidth() { - return internalScreenHeight.get(); + return internalScreenWidth.get(); } u32 getInternalScreenHeight() { From be86b5fe3231570cac207b470449f3edcd87f122 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:33:20 -0600 Subject: [PATCH 13/20] Kernel.Pthreads: Remove unreachable in posix_pthread_mutex_timedlock (#4005) * Fix assert Just a typical day of me pushing something a month ago, nobody testing/reviewing it, then finding out it's broken when that code inevitably makes it into production. * Remove unreachable in posix_pthread_mutex_timedlock It's apparently something that was added during pthreads rewrite, but the actual code for this function seems to be fully implemented? --- src/core/libraries/kernel/threads/mutex.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/libraries/kernel/threads/mutex.cpp b/src/core/libraries/kernel/threads/mutex.cpp index 7a046e973..31e8b900b 100644 --- a/src/core/libraries/kernel/threads/mutex.cpp +++ b/src/core/libraries/kernel/threads/mutex.cpp @@ -261,7 +261,6 @@ int PS4_SYSV_ABI posix_pthread_mutex_lock(PthreadMutexT* mutex) { int PS4_SYSV_ABI posix_pthread_mutex_timedlock(PthreadMutexT* mutex, const OrbisKernelTimespec* abstime) { CHECK_AND_INIT_MUTEX - UNREACHABLE(); return (*mutex)->Lock(abstime); } From c9a9cf2e75e342985e82c058408e78d4ad6ce0ee Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:43:15 +0100 Subject: [PATCH 14/20] fix debug assert (#4006) --- src/input/input_handler.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index 17a82b9cb..7c5263f8f 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -396,22 +396,23 @@ void ParseInputConfig(const std::string game_id = "") { // normal cases InputBinding binding = GetBindingFromString(input_string); - BindingConnection connection(InputID(), nullptr); - auto button_it = string_to_cbutton_map.find(output_string); - if (button_it == string_to_cbutton_map.end()) { - button_it = string_to_hotkey_map.find(output_string); - } - auto axis_it = string_to_axis_map.find(output_string); if (binding.IsEmpty()) { LOG_WARNING(Input, "Invalid format at line: {}, data: \"{}\", skipping line.", lineCount, line); return; } - if (button_it != string_to_hotkey_map.end()) { + BindingConnection connection(InputID(), nullptr); + auto button_it = string_to_cbutton_map.find(output_string); + auto hotkey_it = string_to_hotkey_map.find(output_string); + auto axis_it = string_to_axis_map.find(output_string); + if (button_it != string_to_cbutton_map.end()) { connection = BindingConnection( binding, &*std::ranges::find(output_array, ControllerOutput(button_it->second))); connections.insert(connections.end(), connection); - + } else if (hotkey_it != string_to_hotkey_map.end()) { + connection = BindingConnection( + binding, &*std::ranges::find(output_array, ControllerOutput(hotkey_it->second))); + connections.insert(connections.end(), connection); } else if (axis_it != string_to_axis_map.end()) { int value_to_set = binding.keys[2].type == InputType::Axis ? 0 : axis_it->second.value; connection = BindingConnection( From b44ad1e087eb6131b6b169dfc22cc5df3194bd98 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Mon, 9 Feb 2026 00:15:11 +0800 Subject: [PATCH 15/20] Volume hotkey: show volume value, set game_specific arg correctly, clamp value (#4009) --- src/core/devtools/layer.cpp | 29 +++++++++++++++++++++++++++++ src/core/devtools/layer.h | 1 + src/core/emulator_state.cpp | 8 ++++++++ src/core/emulator_state.h | 5 ++++- src/emulator.cpp | 8 ++++++++ src/input/input_handler.cpp | 11 +++++++++-- 6 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index 928040fec..4be107713 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -32,6 +32,9 @@ static bool show_simple_fps = false; static bool visibility_toggled = false; static bool show_quit_window = false; +static bool show_volume = false; +static float volume_start_time; + static float fps_scale = 1.0f; static int dump_frame_count = 1; @@ -454,6 +457,27 @@ void L::Draw() { End(); } + if (show_volume) { + float current_time = ImGui::GetTime(); + + // Show volume for 3 seconds + if (current_time - volume_start_time >= 3.0) { + show_volume = false; + } else { + SetNextWindowPos(ImVec2(ImGui::GetMainViewport()->WorkPos.x + + ImGui::GetMainViewport()->WorkSize.x - 10, + ImGui::GetMainViewport()->WorkPos.y + 10), + ImGuiCond_Always, ImVec2(1.0f, 0.0f)); + + if (ImGui::Begin("Volume Window", &show_volume, + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) { + Text("Volume: %d", Config::getVolumeSlider()); + } + End(); + } + } + PopID(); } @@ -482,4 +506,9 @@ void ToggleQuitWindow() { show_quit_window = !show_quit_window; } +void ShowVolume() { + volume_start_time = ImGui::GetTime(); + show_volume = true; +} + } // namespace Overlay diff --git a/src/core/devtools/layer.h b/src/core/devtools/layer.h index 96b48a7f0..761135baf 100644 --- a/src/core/devtools/layer.h +++ b/src/core/devtools/layer.h @@ -32,5 +32,6 @@ namespace Overlay { void ToggleSimpleFps(); void SetSimpleFps(bool enabled); void ToggleQuitWindow(); +void ShowVolume(); } // namespace Overlay diff --git a/src/core/emulator_state.cpp b/src/core/emulator_state.cpp index 1f02043a3..20cffe53c 100644 --- a/src/core/emulator_state.cpp +++ b/src/core/emulator_state.cpp @@ -35,3 +35,11 @@ bool EmulatorState::IsAutoPatchesLoadEnabled() const { void EmulatorState::SetAutoPatchesLoadEnabled(bool enable) { m_load_patches_auto = enable; } + +bool EmulatorState::IsGameSpecifigConfigUsed() const { + return m_game_specific_config_used; +} + +void EmulatorState::SetGameSpecifigConfigUsed(bool used) { + m_game_specific_config_used = used; +} diff --git a/src/core/emulator_state.h b/src/core/emulator_state.h index c12af5401..0764b8a81 100644 --- a/src/core/emulator_state.h +++ b/src/core/emulator_state.h @@ -18,6 +18,8 @@ public: void SetGameRunning(bool running); bool IsAutoPatchesLoadEnabled() const; void SetAutoPatchesLoadEnabled(bool enable); + bool IsGameSpecifigConfigUsed() const; + void SetGameSpecifigConfigUsed(bool used); private: static std::shared_ptr s_instance; @@ -26,4 +28,5 @@ private: // state variables bool m_running = false; bool m_load_patches_auto = true; -}; \ No newline at end of file + bool m_game_specific_config_used = false; +}; diff --git a/src/emulator.cpp b/src/emulator.cpp index 6ba80b096..0dde0b7fa 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -28,6 +28,7 @@ #include "common/singleton.h" #include "core/debugger.h" #include "core/devtools/widget/module_list.h" +#include "core/emulator_state.h" #include "core/file_format/psf.h" #include "core/file_format/trp.h" #include "core/file_sys/fs.h" @@ -206,6 +207,13 @@ void Emulator::Run(std::filesystem::path file, std::vector args, Config::load(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (id + ".toml"), true); + if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / + (id + ".toml"))) { + EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(true); + } else { + EmulatorState::GetInstance()->SetGameSpecifigConfigUsed(false); + } + // Initialize logging as soon as possible if (!id.empty() && Config::getSeparateLogFilesEnabled()) { Common::Log::Initialize(id + ".log"); diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index 7c5263f8f..6e5014c1b 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -22,6 +22,8 @@ #include "common/elf_info.h" #include "common/io_file.h" #include "common/path_util.h" +#include "core/devtools/layer.h" +#include "core/emulator_state.h" #include "input/controller.h" #include "input/input_mouse.h" @@ -550,6 +552,7 @@ void ControllerOutput::FinalizeUpdate() { } old_button_state = new_button_state; old_param = *new_param; + bool is_game_specific = EmulatorState::GetInstance()->IsGameSpecifigConfigUsed(); if (button != SDL_GAMEPAD_BUTTON_INVALID) { switch (button) { case SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT: @@ -595,10 +598,14 @@ void ControllerOutput::FinalizeUpdate() { PushSDLEvent(SDL_EVENT_RDOC_CAPTURE); break; case HOTKEY_VOLUME_UP: - Config::setVolumeSlider(Config::getVolumeSlider() + 10, true); + Config::setVolumeSlider(std::clamp(Config::getVolumeSlider() + 10, 0, 500), + is_game_specific); + Overlay::ShowVolume(); break; case HOTKEY_VOLUME_DOWN: - Config::setVolumeSlider(Config::getVolumeSlider() - 10, true); + Config::setVolumeSlider(std::clamp(Config::getVolumeSlider() - 10, 0, 500), + is_game_specific); + Overlay::ShowVolume(); break; case HOTKEY_QUIT: PushSDLEvent(SDL_EVENT_QUIT_DIALOG); From 2a61851a8865c9fef15c78f7b06fc7417f7018ac Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:07:17 -0600 Subject: [PATCH 16/20] Kernel.Process: Implement sceKernelGetModuleInfo2, sceKernelGetModuleList2 (#4001) * twos * Fixes Still can't test properly, but this seems to hide system libs, which I'm pretty sure is the necessary difference here. * Clang * Extra export for sceKernelGetModuleInfo2 --- src/core/libraries/kernel/process.cpp | 48 +++++++++++++++++++++++++++ src/core/module.h | 9 +++++ 2 files changed, 57 insertions(+) diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index 4ea7fa062..31cc9a81b 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -191,6 +191,26 @@ s32 PS4_SYSV_ABI sceKernelGetModuleInfo(s32 handle, Core::OrbisKernelModuleInfo* return ORBIS_OK; } +s32 PS4_SYSV_ABI sceKernelGetModuleInfo2(s32 handle, Core::OrbisKernelModuleInfo* info) { + if (info == nullptr) { + return ORBIS_KERNEL_ERROR_EFAULT; + } + if (info->st_size != sizeof(Core::OrbisKernelModuleInfo)) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + + auto* linker = Common::Singleton::Instance(); + auto* module = linker->GetModule(handle); + if (module == nullptr) { + return ORBIS_KERNEL_ERROR_ESRCH; + } + if (module->IsSystemLib()) { + return ORBIS_KERNEL_ERROR_EPERM; + } + *info = module->GetModuleInfo(); + return ORBIS_OK; +} + s32 PS4_SYSV_ABI sceKernelGetModuleInfoInternal(s32 handle, Core::OrbisKernelModuleInfoEx* info) { if (info == nullptr) { return ORBIS_KERNEL_ERROR_EFAULT; @@ -230,6 +250,31 @@ s32 PS4_SYSV_ABI sceKernelGetModuleList(s32* handles, u64 num_array, u64* out_co return ORBIS_OK; } +s32 PS4_SYSV_ABI sceKernelGetModuleList2(s32* handles, u64 num_array, u64* out_count) { + if (handles == nullptr || out_count == nullptr) { + return ORBIS_KERNEL_ERROR_EFAULT; + } + + auto* linker = Common::Singleton::Instance(); + u64 id = 0; + u64 index = 0; + auto* module = linker->GetModule(id); + while (module != nullptr && index < num_array) { + if (!module->IsSystemLib()) { + handles[index++] = id; + } + id++; + module = linker->GetModule(id); + } + + if (index == num_array && module != nullptr) { + return ORBIS_KERNEL_ERROR_ENOMEM; + } + + *out_count = index; + return ORBIS_OK; +} + s32 PS4_SYSV_ABI exit(s32 status) { UNREACHABLE_MSG("Exiting with status code {}", status); return 0; @@ -249,8 +294,11 @@ void RegisterProcess(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("RpQJJVKTiFM", "libkernel", 1, "libkernel", sceKernelGetModuleInfoForUnwind); LIB_FUNCTION("f7KBOafysXo", "libkernel", 1, "libkernel", sceKernelGetModuleInfoFromAddr); LIB_FUNCTION("kUpgrXIrz7Q", "libkernel", 1, "libkernel", sceKernelGetModuleInfo); + LIB_FUNCTION("QgsKEUfkqMA", "libkernel", 1, "libkernel", sceKernelGetModuleInfo2); + LIB_FUNCTION("QgsKEUfkqMA", "libkernel_module_info", 1, "libkernel", sceKernelGetModuleInfo2); LIB_FUNCTION("HZO7xOos4xc", "libkernel", 1, "libkernel", sceKernelGetModuleInfoInternal); LIB_FUNCTION("IuxnUuXk6Bg", "libkernel", 1, "libkernel", sceKernelGetModuleList); + LIB_FUNCTION("ZzzC3ZGVAkc", "libkernel", 1, "libkernel", sceKernelGetModuleList2); LIB_FUNCTION("6Z83sYWFlA8", "libkernel", 1, "libkernel", exit); } diff --git a/src/core/module.h b/src/core/module.h index c39310406..778344e33 100644 --- a/src/core/module.h +++ b/src/core/module.h @@ -5,6 +5,7 @@ #include #include +#include "common/config.h" #include "common/types.h" #include "core/loader/elf.h" #include "core/loader/symbols_resolver.h" @@ -164,6 +165,14 @@ public: return elf.IsSharedLib(); } + bool IsSystemLib() { + auto system_path = Config::getSysModulesPath(); + if (file.string().starts_with(system_path.string().c_str())) { + return true; + } + return false; + } + template T GetProcParam() const noexcept { return reinterpret_cast(proc_param_virtual_addr); From afc98937157262e32c8abf2ce372c3112fd44da9 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sun, 8 Feb 2026 22:39:36 +0200 Subject: [PATCH 17/20] Added sceAudioOutGetSystemState (#4011) --- src/core/libraries/audio/audioout.cpp | 30 +++++++++++++++++---------- src/core/libraries/audio/audioout.h | 8 ++++++- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/core/libraries/audio/audioout.cpp b/src/core/libraries/audio/audioout.cpp index 6c58df94f..100ddd51c 100644 --- a/src/core/libraries/audio/audioout.cpp +++ b/src/core/libraries/audio/audioout.cpp @@ -496,11 +496,11 @@ s32 PS4_SYSV_ABI sceAudioOutGetPortState(s32 handle, OrbisAudioOutPortState* sta state->rerouteCounter = 0; state->flag = 0; - LOG_INFO(Lib_AudioOut, - "called, handle={:#x}, state={}, output={}, channel={}, volume={}, " - "rerouteCounter={}, flag={}", - handle, fmt::ptr(state), state->output, state->channel, state->volume, - state->rerouteCounter, state->flag); + LOG_DEBUG(Lib_AudioOut, + "called, handle={:#x}, state={}, output={}, channel={}, volume={}, " + "rerouteCounter={}, flag={}", + handle, fmt::ptr(state), state->output, state->channel, state->volume, + state->rerouteCounter, state->flag); return ORBIS_OK; } @@ -729,7 +729,7 @@ s32 PS4_SYSV_ABI sceAudioOutSetVolume(s32 handle, s32 flag, s32* vol) { } s32 PS4_SYSV_ABI sceAudioOutSetMixLevelPadSpk(s32 handle, s32 mixLevel) { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); + LOG_INFO(Lib_AudioOut, "(STUBBED) called"); if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { LOG_ERROR(Lib_AudioOut, "audio is not init"); return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; @@ -769,6 +769,19 @@ s32 PS4_SYSV_ABI sceAudioOutSetMixLevelPadSpk(s32 handle, s32 mixLevel) { return ORBIS_OK; } +s32 PS4_SYSV_ABI sceAudioOutGetSystemState(OrbisAudioOutSystemState* state) { + if (lazy_init.load(std::memory_order_relaxed) == 0 || audio == nullptr) { + LOG_ERROR(Lib_AudioOut, "audio is not init"); + return ORBIS_AUDIO_OUT_ERROR_NOT_INIT; + } + if (state == nullptr) { + return ORBIS_AUDIO_OUT_ERROR_INVALID_POINTER; + } + memset(state, 0, sizeof(*state)); + LOG_DEBUG(Lib_AudioOut, "called"); + return ORBIS_OK; +} + /* * Stubbed functions **/ @@ -892,11 +905,6 @@ s32 PS4_SYSV_ABI sceAudioOutGetSparkVss() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceAudioOutGetSystemState() { - LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); - return ORBIS_OK; -} - s32 PS4_SYSV_ABI sceAudioOutInitIpmiGetSession() { LOG_ERROR(Lib_AudioOut, "(STUBBED) called"); return ORBIS_OK; diff --git a/src/core/libraries/audio/audioout.h b/src/core/libraries/audio/audioout.h index 93db8150f..ffc1f9eb1 100644 --- a/src/core/libraries/audio/audioout.h +++ b/src/core/libraries/audio/audioout.h @@ -97,6 +97,12 @@ struct OrbisAudioOutPortState { u64 reserved64[2]; }; +struct OrbisAudioOutSystemState { + float loudness; + u8 reserved8[4]; + u64 reserved64[3]; +}; + struct AudioFormatInfo { bool is_float; u8 sample_size; @@ -162,7 +168,7 @@ s32 PS4_SYSV_ABI sceAudioOutGetSimulatedBusUsableStatusByBusType(); s32 PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo(); s32 PS4_SYSV_ABI sceAudioOutGetSimulatedHandleStatusInfo2(); s32 PS4_SYSV_ABI sceAudioOutGetSparkVss(); -s32 PS4_SYSV_ABI sceAudioOutGetSystemState(); +s32 PS4_SYSV_ABI sceAudioOutGetSystemState(OrbisAudioOutSystemState* state); s32 PS4_SYSV_ABI sceAudioOutInit(); s32 PS4_SYSV_ABI sceAudioOutInitIpmiGetSession(); s32 PS4_SYSV_ABI sceAudioOutMasteringGetState(); From 42f2697b500f251b2268c7e46707276e586a033f Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:40:47 +0100 Subject: [PATCH 18/20] Fix deadlock from missed unlock call after #3946 (#4013) * Fix deadlock from missed unlock call after #3946 * copyright 2026 * Add the same fix to PoolCommit --- src/core/memory.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 561e72617..9d26142ce 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/alignment.h" @@ -73,7 +73,7 @@ void MemoryManager::SetupMemoryRegions(u64 flexible_size, bool use_extended_mem1 } u64 MemoryManager::ClampRangeSize(VAddr virtual_addr, u64 size) { - static constexpr u64 MinSizeToClamp = 3_GB; + static constexpr u64 MinSizeToClamp = 1_GB; // Dont bother with clamping if the size is small so we dont pay a map lookup on every buffer. if (size < MinSizeToClamp) { return size; @@ -349,7 +349,8 @@ s32 MemoryManager::Free(PAddr phys_addr, u64 size, bool is_checked) { } s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 mtype) { - std::scoped_lock lk{mutex, unmap_mutex}; + std::scoped_lock lk{unmap_mutex}; + std::unique_lock lk2{mutex}; ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); @@ -434,6 +435,7 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 // Merge this VMA with similar nearby areas MergeAdjacent(vma_map, new_vma_handle); + lk2.unlock(); if (IsValidGpuMapping(mapped_addr, size)) { rasterizer->MapMemory(mapped_addr, size); } @@ -554,7 +556,7 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo } // Acquire writer lock. - std::scoped_lock lk2{mutex}; + std::unique_lock lk2{mutex}; // Create VMA representing this mapping. auto new_vma_handle = CreateArea(virtual_addr, size, prot, flags, type, name, alignment); @@ -650,6 +652,8 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo // TRACK_ALLOC(mapped_addr, size, "VMEM"); } + lk2.unlock(); + // If this is not a reservation, then map to GPU and address space if (IsValidGpuMapping(mapped_addr, size)) { rasterizer->MapMemory(mapped_addr, size); From ffae535a5ca879f7f543335adbd9035340380702 Mon Sep 17 00:00:00 2001 From: Niram7777 Date: Mon, 9 Feb 2026 18:19:39 +0000 Subject: [PATCH 19/20] [LOG] group same lines with counter (#4010) * [LOG] group same lines with counter * Log in single line counter * Protect log singleton from ps4 threads * Log always compact --- src/common/logging/backend.cpp | 50 ++++++++++++++++++++++++++++------ src/common/logging/log_entry.h | 1 + 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index d7c816da3..168350b96 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -208,26 +209,41 @@ public: } } + std::unique_lock entry_loc(_mutex); + + if (_last_entry.message == message) { + ++_last_entry.counter; + return; + } + + if (_last_entry.counter >= 2) { + _last_entry.message += " x" + std::to_string(_last_entry.counter); + } + + if (_last_entry.counter >= 1) { + if (Config::getLogType() == "async") { + message_queue.EmplaceWait(_last_entry); + } else { + ForEachBackend([this](auto& backend) { backend.Write(this->_last_entry); }); + std::fflush(stdout); + } + } + using std::chrono::duration_cast; using std::chrono::microseconds; using std::chrono::steady_clock; - const Entry entry = { + this->_last_entry = { .timestamp = duration_cast(steady_clock::now() - time_origin), .log_class = log_class, .log_level = log_level, .filename = filename, .line_num = line_num, .function = function, - .message = std::move(message), + .message = message, .thread = Common::GetCurrentThreadName(), + .counter = 1, }; - if (Config::getLogType() == "async") { - message_queue.EmplaceWait(entry); - } else { - ForEachBackend([&entry](auto& backend) { backend.Write(entry); }); - std::fflush(stdout); - } } private: @@ -259,6 +275,22 @@ private: } void StopBackendThread() { + // 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") { + message_queue.EmplaceWait(_last_entry); + } else { + ForEachBackend([this](auto& backend) { backend.Write(this->_last_entry); }); + std::fflush(stdout); + } + } + + this->_last_entry = {}; + backend_thread.request_stop(); if (backend_thread.joinable()) { backend_thread.join(); @@ -292,6 +324,8 @@ private: MPSCQueue message_queue{}; std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()}; std::jthread backend_thread; + Entry _last_entry; + std::mutex _mutex; }; } // namespace diff --git a/src/common/logging/log_entry.h b/src/common/logging/log_entry.h index 6c529f878..7b52ad7e1 100644 --- a/src/common/logging/log_entry.h +++ b/src/common/logging/log_entry.h @@ -22,6 +22,7 @@ struct Entry { std::string function; std::string message; std::string thread; + u32 counter = 0; }; } // namespace Common::Log From a706b325f42e362eb1f41a527d9967a3b38ef323 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Mon, 9 Feb 2026 22:59:39 +0200 Subject: [PATCH 20/20] optimize sdl3 audio out (#4015) --- src/core/libraries/audio/sdl_audio_out.cpp | 585 +++++++++++++-------- 1 file changed, 365 insertions(+), 220 deletions(-) diff --git a/src/core/libraries/audio/sdl_audio_out.cpp b/src/core/libraries/audio/sdl_audio_out.cpp index 572525c85..ce2598759 100644 --- a/src/core/libraries/audio/sdl_audio_out.cpp +++ b/src/core/libraries/audio/sdl_audio_out.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -14,15 +15,32 @@ #include "core/libraries/audio/audioout_backend.h" #include "core/libraries/kernel/threads.h" +// SIMD support detection +#if defined(__x86_64__) || defined(_M_X64) +#include +#define HAS_SSE2 +#endif + #define SDL_INVALID_AUDIODEVICEID 0 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 + +// Queue management +constexpr u32 QUEUE_MULTIPLIER = 4; +// Memory alignment for SIMD +constexpr size_t AUDIO_BUFFER_ALIGNMENT = 32; // Channel positions -enum ChannelPos { +enum ChannelPos : u8 { FL = 0, FR = 1, FC = 2, @@ -45,187 +63,210 @@ public: 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) { - // Calculate timing - period_us = (1000000ULL * buffer_frames + sample_rate / 2) / sample_rate; - last_output_time = 0; - next_output_time = 0; - - // Allocate internal buffer - internal_buffer_size = buffer_frames * sizeof(float) * num_channels; - internal_buffer = std::malloc(internal_buffer_size); - if (!internal_buffer) { - LOG_ERROR(Lib_AudioOut, "Failed to allocate internal audio buffer"); - return; + if (!Initialize(port.type)) { + LOG_ERROR(Lib_AudioOut, "Failed to initialize SDL audio backend"); } - - // Initialize current gain - current_gain.store(Config::getVolumeSlider() / 100.0f); - - // Select converter function - SelectConverter(); - - // Open SDL device - if (!OpenDevice(port.type)) { - std::free(internal_buffer); - internal_buffer = nullptr; - return; - } - - CalculateQueueThreshold(); } ~SDLPortBackend() override { - if (stream) { - SDL_DestroyAudioStream(stream); - } - if (internal_buffer) { - std::free(internal_buffer); - } + Cleanup(); } void Output(void* ptr) override { - if (!stream || !internal_buffer) { + if (!stream || !internal_buffer || !convert) [[unlikely]] { return; } - // Check for volume changes and update if needed - UpdateVolumeIfChanged(); - - // Get current time in microseconds - u64 current_time = Kernel::sceKernelGetProcessTime(); - - if (ptr != nullptr) { - // Simple format conversion (no volume application) - convert(ptr, internal_buffer, buffer_frames, nullptr); - - if (next_output_time == 0) { - next_output_time = current_time + period_us; - } else if (current_time > next_output_time) { - next_output_time = current_time + period_us; - } else { - u64 wait_until = next_output_time; - next_output_time += period_us; - - if (current_time < wait_until) { - u64 sleep_us = wait_until - current_time; - if (sleep_us > 10) { - sleep_us -= 10; - std::this_thread::sleep_for(std::chrono::microseconds(sleep_us)); - } - } - } - - last_output_time = current_time; - - // Check queue and clear if backed up - if (const auto queued = SDL_GetAudioStreamQueued(stream); queued >= queue_threshold) { - LOG_DEBUG(Lib_AudioOut, "Clearing backed up audio queue ({} >= {})", queued, - queue_threshold); - SDL_ClearAudioStream(stream); - CalculateQueueThreshold(); - } - - if (!SDL_PutAudioStreamData(stream, internal_buffer, internal_buffer_size)) { - LOG_ERROR(Lib_AudioOut, "Failed to output to SDL audio stream: {}", SDL_GetError()); - } + if (ptr == nullptr) [[unlikely]] { + return; } + + UpdateVolumeIfChanged(); + const u64 current_time = Kernel::sceKernelGetProcessTime(); + convert(ptr, internal_buffer, buffer_frames, nullptr); + HandleTiming(current_time); + + if ((output_count++ & 0xF) == 0) { // Check every 16 outputs + ManageAudioQueue(); + } + + if (!SDL_PutAudioStreamData(stream, internal_buffer, internal_buffer_size)) [[unlikely]] { + LOG_ERROR(Lib_AudioOut, "Failed to output to SDL audio stream: {}", SDL_GetError()); + } + + last_output_time.store(current_time, std::memory_order_release); } void SetVolume(const std::array& ch_volumes) override { - if (!stream) { + if (!stream) [[unlikely]] { return; } + float max_channel_gain = 0.0f; - for (int i = 0; i < num_channels && i < 8; i++) { - float channel_gain = static_cast(ch_volumes[i]) / VOLUME_0DB; + const u32 channels_to_check = std::min(num_channels, 8u); + + for (u32 i = 0; i < channels_to_check; i++) { + const float channel_gain = static_cast(ch_volumes[i]) * INV_VOLUME_0DB; max_channel_gain = std::max(max_channel_gain, channel_gain); } - // Combine with global volume slider - float total_gain = max_channel_gain * (Config::getVolumeSlider() / 100.0f); + const float slider_gain = Config::getVolumeSlider() * 0.01f; // Faster than /100.0f + const float total_gain = max_channel_gain * slider_gain; - std::lock_guard lock(volume_mutex); + const float current = current_gain.load(std::memory_order_acquire); + if (std::abs(total_gain - current) < VOLUME_EPSILON) { + return; + } + + // Apply volume change if (SDL_SetAudioStreamGain(stream, total_gain)) { - current_gain.store(total_gain); + 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, Config::getVolumeSlider() / 100.0f); + total_gain, max_channel_gain, slider_gain); } else { LOG_ERROR(Lib_AudioOut, "Failed to set audio stream gain: {}", SDL_GetError()); } } u64 GetLastOutputTime() const { - return last_output_time; + return last_output_time.load(std::memory_order_acquire); } private: - std::atomic volume_update_needed{false}; - u64 last_volume_check_time{0}; - static constexpr u64 VOLUME_CHECK_INTERVAL_US = 50000; // Check every 50ms + bool Initialize(OrbisAudioOutPort type) { + // Calculate timing parameters + period_us = (1000000ULL * buffer_frames + sample_rate / 2) / sample_rate; + + // Allocate aligned internal buffer for SIMD operations + internal_buffer_size = buffer_frames * sizeof(float) * num_channels; + +#ifdef _WIN32 + internal_buffer = _aligned_malloc(internal_buffer_size, AUDIO_BUFFER_ALIGNMENT); +#else + if (posix_memalign(&internal_buffer, AUDIO_BUFFER_ALIGNMENT, internal_buffer_size) != 0) { + internal_buffer = nullptr; + } +#endif + + if (!internal_buffer) { + LOG_ERROR(Lib_AudioOut, "Failed to allocate aligned audio buffer of size {}", + internal_buffer_size); + return false; + } + + // Initialize current gain + current_gain.store(Config::getVolumeSlider() * 0.01f, std::memory_order_relaxed); + + if (!SelectConverter()) { + FreeAlignedBuffer(); + return false; + } + + // Open SDL device + if (!OpenDevice(type)) { + FreeAlignedBuffer(); + return false; + } + + CalculateQueueThreshold(); + return true; + } + + void Cleanup() { + if (stream) { + SDL_DestroyAudioStream(stream); + stream = nullptr; + } + FreeAlignedBuffer(); + } + + void FreeAlignedBuffer() { + if (internal_buffer) { +#ifdef _WIN32 + _aligned_free(internal_buffer); +#else + free(internal_buffer); +#endif + internal_buffer = nullptr; + } + } void UpdateVolumeIfChanged() { - u64 current_time = Kernel::sceKernelGetProcessTime(); + const u64 current_time = Kernel::sceKernelGetProcessTime(); - // Only check volume every 50ms to reduce overhead - if (current_time - last_volume_check_time >= VOLUME_CHECK_INTERVAL_US) { - last_volume_check_time = current_time; + if (current_time - last_volume_check_time < VOLUME_CHECK_INTERVAL_US) { + return; + } - float config_volume = Config::getVolumeSlider() / 100.0f; - float stored_gain = current_gain.load(); + last_volume_check_time = current_time; - if (std::abs(config_volume - stored_gain) > 0.001f) { - if (SDL_SetAudioStreamGain(stream, config_volume)) { - current_gain.store(config_volume); - LOG_DEBUG(Lib_AudioOut, "Updated audio gain to {:.3f}", config_volume); - } else { - LOG_ERROR(Lib_AudioOut, "Failed to set audio stream gain: {}", SDL_GetError()); - } + const float config_volume = Config::getVolumeSlider() * 0.01f; + const float stored_gain = current_gain.load(std::memory_order_acquire); + + // Only update if the difference is significant + if (std::abs(config_volume - stored_gain) > VOLUME_EPSILON) { + if (SDL_SetAudioStreamGain(stream, config_volume)) { + 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 stream gain: {}", SDL_GetError()); } } } + + void HandleTiming(u64 current_time) { + if (next_output_time == 0) [[unlikely]] { + // First output - set initial timing + next_output_time = current_time + period_us; + return; + } + + const s64 time_diff = static_cast(current_time - next_output_time); + + if (time_diff > static_cast(TIMING_RESYNC_THRESHOLD_US)) [[unlikely]] { + // We're far behind - resync + next_output_time = current_time + period_us; + } else if (time_diff < 0) { + // We're ahead of schedule - wait + const u64 time_to_wait = static_cast(-time_diff); + next_output_time += period_us; + + if (time_to_wait > MIN_SLEEP_THRESHOLD_US) { + // Sleep for most of the wait period + const u64 sleep_duration = time_to_wait - MIN_SLEEP_THRESHOLD_US; + std::this_thread::sleep_for(std::chrono::microseconds(sleep_duration)); + } + } else { + // Slightly behind or on time - just advance + next_output_time += period_us; + } + } + + void ManageAudioQueue() { + const auto queued = SDL_GetAudioStreamQueued(stream); + + if (queued >= queue_threshold) [[unlikely]] { + LOG_DEBUG(Lib_AudioOut, "Clearing backed up audio queue ({} >= {})", queued, + queue_threshold); + SDL_ClearAudioStream(stream); + CalculateQueueThreshold(); + } + } + bool OpenDevice(OrbisAudioOutPort type) { const SDL_AudioSpec fmt = { - .format = SDL_AUDIO_F32LE, // Always use float for internal processing + .format = SDL_AUDIO_F32LE, .channels = static_cast(num_channels), .freq = static_cast(sample_rate), }; - // Determine device name - std::string device_name = GetDeviceName(type); - SDL_AudioDeviceID dev_id = SDL_INVALID_AUDIODEVICEID; + // Determine device + const std::string device_name = GetDeviceName(type); + const SDL_AudioDeviceID dev_id = SelectAudioDevice(device_name, type); - if (device_name == "None") { - LOG_INFO(Lib_AudioOut, "Audio device disabled for port type {}", - static_cast(type)); + if (dev_id == SDL_INVALID_AUDIODEVICEID) { return false; - } else if (device_name.empty() || device_name == "Default Device") { - dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; - } else { - int num_devices = 0; - SDL_AudioDeviceID* dev_array = SDL_GetAudioPlaybackDevices(&num_devices); - - if (dev_array) { - bool found = false; - for (int i = 0; i < num_devices; i++) { - const char* dev_name = SDL_GetAudioDeviceName(dev_array[i]); - if (dev_name && std::string(dev_name) == device_name) { - dev_id = dev_array[i]; - found = true; - break; - } - } - SDL_free(dev_array); - - if (!found) { - LOG_WARNING(Lib_AudioOut, "Audio device '{}' not found, using default", - device_name); - dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; - } - } else { - LOG_WARNING(Lib_AudioOut, "No audio devices found, using default"); - dev_id = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; - } } // Create audio stream @@ -235,30 +276,15 @@ private: return false; } - // Set channel map - if (num_channels > 0) { - std::vector channel_map(num_channels); - - if (is_std && num_channels == 8) { - // Standard 8CH layout - channel_map = {FL, FR, FC, LF, STD_SL, STD_SR, STD_BL, STD_BR}; - } else { - // Use provided channel layout - for (int i = 0; i < num_channels; i++) { - channel_map[i] = channel_layout[i]; - } - } - - if (!SDL_SetAudioStreamInputChannelMap(stream, channel_map.data(), num_channels)) { - LOG_ERROR(Lib_AudioOut, "Failed to set channel map: {}", SDL_GetError()); - SDL_DestroyAudioStream(stream); - stream = nullptr; - return false; - } + // Configure channel mapping + if (!ConfigureChannelMap()) { + SDL_DestroyAudioStream(stream); + stream = nullptr; + return false; } // Set initial volume - float initial_gain = current_gain.load(); + const float initial_gain = current_gain.load(std::memory_order_relaxed); if (!SDL_SetAudioStreamGain(stream, initial_gain)) { LOG_WARNING(Lib_AudioOut, "Failed to set initial audio gain: {}", SDL_GetError()); } @@ -276,24 +302,81 @@ private: return true; } - std::string GetDeviceName(OrbisAudioOutPort type) { + SDL_AudioDeviceID SelectAudioDevice(const std::string& device_name, OrbisAudioOutPort type) { + if (device_name == "None") { + LOG_INFO(Lib_AudioOut, "Audio device disabled for port type {}", + static_cast(type)); + return SDL_INVALID_AUDIODEVICEID; + } + + if (device_name.empty() || device_name == "Default Device") { + return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; + } + + // Search for specific device + int num_devices = 0; + SDL_AudioDeviceID* dev_array = SDL_GetAudioPlaybackDevices(&num_devices); + + if (!dev_array) { + LOG_WARNING(Lib_AudioOut, "No audio devices found, using default"); + return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; + } + + SDL_AudioDeviceID selected_device = SDL_INVALID_AUDIODEVICEID; + + for (int i = 0; i < num_devices; i++) { + const char* dev_name = SDL_GetAudioDeviceName(dev_array[i]); + if (dev_name && device_name == dev_name) { + selected_device = dev_array[i]; + break; + } + } + + SDL_free(dev_array); + + if (selected_device == SDL_INVALID_AUDIODEVICEID) { + LOG_WARNING(Lib_AudioOut, "Audio device '{}' not found, using default", device_name); + return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; + } + + return selected_device; + } + + bool ConfigureChannelMap() { + if (num_channels == 0) { + return true; + } + + std::vector channel_map(num_channels); + + if (is_std && num_channels == 8) { + // Standard 8CH layout requires remapping + channel_map = {FL, FR, FC, LF, STD_SL, STD_SR, STD_BL, STD_BR}; + } else { + std::copy_n(channel_layout.begin(), num_channels, channel_map.begin()); + } + + if (!SDL_SetAudioStreamInputChannelMap(stream, channel_map.data(), num_channels)) { + LOG_ERROR(Lib_AudioOut, "Failed to set channel map: {}", SDL_GetError()); + return false; + } + + return true; + } + + std::string GetDeviceName(OrbisAudioOutPort type) const { switch (type) { case OrbisAudioOutPort::Main: case OrbisAudioOutPort::Bgm: return Config::getMainOutputDevice(); - // case OrbisAudioOutPort::Voice: - // case OrbisAudioOutPort::Personal: - // return Config::getHeadphoneOutputDevice(); case OrbisAudioOutPort::PadSpk: return Config::getPadSpkOutputDevice(); - // case OrbisAudioOutPort::Aux: - // return Config::getSpecialOutputDevice(); default: return Config::getMainOutputDevice(); } } - void SelectConverter() { + bool SelectConverter() { if (is_float) { switch (num_channels) { case 1: @@ -303,15 +386,11 @@ private: convert = &ConvertF32Stereo; break; case 8: - if (is_std) { - convert = &ConvertF32Std8CH; - } else { - convert = &ConvertF32_8CH; - } + convert = is_std ? &ConvertF32Std8CH : &ConvertF32_8CH; break; default: LOG_ERROR(Lib_AudioOut, "Unsupported float channel count: {}", num_channels); - convert = nullptr; + return false; } } else { switch (num_channels) { @@ -319,32 +398,43 @@ private: convert = &ConvertS16Mono; break; case 2: +#if defined(HAS_SSE2) + convert = &ConvertS16StereoSIMD; +#else convert = &ConvertS16Stereo; +#endif break; case 8: +#if defined(HAS_SSE2) + convert = &ConvertS16_8CH_SIMD; +#else convert = &ConvertS16_8CH; +#endif break; default: LOG_ERROR(Lib_AudioOut, "Unsupported S16 channel count: {}", num_channels); - convert = nullptr; + return false; } } + + return true; } void CalculateQueueThreshold() { - if (!stream) + if (!stream) { return; + } SDL_AudioSpec discard; - int sdl_buffer_frames; + int sdl_buffer_frames = 0; + if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(stream), &discard, &sdl_buffer_frames)) { LOG_WARNING(Lib_AudioOut, "Failed to get SDL buffer size: {}", SDL_GetError()); - sdl_buffer_frames = 0; } - u32 sdl_buffer_size = sdl_buffer_frames * sizeof(float) * num_channels; - queue_threshold = std::max(guest_buffer_size, sdl_buffer_size) * 4; + const u32 sdl_buffer_size = sdl_buffer_frames * sizeof(float) * num_channels; + queue_threshold = std::max(guest_buffer_size, sdl_buffer_size) * QUEUE_MULTIPLIER; LOG_DEBUG(Lib_AudioOut, "Audio queue threshold: {} bytes (SDL buffer: {} frames)", queue_threshold, sdl_buffer_frames); @@ -352,15 +442,12 @@ private: using ConverterFunc = void (*)(const void* src, void* dst, u32 frames, const float* volumes); - // Remove volume parameter and application from all converters static void ConvertS16Mono(const void* src, void* dst, u32 frames, const float*) { const s16* s = static_cast(src); float* d = static_cast(dst); - constexpr float inv_scale = 1.0f / VOLUME_0DB; - for (u32 i = 0; i < frames; i++) { - d[i] = s[i] * inv_scale; + d[i] = s[i] * INV_VOLUME_0DB; } } @@ -368,28 +455,82 @@ private: const s16* s = static_cast(src); float* d = static_cast(dst); - constexpr float inv_scale = 1.0f / VOLUME_0DB; - - for (u32 i = 0; i < frames; i++) { - d[i * 2] = s[i * 2] * inv_scale; - d[i * 2 + 1] = s[i * 2 + 1] * inv_scale; + const u32 num_samples = frames << 1; // * 2 + for (u32 i = 0; i < num_samples; i++) { + d[i] = s[i] * INV_VOLUME_0DB; } } +#ifdef HAS_SSE2 + static void ConvertS16StereoSIMD(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(INV_VOLUME_0DB); + const u32 num_samples = frames << 1; + u32 i = 0; + + // Process 8 samples at a time (4 stereo frames) + for (; i + 8 <= num_samples; i += 8) { + // Load 8 s16 values + __m128i s16_vals = _mm_loadu_si128(reinterpret_cast(&s[i])); + + // Convert to 32-bit integers + __m128i s32_lo = _mm_cvtepi16_epi32(s16_vals); + __m128i s32_hi = _mm_cvtepi16_epi32(_mm_srli_si128(s16_vals, 8)); + + // Convert to float and scale + __m128 f_lo = _mm_mul_ps(_mm_cvtepi32_ps(s32_lo), scale); + __m128 f_hi = _mm_mul_ps(_mm_cvtepi32_ps(s32_hi), scale); + + // Store results + _mm_storeu_ps(&d[i], f_lo); + _mm_storeu_ps(&d[i + 4], f_hi); + } + + // Handle remaining samples + for (; i < num_samples; i++) { + d[i] = s[i] * INV_VOLUME_0DB; + } + } +#endif + static void ConvertS16_8CH(const void* src, void* dst, u32 frames, const float*) { const s16* s = static_cast(src); float* d = static_cast(dst); - constexpr float inv_scale = 1.0f / VOLUME_0DB; - - for (u32 i = 0; i < frames; i++) { - for (int ch = 0; ch < 8; ch++) { - d[i * 8 + ch] = s[i * 8 + ch] * inv_scale; - } + const u32 num_samples = frames << 3; // * 8 + for (u32 i = 0; i < num_samples; i++) { + d[i] = s[i] * INV_VOLUME_0DB; } } - // Float converters become simple memcpy or passthrough +#ifdef HAS_SSE2 + static void ConvertS16_8CH_SIMD(const void* src, void* dst, u32 frames, const float*) { + const s16* s = static_cast(src); + float* d = static_cast(dst); + + const __m128 scale = _mm_set1_ps(INV_VOLUME_0DB); + const u32 num_samples = frames << 3; + u32 i = 0; + + // Process 8 samples at a time + for (; i + 8 <= num_samples; i += 8) { + __m128i s16_vals = _mm_loadu_si128(reinterpret_cast(&s[i])); + __m128i s32_lo = _mm_cvtepi16_epi32(s16_vals); + __m128i s32_hi = _mm_cvtepi16_epi32(_mm_srli_si128(s16_vals, 8)); + __m128 f_lo = _mm_mul_ps(_mm_cvtepi32_ps(s32_lo), scale); + __m128 f_hi = _mm_mul_ps(_mm_cvtepi32_ps(s32_hi), scale); + _mm_storeu_ps(&d[i], f_lo); + _mm_storeu_ps(&d[i + 4], f_hi); + } + + for (; i < num_samples; i++) { + d[i] = s[i] * INV_VOLUME_0DB; + } + } +#endif + static void ConvertF32Mono(const void* src, void* dst, u32 frames, const float*) { std::memcpy(dst, src, frames * sizeof(float)); } @@ -406,50 +547,54 @@ private: const float* s = static_cast(src); float* d = static_cast(dst); + // Channel remapping for standard 8CH layout for (u32 i = 0; i < frames; i++) { - d[i * 8 + FL] = s[i * 8 + FL]; - d[i * 8 + FR] = s[i * 8 + FR]; - d[i * 8 + FC] = s[i * 8 + FC]; - d[i * 8 + LF] = s[i * 8 + LF]; - d[i * 8 + SL] = s[i * 8 + STD_SL]; // Channel remapping still needed - d[i * 8 + SR] = s[i * 8 + STD_SR]; - d[i * 8 + BL] = s[i * 8 + STD_BL]; - d[i * 8 + BR] = s[i * 8 + STD_BR]; + const u32 offset = i << 3; // * 8 + + 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]; } } - // Member variables - u32 frame_size; - u32 guest_buffer_size; - u32 buffer_frames; - u32 sample_rate; - u32 num_channels; - bool is_float; - bool is_std; - std::array channel_layout; + // Audio format parameters + const u32 frame_size; + const u32 guest_buffer_size; + const u32 buffer_frames; + const u32 sample_rate; + const u32 num_channels; + const bool is_float; + const bool is_std; + const std::array channel_layout; - u64 period_us; - u64 last_output_time; - u64 next_output_time; + alignas(64) u64 period_us{0}; + alignas(64) std::atomic last_output_time{0}; + u64 next_output_time{0}; + u64 last_volume_check_time{0}; + u32 output_count{0}; // Buffers - u32 internal_buffer_size; - void* internal_buffer; + u32 internal_buffer_size{0}; + void* internal_buffer{nullptr}; - // Converter function - ConverterFunc convert; + // Converter function pointer + ConverterFunc convert{nullptr}; - // Volume tracking - std::atomic current_gain{1.0f}; - mutable std::mutex volume_mutex; + // Volume management + alignas(64) std::atomic current_gain{1.0f}; - // SDL - SDL_AudioStream* stream{}; - u32 queue_threshold{}; + // SDL audio stream + SDL_AudioStream* stream{nullptr}; + u32 queue_threshold{0}; }; std::unique_ptr SDLAudioOut::Open(PortOut& port) { return std::make_unique(port); } -} // namespace Libraries::AudioOut \ No newline at end of file +} // namespace Libraries::AudioOut