From 4ba0e6267015dba446509526d373a11fb6cba9c2 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 27 Jan 2026 04:25:23 -0600 Subject: [PATCH 01/50] Kernel.Vmm: Attempt to address race conditions involving ClampRangeSize, CopySparseMemory, and TryWriteBacking (#3956) * no no * Adjust locking strategy Use a separate mutex for the initial error checks + GPU unmap instead of using the reader lock. Make sure all writers lock this separate mutex, and for those that don't perform GPU unmaps, lock the writer lock immediately too. This gets around every race condition I've envisioned so far, and hopefully does the trick? * Clang * Always GPU unmap GPU unmaps have logic built-in to only run on mapped areas. Not sure if userfaultfd would work with this, but since that's already broken anyway, I'll let reviewers decide that. Without doing this, I'd need to do an extra pass through VMAs to find what all needs to be GPU modified before I can unmap from GPU, then perform remaining unmap work. Especially for places like MapMemory, that's a lot of code bloat. * Fixups * Update memory.cpp * Rename mutex It's really just a mutex for the sole purpose of dealing with GPU unmaps, so unmap_mutex is a bit more fitting than transition_mutex --- src/common/shared_first_mutex.h | 9 ++ src/core/address_space.cpp | 8 +- src/core/address_space.h | 3 +- src/core/memory.cpp | 214 +++++++++++++++++++------------- src/core/memory.h | 9 +- 5 files changed, 145 insertions(+), 98 deletions(-) diff --git a/src/common/shared_first_mutex.h b/src/common/shared_first_mutex.h index b150c956b..fcf9d0c4f 100644 --- a/src/common/shared_first_mutex.h +++ b/src/common/shared_first_mutex.h @@ -17,6 +17,15 @@ public: writer_active = true; } + bool try_lock() { + std::lock_guard lock(mtx); + if (writer_active || readers > 0) { + return false; + } + writer_active = true; + return true; + } + void unlock() { std::lock_guard lock(mtx); writer_active = false; diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index f4a6b640e..194f676f9 100644 --- a/src/core/address_space.cpp +++ b/src/core/address_space.cpp @@ -709,7 +709,7 @@ struct AddressSpace::Impl { return ret; } - void Unmap(VAddr virtual_addr, u64 size, bool) { + void Unmap(VAddr virtual_addr, u64 size) { // Check to see if we are adjacent to any regions. VAddr start_address = virtual_addr; VAddr end_address = start_address + size; @@ -792,12 +792,8 @@ void* AddressSpace::MapFile(VAddr virtual_addr, u64 size, u64 offset, u32 prot, #endif } -void AddressSpace::Unmap(VAddr virtual_addr, u64 size, bool has_backing) { -#ifdef _WIN32 +void AddressSpace::Unmap(VAddr virtual_addr, u64 size) { impl->Unmap(virtual_addr, size); -#else - impl->Unmap(virtual_addr, size, has_backing); -#endif } void AddressSpace::Protect(VAddr virtual_addr, u64 size, MemoryPermission perms) { diff --git a/src/core/address_space.h b/src/core/address_space.h index fa47bb47e..b71f66f28 100644 --- a/src/core/address_space.h +++ b/src/core/address_space.h @@ -79,8 +79,9 @@ public: void* MapFile(VAddr virtual_addr, u64 size, u64 offset, u32 prot, uintptr_t fd); /// Unmaps specified virtual memory area. - void Unmap(VAddr virtual_addr, u64 size, bool has_backing); + void Unmap(VAddr virtual_addr, u64 size); + /// Protects requested region. void Protect(VAddr virtual_addr, u64 size, MemoryPermission perms); // Returns an interval set containing all usable regions. diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 32518907a..1aeecebf1 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -79,6 +79,7 @@ u64 MemoryManager::ClampRangeSize(VAddr virtual_addr, u64 size) { return size; } + std::shared_lock lk{mutex}; ASSERT_MSG(IsValidMapping(virtual_addr), "Attempted to access invalid address {:#x}", virtual_addr); @@ -117,6 +118,7 @@ void MemoryManager::SetPrtArea(u32 id, VAddr address, u64 size) { } void MemoryManager::CopySparseMemory(VAddr virtual_addr, u8* dest, u64 size) { + std::shared_lock lk{mutex}; ASSERT_MSG(IsValidMapping(virtual_addr), "Attempted to access invalid address {:#x}", virtual_addr); @@ -137,6 +139,7 @@ void MemoryManager::CopySparseMemory(VAddr virtual_addr, u8* dest, u64 size) { bool MemoryManager::TryWriteBacking(void* address, const void* data, u64 size) { const VAddr virtual_addr = std::bit_cast(address); + std::shared_lock lk{mutex}; ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); @@ -263,9 +266,7 @@ s32 MemoryManager::Free(PAddr phys_addr, u64 size, bool is_checked) { return ORBIS_OK; } - // Lock mutex - std::scoped_lock lk{mutex}; - + std::scoped_lock lk{unmap_mutex}; // If this is a checked free, then all direct memory in range must be allocated. std::vector> free_list; u64 remaining_size = size; @@ -316,6 +317,17 @@ s32 MemoryManager::Free(PAddr phys_addr, u64 size, bool is_checked) { } } } + + // Early unmap from GPU to avoid deadlocking. + for (auto& [addr, unmap_size] : remove_list) { + if (IsValidGpuMapping(addr, unmap_size)) { + rasterizer->UnmapMemory(addr, unmap_size); + } + } + + // Acquire writer lock + std::scoped_lock lk2{mutex}; + for (const auto& [addr, size] : remove_list) { LOG_INFO(Kernel_Vmm, "Unmapping direct mapping {:#x} with size {:#x}", addr, size); UnmapMemoryImpl(addr, size); @@ -337,7 +349,7 @@ 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}; + std::scoped_lock lk{mutex, unmap_mutex}; ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); @@ -429,54 +441,31 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 return ORBIS_OK; } -std::pair MemoryManager::CreateArea( - VAddr virtual_addr, u64 size, MemoryProt prot, MemoryMapFlags flags, VMAType type, - std::string_view name, u64 alignment) { - - // Limit the minimum address to SystemManagedVirtualBase to prevent hardware-specific issues. - VAddr mapped_addr = (virtual_addr == 0) ? impl.SystemManagedVirtualBase() : virtual_addr; - - // Fixed mapping means the virtual address must exactly match the provided one. - // On a PS4, the Fixed flag is ignored if address 0 is provided. - if (True(flags & MemoryMapFlags::Fixed) && virtual_addr != 0) { - ASSERT_MSG(IsValidMapping(mapped_addr, size), "Attempted to access invalid address {:#x}", - mapped_addr); - auto vma = FindVMA(mapped_addr)->second; - // There's a possible edge case where we're mapping to a partially reserved range. - // To account for this, unmap any reserved areas within this mapping range first. - auto unmap_addr = mapped_addr; +MemoryManager::VMAHandle MemoryManager::CreateArea(VAddr virtual_addr, u64 size, MemoryProt prot, + MemoryMapFlags flags, VMAType type, + std::string_view name, u64 alignment) { + // Locate the VMA representing the requested region + auto vma = FindVMA(virtual_addr)->second; + if (True(flags & MemoryMapFlags::Fixed)) { + // If fixed is specified, map directly to the region of virtual_addr + size. + // Callers should check to ensure the NoOverwrite flag is handled appropriately beforehand. + auto unmap_addr = virtual_addr; auto unmap_size = size; - - // If flag NoOverwrite is provided, don't overwrite mapped VMAs. - // When it isn't provided, VMAs can be overwritten regardless of if they're mapped. - while ((False(flags & MemoryMapFlags::NoOverwrite) || vma.IsFree()) && - unmap_addr < mapped_addr + size) { + while (unmap_size > 0) { auto unmapped = UnmapBytesFromEntry(unmap_addr, vma, unmap_size); unmap_addr += unmapped; unmap_size -= unmapped; vma = FindVMA(unmap_addr)->second; } - - vma = FindVMA(mapped_addr)->second; - auto remaining_size = vma.base + vma.size - mapped_addr; - if (!vma.IsFree() || remaining_size < size) { - LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at address {:#x}", size, mapped_addr); - return {ORBIS_KERNEL_ERROR_ENOMEM, vma_map.end()}; - } - } else { - // When MemoryMapFlags::Fixed is not specified, and mapped_addr is 0, - // search from address 0x200000000 instead. - alignment = alignment > 0 ? alignment : 16_KB; - mapped_addr = virtual_addr == 0 ? 0x200000000 : mapped_addr; - mapped_addr = SearchFree(mapped_addr, size, alignment); - if (mapped_addr == -1) { - // No suitable memory areas to map to - return {ORBIS_KERNEL_ERROR_ENOMEM, vma_map.end()}; - } } + vma = FindVMA(virtual_addr)->second; + + // By this point, vma should be free and ready to map. + // Caller performs address searches for non-fixed mappings before this. + ASSERT_MSG(vma.IsFree(), "VMA to map is not free"); // Create a memory area representing this mapping. - const auto new_vma_handle = CarveVMA(mapped_addr, size); + const auto new_vma_handle = CarveVMA(virtual_addr, size); auto& new_vma = new_vma_handle->second; const bool is_exec = True(prot & MemoryProt::CpuExec); if (True(prot & MemoryProt::CpuWrite)) { @@ -484,12 +473,13 @@ std::pair MemoryManager::CreateArea( prot |= MemoryProt::CpuRead; } + // Update VMA appropriately. new_vma.disallow_merge = True(flags & MemoryMapFlags::NoCoalesce); new_vma.prot = prot; new_vma.name = name; new_vma.type = type; new_vma.phys_areas.clear(); - return {ORBIS_OK, new_vma_handle}; + return new_vma_handle; } s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, MemoryProt prot, @@ -504,8 +494,7 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo total_flexible_size - flexible_usage, size); return ORBIS_KERNEL_ERROR_EINVAL; } - - std::scoped_lock lk{mutex}; + std::scoped_lock lk{unmap_mutex}; PhysHandle dmem_area; // Validate the requested physical address range @@ -538,12 +527,37 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo } } - auto [result, new_vma_handle] = - CreateArea(virtual_addr, size, prot, flags, type, name, alignment); - if (result != ORBIS_OK) { - return result; + if (True(flags & MemoryMapFlags::Fixed) && True(flags & MemoryMapFlags::NoOverwrite)) { + // Perform necessary error checking for Fixed & NoOverwrite case + ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", + virtual_addr); + auto vma = FindVMA(virtual_addr)->second; + auto remaining_size = vma.base + vma.size - virtual_addr; + if (!vma.IsFree() || remaining_size < size) { + LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at address {:#x}", size, virtual_addr); + return ORBIS_KERNEL_ERROR_ENOMEM; + } + } else if (False(flags & MemoryMapFlags::Fixed)) { + // Find a free virtual addr to map + alignment = alignment > 0 ? alignment : 16_KB; + virtual_addr = virtual_addr == 0 ? DEFAULT_MAPPING_BASE : virtual_addr; + virtual_addr = SearchFree(virtual_addr, size, alignment); + if (virtual_addr == -1) { + // No suitable memory areas to map to + return ORBIS_KERNEL_ERROR_ENOMEM; + } } + // Perform early GPU unmap to avoid potential deadlocks + if (IsValidGpuMapping(virtual_addr, size)) { + rasterizer->UnmapMemory(virtual_addr, size); + } + + // Acquire writer lock. + std::scoped_lock lk2{mutex}; + + // Create VMA representing this mapping. + auto new_vma_handle = CreateArea(virtual_addr, size, prot, flags, type, name, alignment); auto& new_vma = new_vma_handle->second; auto mapped_addr = new_vma.base; bool is_exec = True(prot & MemoryProt::CpuExec); @@ -590,7 +604,7 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo // Map the physical memory for this direct memory mapping. auto phys_addr_to_search = phys_addr; u64 remaining_size = size; - dmem_area = FindDmemArea(phys_addr); + 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. @@ -638,14 +652,15 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo rasterizer->MapMemory(mapped_addr, size); } } + return ORBIS_OK; } s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, MemoryProt prot, MemoryMapFlags flags, s32 fd, s64 phys_addr) { - std::scoped_lock lk{mutex}; + uintptr_t handle = 0; + std::scoped_lock lk{unmap_mutex}; // Get the file to map - auto* h = Common::Singleton::Instance(); auto file = h->GetFile(fd); if (file == nullptr) { @@ -663,12 +678,13 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory prot |= MemoryProt::CpuRead; } - const auto handle = file->f.GetFileMapping(); + handle = file->f.GetFileMapping(); if (False(file->f.GetAccessMode() & Common::FS::FileAccessMode::Write) || False(file->f.GetAccessMode() & Common::FS::FileAccessMode::Append)) { - // If the file does not have write access, ensure prot does not contain write permissions. - // On real hardware, these mappings succeed, but the memory cannot be written to. + // If the file does not have write access, ensure prot does not contain write + // permissions. On real hardware, these mappings succeed, but the memory cannot be + // written to. prot &= ~MemoryProt::CpuWrite; } @@ -682,13 +698,38 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory prot &= ~MemoryProt::CpuExec; } - auto [result, new_vma_handle] = - CreateArea(virtual_addr, size, prot, flags, VMAType::File, "anon", 0); - if (result != ORBIS_OK) { - return result; + if (True(flags & MemoryMapFlags::Fixed) && False(flags & MemoryMapFlags::NoOverwrite)) { + ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", + virtual_addr); + auto vma = FindVMA(virtual_addr)->second; + + auto remaining_size = vma.base + vma.size - virtual_addr; + if (!vma.IsFree() || remaining_size < size) { + LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at address {:#x}", size, virtual_addr); + return ORBIS_KERNEL_ERROR_ENOMEM; + } + } else if (False(flags & MemoryMapFlags::Fixed)) { + virtual_addr = virtual_addr == 0 ? DEFAULT_MAPPING_BASE : virtual_addr; + virtual_addr = SearchFree(virtual_addr, size, 16_KB); + if (virtual_addr == -1) { + // No suitable memory areas to map to + return ORBIS_KERNEL_ERROR_ENOMEM; + } } + // Perform early GPU unmap to avoid potential deadlocks + if (IsValidGpuMapping(virtual_addr, size)) { + rasterizer->UnmapMemory(virtual_addr, size); + } + + // Aquire writer lock + std::scoped_lock lk2{mutex}; + + // Update VMA map and map to address space. + auto new_vma_handle = CreateArea(virtual_addr, size, prot, flags, VMAType::File, "anon", 0); + auto& new_vma = new_vma_handle->second; + new_vma.fd = fd; auto mapped_addr = new_vma.base; bool is_exec = True(prot & MemoryProt::CpuExec); @@ -699,7 +740,7 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory } s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { - std::scoped_lock lk{mutex}; + std::scoped_lock lk{unmap_mutex}; ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); @@ -713,6 +754,14 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { it++; } + // Perform early GPU unmap to avoid potential deadlocks + if (IsValidGpuMapping(virtual_addr, size)) { + rasterizer->UnmapMemory(virtual_addr, size); + } + + // Aquire writer mutex + std::scoped_lock lk2{mutex}; + // Loop through all vmas in the area, unmap them. u64 remaining_size = size; VAddr current_addr = virtual_addr; @@ -721,13 +770,7 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { const auto& vma_base = handle->second; const auto start_in_vma = current_addr - vma_base.base; const auto size_in_vma = std::min(remaining_size, vma_base.size - start_in_vma); - if (vma_base.type == VMAType::Pooled) { - // We always map PoolCommitted memory to GPU, so unmap when decomitting. - if (IsValidGpuMapping(current_addr, size_in_vma)) { - rasterizer->UnmapMemory(current_addr, size_in_vma); - } - // Track how much pooled memory is decommitted pool_budget += size_in_vma; @@ -772,7 +815,7 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { } // Unmap from address space - impl.Unmap(virtual_addr, size, true); + impl.Unmap(virtual_addr, size); // Tracy memory tracking breaks from merging memory areas. Disabled for now. // TRACK_FREE(virtual_addr, "VMEM"); @@ -783,29 +826,32 @@ s32 MemoryManager::UnmapMemory(VAddr virtual_addr, u64 size) { if (size == 0) { return ORBIS_OK; } - std::scoped_lock lk{mutex}; + + std::scoped_lock lk{unmap_mutex}; + // Align address and size appropriately virtual_addr = Common::AlignDown(virtual_addr, 16_KB); size = Common::AlignUp(size, 16_KB); ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); - u64 bytes_unmapped = UnmapMemoryImpl(virtual_addr, size); - return bytes_unmapped; + + // If the requested range has GPU access, unmap from GPU. + if (IsValidGpuMapping(virtual_addr, size)) { + rasterizer->UnmapMemory(virtual_addr, size); + } + + // Acquire writer lock. + std::scoped_lock lk2{mutex}; + return UnmapMemoryImpl(virtual_addr, size); } u64 MemoryManager::UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma_base, u64 size) { const auto start_in_vma = virtual_addr - vma_base.base; const auto size_in_vma = std::min(vma_base.size - start_in_vma, size); const auto vma_type = vma_base.type; - const bool has_backing = HasPhysicalBacking(vma_base) || vma_base.type == VMAType::File; - const bool readonly_file = - vma_base.prot == MemoryProt::CpuRead && vma_base.type == VMAType::File; - const bool is_exec = True(vma_base.prot & MemoryProt::CpuExec); - if (vma_base.type == VMAType::Free || vma_base.type == VMAType::Pooled) { return size_in_vma; } - PAddr phys_base = 0; VAddr current_addr = virtual_addr; if (vma_base.phys_areas.size() > 0) { u64 size_to_free = size_in_vma; @@ -860,14 +906,9 @@ u64 MemoryManager::UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma if (vma_type != VMAType::Reserved && vma_type != VMAType::PoolReserved) { // Unmap the memory region. - impl.Unmap(virtual_addr, size_in_vma, has_backing); + impl.Unmap(virtual_addr, size_in_vma); // Tracy memory tracking breaks from merging memory areas. Disabled for now. // TRACK_FREE(virtual_addr, "VMEM"); - - // If this mapping has GPU access, unmap from GPU. - if (IsValidGpuMapping(virtual_addr, size)) { - rasterizer->UnmapMemory(virtual_addr, size); - } } return size_in_vma; } @@ -983,7 +1024,7 @@ s32 MemoryManager::Protect(VAddr addr, u64 size, MemoryProt prot) { } // Ensure the range to modify is valid - std::scoped_lock lk{mutex}; + std::scoped_lock lk{mutex, unmap_mutex}; ASSERT_MSG(IsValidMapping(addr, size), "Attempted to access invalid address {:#x}", addr); // Appropriately restrict flags. @@ -1141,7 +1182,7 @@ s32 MemoryManager::DirectQueryAvailable(PAddr search_start, PAddr search_end, u6 } s32 MemoryManager::SetDirectMemoryType(VAddr addr, u64 size, s32 memory_type) { - std::scoped_lock lk{mutex}; + std::scoped_lock lk{mutex, unmap_mutex}; ASSERT_MSG(IsValidMapping(addr, size), "Attempted to access invalid address {:#x}", addr); @@ -1188,7 +1229,7 @@ s32 MemoryManager::SetDirectMemoryType(VAddr addr, u64 size, s32 memory_type) { } void MemoryManager::NameVirtualRange(VAddr virtual_addr, u64 size, std::string_view name) { - std::scoped_lock lk{mutex}; + std::scoped_lock lk{mutex, unmap_mutex}; // Sizes are aligned up to the nearest 16_KB u64 aligned_size = Common::AlignUp(size, 16_KB); @@ -1246,7 +1287,6 @@ s32 MemoryManager::IsStack(VAddr addr, void** start, void** end) { ASSERT_MSG(IsValidMapping(addr), "Attempted to access invalid address {:#x}", addr); const auto& vma = FindVMA(addr)->second; if (vma.IsFree()) { - mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_EACCES; } diff --git a/src/core/memory.h b/src/core/memory.h index 92a1016bf..f9ae64942 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -28,6 +28,8 @@ class MemoryMapViewer; namespace Core { +constexpr u64 DEFAULT_MAPPING_BASE = 0x200000000; + enum class MemoryProt : u32 { NoAccess = 0, CpuRead = 1, @@ -304,10 +306,8 @@ private: vma.type == VMAType::Pooled; } - std::pair CreateArea(VAddr virtual_addr, u64 size, - MemoryProt prot, MemoryMapFlags flags, - VMAType type, std::string_view name, - u64 alignment); + VMAHandle CreateArea(VAddr virtual_addr, u64 size, MemoryProt prot, MemoryMapFlags flags, + VMAType type, std::string_view name, u64 alignment); VAddr SearchFree(VAddr virtual_addr, u64 size, u32 alignment); @@ -333,6 +333,7 @@ private: PhysMap fmem_map; VMAMap vma_map; Common::SharedFirstMutex mutex{}; + std::mutex unmap_mutex{}; u64 total_direct_size{}; u64 total_flexible_size{}; u64 flexible_usage{}; From 1e059cac04426deef931ed9198a824083bca8038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 27 Jan 2026 11:27:56 +0100 Subject: [PATCH 02/50] Implement V_LSHR_B64 (#3961) --- src/shader_recompiler/frontend/translate/translate.h | 1 + src/shader_recompiler/frontend/translate/vector_alu.cpp | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index f999a3e3e..3d4d7ca72 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -259,6 +259,7 @@ public: void V_CVT_PK_I16_I32(const GcnInst& inst); void V_CVT_PK_U8_F32(const GcnInst& inst); void V_LSHL_B64(const GcnInst& inst); + void V_LSHR_B64(const GcnInst& inst); void V_ALIGNBIT_B32(const GcnInst& inst); void V_ALIGNBYTE_B32(const GcnInst& inst); void V_MUL_F64(const GcnInst& inst); diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index 0803647a2..5cb04f12f 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -394,6 +394,8 @@ void Translator::EmitVectorAlu(const GcnInst& inst) { return V_CVT_PK_U8_F32(inst); case Opcode::V_LSHL_B64: return V_LSHL_B64(inst); + case Opcode::V_LSHR_B64: + return V_LSHR_B64(inst); case Opcode::V_ADD_F64: return V_ADD_F64(inst); case Opcode::V_ALIGNBIT_B32: @@ -1357,6 +1359,12 @@ void Translator::V_LSHL_B64(const GcnInst& inst) { SetDst64(inst.dst[0], ir.ShiftLeftLogical(src0, ir.BitwiseAnd(src1, ir.Imm64(u64(0x3F))))); } +void Translator::V_LSHR_B64(const GcnInst& inst) { + const IR::U64 src0{GetSrc64(inst.src[0])}; + const IR::U64 src1{GetSrc64(inst.src[1])}; + SetDst64(inst.dst[0], ir.ShiftRightLogical(src0, ir.BitwiseAnd(src1, ir.Imm64(u64(0x3F))))); +} + void Translator::V_ALIGNBIT_B32(const GcnInst& inst) { const IR::U32 src0{GetSrc(inst.src[0])}; const IR::U32 src1{GetSrc(inst.src[1])}; From b17ac0fdda764d3d5dd65d5a322c66018824f430 Mon Sep 17 00:00:00 2001 From: Berk Date: Tue, 27 Jan 2026 16:59:56 +0300 Subject: [PATCH 03/50] Fix most of vscode problems and switch to Arch for more stability (#3964) --- .../.devcontainer/devcontainer.json | 21 ++++--- documents/Docker Builder/.docker/Dockerfile | 58 ++++++++++--------- documents/building-docker.md | 11 +++- 3 files changed, 53 insertions(+), 37 deletions(-) diff --git a/documents/Docker Builder/.devcontainer/devcontainer.json b/documents/Docker Builder/.devcontainer/devcontainer.json index 32e301bd9..9093f6722 100644 --- a/documents/Docker Builder/.devcontainer/devcontainer.json +++ b/documents/Docker Builder/.devcontainer/devcontainer.json @@ -17,29 +17,32 @@ "customizations": { "vscode": { "extensions": [ - "llvm-vs-code-extensions.vscode-clangd" + "llvm-vs-code-extensions.vscode-clangd", + "ms-vscode.cmake-tools" ], "settings": { - "C_Cpp.intelliSenseEngine": "disabled", "clangd.arguments": [ "--background-index", "--clang-tidy", "--completion-style=detailed", - "--header-insertion=never" - ] + "--header-insertion=never", + "--compile-commands-dir=/workspaces/shadPS4/Build/x64-Clang-Release" + ], + "C_Cpp.intelliSenseEngine": "Disabled" } } }, "settings": { - "cmake.configureOnOpen": false, + "cmake.configureOnOpen": false, "cmake.generator": "Unix Makefiles", "cmake.environment": { "CC": "clang", "CXX": "clang++" }, - "cmake.configureSettings": { - "CMAKE_CXX_STANDARD": "23", - "CMAKE_CXX_STANDARD_REQUIRED": "ON" - } + "cmake.configureEnvironment": { + "CMAKE_CXX_STANDARD": "23", + "CMAKE_CXX_STANDARD_REQUIRED": "ON", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } } } \ No newline at end of file diff --git a/documents/Docker Builder/.docker/Dockerfile b/documents/Docker Builder/.docker/Dockerfile index 285144374..b168a6a72 100644 --- a/documents/Docker Builder/.docker/Dockerfile +++ b/documents/Docker Builder/.docker/Dockerfile @@ -1,38 +1,42 @@ # SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later -FROM ubuntu:24.04 +FROM archlinux:latest -ENV DEBIAN_FRONTEND=noninteractive +RUN pacman-key --init && \ + pacman-key --populate archlinux && \ + pacman -Syu --noconfirm -RUN apt-get update && apt-get install -y \ - build-essential \ +RUN pacman -S --noconfirm \ + base-devel \ clang \ + ninja \ git \ ca-certificates \ wget \ - libasound2-dev \ - libpulse-dev \ - libopenal-dev \ - libssl-dev \ - zlib1g-dev \ - libedit-dev \ - libudev-dev \ - libevdev-dev \ - libsdl2-dev \ - libjack-dev \ - libsndio-dev \ - libxtst-dev \ - libvulkan-dev \ - vulkan-validationlayers \ - libpng-dev \ - clang-tidy \ - && rm -rf /var/lib/apt/lists/* - -RUN wget -qO - https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor -o /usr/share/keyrings/kitware-archive-keyring.gpg \ - && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ noble main" > /etc/apt/sources.list.d/kitware.list \ - && apt-get update \ - && apt-get install -y cmake \ - && rm -rf /var/lib/apt/lists/*/* + alsa-lib \ + libpulse \ + openal \ + openssl \ + zlib \ + libedit \ + systemd-libs \ + libevdev \ + sdl2 \ + jack \ + sndio \ + libxtst \ + vulkan-headers \ + vulkan-validation-layers \ + libpng \ + clang-tools-extra \ + cmake \ + libx11 \ + libxrandr \ + libxcursor \ + libxi \ + libxinerama \ + libxss \ + && pacman -Scc --noconfirm WORKDIR /workspaces/shadPS4 \ No newline at end of file diff --git a/documents/building-docker.md b/documents/building-docker.md index 95be00044..84d238751 100644 --- a/documents/building-docker.md +++ b/documents/building-docker.md @@ -54,7 +54,16 @@ or your fork link. git submodule update --init --recursive ``` -## Step 3: Build with CMake +## Step 3: Build with CMake Tools (GUI) + +Generate build with CMake Tools. + +1. Go `CMake Tools > Configure > '>'` +2. And `Build > '>'` + +Compiled executable in `Build` folder. + +## Alternative Step 3: Build with CMake Generate the build directory and configure the project using Clang: From 1473b2358a639dd5484382db37b97aa72a81c590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 27 Jan 2026 19:18:05 +0100 Subject: [PATCH 04/50] Implement V_CMP_OP_F64 (#3962) --- .../frontend/translate/translate.h | 1 + .../frontend/translate/vector_alu.cpp | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 3d4d7ca72..1e93eea7c 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -231,6 +231,7 @@ public: // VOPC void V_CMP_F32(ConditionOp op, bool set_exec, const GcnInst& inst); + void V_CMP_F64(ConditionOp op, bool set_exec, const GcnInst& inst); void V_CMP_U32(ConditionOp op, bool is_signed, bool set_exec, const GcnInst& inst); void V_CMP_U64(ConditionOp op, bool is_signed, bool set_exec, const GcnInst& inst); void V_CMP_CLASS_F32(const GcnInst& inst); diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index 5cb04f12f..35ff6af37 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -264,6 +264,34 @@ void Translator::EmitVectorAlu(const GcnInst& inst) { case Opcode::V_CMPX_TRU_F32: return V_CMP_F32(ConditionOp::TRU, true, inst); + // V_CMP_{OP16}_F64 + case Opcode::V_CMP_F_F64: + return V_CMP_F64(ConditionOp::F, false, inst); + case Opcode::V_CMP_LT_F64: + return V_CMP_F64(ConditionOp::LT, false, inst); + case Opcode::V_CMP_EQ_F64: + return V_CMP_F64(ConditionOp::EQ, false, inst); + case Opcode::V_CMP_LE_F64: + return V_CMP_F64(ConditionOp::LE, false, inst); + case Opcode::V_CMP_GT_F64: + return V_CMP_F64(ConditionOp::GT, false, inst); + case Opcode::V_CMP_LG_F64: + return V_CMP_F64(ConditionOp::LG, false, inst); + case Opcode::V_CMP_GE_F64: + return V_CMP_F64(ConditionOp::GE, false, inst); + case Opcode::V_CMP_U_F64: + return V_CMP_F64(ConditionOp::U, false, inst); + case Opcode::V_CMP_NGE_F64: + return V_CMP_F64(ConditionOp::LT, false, inst); + case Opcode::V_CMP_NGT_F64: + return V_CMP_F64(ConditionOp::LE, false, inst); + case Opcode::V_CMP_NLE_F64: + return V_CMP_F64(ConditionOp::GT, false, inst); + case Opcode::V_CMP_NEQ_F64: + return V_CMP_F64(ConditionOp::LG, false, inst); + case Opcode::V_CMP_NLT_F64: + return V_CMP_F64(ConditionOp::GE, false, inst); + // V_CMP_{OP8}_I32 case Opcode::V_CMP_LT_I32: return V_CMP_U32(ConditionOp::LT, true, false, inst); @@ -1013,6 +1041,47 @@ void Translator::V_CMP_F32(ConditionOp op, bool set_exec, const GcnInst& inst) { } } +void Translator::V_CMP_F64(ConditionOp op, bool set_exec, const GcnInst& inst) { + const IR::F64 src0{GetSrc64(inst.src[0])}; + const IR::F64 src1{GetSrc64(inst.src[1])}; + const IR::U1 result = [&] { + switch (op) { + case ConditionOp::F: + return ir.Imm1(false); + case ConditionOp::EQ: + return ir.FPEqual(src0, src1); + case ConditionOp::LG: + return ir.FPNotEqual(src0, src1); + case ConditionOp::GT: + return ir.FPGreaterThan(src0, src1); + case ConditionOp::LT: + return ir.FPLessThan(src0, src1); + case ConditionOp::LE: + return ir.FPLessThanEqual(src0, src1); + case ConditionOp::GE: + return ir.FPGreaterThanEqual(src0, src1); + case ConditionOp::U: + return ir.LogicalOr(ir.FPIsNan(src0), ir.FPIsNan(src1)); + default: + UNREACHABLE(); + } + }(); + if (set_exec) { + ir.SetExec(result); + } + + switch (inst.dst[1].field) { + case OperandField::VccLo: + ir.SetVcc(result); + break; + case OperandField::ScalarGPR: + ir.SetThreadBitScalarReg(IR::ScalarReg(inst.dst[1].code), result); + break; + default: + UNREACHABLE(); + } +} + void Translator::V_CMP_U32(ConditionOp op, bool is_signed, bool set_exec, const GcnInst& inst) { const IR::U32 src0{GetSrc(inst.src[0])}; const IR::U32 src1{GetSrc(inst.src[1])}; From c81ebe6418b78f25a32c7394147904e6ce9de679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 27 Jan 2026 22:08:26 +0100 Subject: [PATCH 05/50] Implement V_FFBH_I32 (#3965) --- .../frontend/translate/translate.h | 1 + .../frontend/translate/vector_alu.cpp | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 1e93eea7c..08b0192f5 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -219,6 +219,7 @@ public: void V_NOT_B32(const GcnInst& inst); void V_BFREV_B32(const GcnInst& inst); void V_FFBH_U32(const GcnInst& inst); + void V_FFBH_I32(const GcnInst& inst); void V_FFBL_B32(const GcnInst& inst); void V_FREXP_EXP_I32_F64(const GcnInst& inst); void V_FREXP_MANT_F64(const GcnInst& inst); diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index 35ff6af37..08a0f6527 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -188,6 +188,8 @@ void Translator::EmitVectorAlu(const GcnInst& inst) { return V_FFBH_U32(inst); case Opcode::V_FFBL_B32: return V_FFBL_B32(inst); + case Opcode::V_FFBH_I32: + return V_FFBH_I32(inst); case Opcode::V_FREXP_EXP_I32_F64: return V_FREXP_EXP_I32_F64(inst); case Opcode::V_FREXP_MANT_F64: @@ -948,6 +950,19 @@ void Translator::V_FFBL_B32(const GcnInst& inst) { SetDst(inst.dst[0], ir.FindILsb(src0)); } +void Translator::V_FFBH_I32(const GcnInst& inst) { + const IR::U32 src0{GetSrc(inst.src[0])}; + // Gcn wants the MSB position counting from the left, but SPIR-V counts from the rightmost (LSB) + // position + const IR::U32 msb_pos = ir.FindSMsb(src0); + const IR::U32 pos_from_left = ir.ISub(ir.Imm32(31), msb_pos); + // Select 0xFFFFFFFF if src0 was 0 or -1 + const IR::U32 minusOne = ir.Imm32(~0U); + const IR::U1 cond = + ir.LogicalAnd(ir.INotEqual(src0, ir.Imm32(0)), ir.INotEqual(src0, minusOne)); + SetDst(inst.dst[0], IR::U32{ir.Select(cond, pos_from_left, minusOne)}); +} + void Translator::V_FREXP_EXP_I32_F64(const GcnInst& inst) { const IR::F64 src0{GetSrc64(inst.src[0])}; SetDst(inst.dst[0], ir.FPFrexpExp(src0)); From 9314633573b1ae73c84df41abaa397b1cd0908be Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:54:01 -0800 Subject: [PATCH 06/50] externals: Update MoltenVK to fix primitive restart disable issue. (#3967) --- externals/MoltenVK | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/externals/MoltenVK b/externals/MoltenVK index f168dec05..f79c6c569 160000 --- a/externals/MoltenVK +++ b/externals/MoltenVK @@ -1 +1 @@ -Subproject commit f168dec05998ab0ca09a400bab6831a95c0bdb2e +Subproject commit f79c6c5690d3ee06ec3a00d11a8b1bab4aa1d030 From cc70fa8bb5effd532ace4c7bdfdfa43f97d92c68 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:25:48 +0100 Subject: [PATCH 07/50] Fix thread names not updating correctly (#3968) Also, apply consistency to thread names Also also, copyright 2026 --- src/common/thread.cpp | 9 +++++++++ src/core/libraries/kernel/threads/pthread.cpp | 3 ++- src/core/linker.cpp | 4 ++-- src/emulator.cpp | 4 ++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/common/thread.cpp b/src/common/thread.cpp index 54194186c..d6daaa852 100644 --- a/src/common/thread.cpp +++ b/src/common/thread.cpp @@ -174,6 +174,9 @@ bool AccurateSleep(const std::chrono::nanoseconds duration, std::chrono::nanosec // Sets the debugger-visible name of the current thread. void SetCurrentThreadName(const char* name) { + if (Libraries::Kernel::g_curthread) { + Libraries::Kernel::g_curthread->name = std::string{name}; + } SetThreadDescription(GetCurrentThread(), UTF8ToUTF16W(name).data()); } @@ -186,6 +189,9 @@ void SetThreadName(void* thread, const char* name) { // MinGW with the POSIX threading model does not support pthread_setname_np #if !defined(_WIN32) || defined(_MSC_VER) void SetCurrentThreadName(const char* name) { + if (Libraries::Kernel::g_curthread) { + Libraries::Kernel::g_curthread->name = std::string{name}; + } #ifdef __APPLE__ pthread_setname_np(name); #elif defined(__Bitrig__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__OpenBSD__) @@ -212,6 +218,9 @@ void SetThreadName(void* thread, const char* name) { #if defined(_WIN32) void SetCurrentThreadName(const char*) { + if (Libraries::Kernel::g_curthread) { + Libraries::Kernel::g_curthread->name = std::string{name}; + } // Do Nothing on MinGW } diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index 6c11eebc2..f1107ef30 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.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/assert.h" @@ -325,6 +325,7 @@ PthreadT PS4_SYSV_ABI posix_pthread_self() { } void PS4_SYSV_ABI posix_pthread_set_name_np(PthreadT thread, const char* name) { + LOG_INFO(Kernel_Pthread, "called, new name: {}", name); Common::SetCurrentThreadName(name); } diff --git a/src/core/linker.cpp b/src/core/linker.cpp index 97d766a38..7a0653e9f 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.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" @@ -105,7 +105,7 @@ void Linker::Execute(const std::vector& args) { memory->SetupMemoryRegions(fmem_size, use_extended_mem1, use_extended_mem2); main_thread.Run([this, module, &args](std::stop_token) { - Common::SetCurrentThreadName("GAME_MainThread"); + Common::SetCurrentThreadName("Game:Main"); if (auto& ipc = IPC::Instance()) { ipc.WaitForStart(); } diff --git a/src/emulator.cpp b/src/emulator.cpp index 263bd9c2b..9044ed027 100644 --- a/src/emulator.cpp +++ b/src/emulator.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 @@ -96,7 +96,7 @@ s32 ReadCompiledSdkVersion(const std::filesystem::path& file) { void Emulator::Run(std::filesystem::path file, std::vector args, std::optional p_game_folder) { - Common::SetCurrentThreadName("Main Thread"); + Common::SetCurrentThreadName("shadPS4:Main"); if (waitForDebuggerBeforeRun) { Debugger::WaitForDebuggerAttach(); } From f14bad729c4d83e5e32d90f35a0cd2a2a3f8ff62 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 28 Jan 2026 07:10:37 -0600 Subject: [PATCH 08/50] Np: libSceNpPartner001 stubs (#3963) * Initial work * Oops --- CMakeLists.txt | 2 + src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + src/core/libraries/libs.cpp | 2 + src/core/libraries/np/np_partner.cpp | 83 ++++++++++++++++++++++++ src/core/libraries/np/np_partner.h | 15 +++++ src/core/libraries/np/np_partner_error.h | 9 +++ 7 files changed, 113 insertions(+) create mode 100644 src/core/libraries/np/np_partner.cpp create mode 100644 src/core/libraries/np/np_partner.h create mode 100644 src/core/libraries/np/np_partner_error.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c81c7550..0299d242b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -599,6 +599,8 @@ set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/np_profile_dialog.h src/core/libraries/np/np_sns_facebook_dialog.cpp src/core/libraries/np/np_sns_facebook_dialog.h + src/core/libraries/np/np_partner.cpp + src/core/libraries/np/np_partner.h ) set(ZLIB_LIB src/core/libraries/zlib/zlib.cpp diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 9c8b80255..22ee8ddb3 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -113,6 +113,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, NpWebApi2) \ SUB(Lib, NpProfileDialog) \ SUB(Lib, NpSnsFacebookDialog) \ + SUB(Lib, NpPartner) \ SUB(Lib, Screenshot) \ SUB(Lib, LibCInternal) \ SUB(Lib, AppContent) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index dc7c561a4..ed7bbaa3f 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -110,6 +110,7 @@ enum class Class : u8 { Lib_Mouse, ///< The LibSceMouse implementation Lib_WebBrowserDialog, ///< The LibSceWebBrowserDialog implementation Lib_NpParty, ///< The LibSceNpParty implementation + Lib_NpPartner, ///< The LibSceNpPartner implementation Lib_Zlib, ///< The LibSceZlib implementation. Lib_Hmd, ///< The LibSceHmd implementation. Lib_HmdSetupDialog, ///< The LibSceHmdSetupDialog implementation. diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 7f679e7c2..30fed4fee 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -35,6 +35,7 @@ #include "core/libraries/np/np_commerce.h" #include "core/libraries/np/np_common.h" #include "core/libraries/np/np_manager.h" +#include "core/libraries/np/np_partner.h" #include "core/libraries/np/np_party.h" #include "core/libraries/np/np_profile_dialog.h" #include "core/libraries/np/np_score.h" @@ -105,6 +106,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Np::NpSnsFacebookDialog::RegisterLib(sym); Libraries::Np::NpAuth::RegisterLib(sym); Libraries::Np::NpParty::RegisterLib(sym); + Libraries::Np::NpPartner::RegisterLib(sym); Libraries::ScreenShot::RegisterLib(sym); Libraries::AppContent::RegisterLib(sym); Libraries::PngDec::RegisterLib(sym); diff --git a/src/core/libraries/np/np_partner.cpp b/src/core/libraries/np/np_partner.cpp new file mode 100644 index 000000000..447144344 --- /dev/null +++ b/src/core/libraries/np/np_partner.cpp @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" +#include "core/libraries/np/np_partner.h" +#include "core/libraries/np/np_partner_error.h" +#include "core/libraries/system/userservice.h" + +namespace Libraries::Np::NpPartner { + +static bool g_library_init = false; +std::mutex g_library_mutex{}; + +/** + * Terminates the library + */ +s32 PS4_SYSV_ABI Func_A4CC5784DA33517F() { + LOG_ERROR(Lib_NpPartner, "(STUBBED) called"); + if (!g_library_init) { + return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED; + } + std::scoped_lock lk{g_library_mutex}; + g_library_init = false; + return ORBIS_OK; +} + +/** + * Aborts requests started by Func_F8E9DB52CD425743 + */ +s32 PS4_SYSV_ABI Func_A507D84D91F39CC7() { + LOG_ERROR(Lib_NpPartner, "(STUBBED) called"); + if (!g_library_init) { + return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED; + } + // Request logic is unimplemented, so this does nothing. + return ORBIS_OK; +} + +/** + * Initializes the library + */ +s32 PS4_SYSV_ABI Func_EC2C48E74FF19429() { + LOG_ERROR(Lib_NpPartner, "(STUBBED) called"); + g_library_init = true; + // Also retrieves and sends compiled SDK version to server. + return ORBIS_OK; +} + +/** + * Creates an NP request to determine if the user has a subscription to EA's services. + */ +s32 PS4_SYSV_ABI Func_F8E9DB52CD425743(UserService::OrbisUserServiceUserId user_id, bool* result) { + LOG_ERROR(Lib_NpPartner, "(STUBBED) called"); + if (!g_library_init) { + return ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED; + } + if (result == nullptr) { + return ORBIS_NP_PARTNER_ERROR_INVALID_ARGUMENT; + } + std::scoped_lock lk{g_library_mutex}; + // In the real library, this creates and sends a request that checks for EA subscription, + // then waits for the request to return a response, and returns that response. + // NP signed out likely returns an error, but I haven't figured out the error code yet. + // For now, stub having no subscription. + *result = false; + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("pMxXhNozUX8", "libSceNpPartner001", 1, "libSceNpPartner001", + Func_A4CC5784DA33517F); + LIB_FUNCTION("pQfYTZHznMc", "libSceNpPartner001", 1, "libSceNpPartner001", + Func_A507D84D91F39CC7); + LIB_FUNCTION("7CxI50-xlCk", "libSceNpPartner001", 1, "libSceNpPartner001", + Func_EC2C48E74FF19429); + LIB_FUNCTION("+OnbUs1CV0M", "libSceNpPartner001", 1, "libSceNpPartner001", + Func_F8E9DB52CD425743); +}; + +} // namespace Libraries::Np::NpPartner \ No newline at end of file diff --git a/src/core/libraries/np/np_partner.h b/src/core/libraries/np/np_partner.h new file mode 100644 index 000000000..4cb2a6f5f --- /dev/null +++ b/src/core/libraries/np/np_partner.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Np::NpPartner { + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Np::NpPartner \ No newline at end of file diff --git a/src/core/libraries/np/np_partner_error.h b/src/core/libraries/np/np_partner_error.h new file mode 100644 index 000000000..be1c7c594 --- /dev/null +++ b/src/core/libraries/np/np_partner_error.h @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/libraries/error_codes.h" + +constexpr int ORBIS_NP_PARTNER_ERROR_NOT_INITIALIZED = 0x819d0001; +constexpr int ORBIS_NP_PARTNER_ERROR_INVALID_ARGUMENT = 0x819d0002; \ No newline at end of file From 25d175fd41882bd43d9644bc0e98fce3fdd6c535 Mon Sep 17 00:00:00 2001 From: Berk Date: Thu, 29 Jan 2026 15:43:15 +0300 Subject: [PATCH 09/50] [clang-format] Added correct version and vscode formatter (#3971) --- .../.devcontainer/devcontainer.json | 21 +++++++++++-------- documents/Docker Builder/.docker/Dockerfile | 3 +++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/documents/Docker Builder/.devcontainer/devcontainer.json b/documents/Docker Builder/.devcontainer/devcontainer.json index 9093f6722..1139ffa33 100644 --- a/documents/Docker Builder/.devcontainer/devcontainer.json +++ b/documents/Docker Builder/.devcontainer/devcontainer.json @@ -17,8 +17,9 @@ "customizations": { "vscode": { "extensions": [ - "llvm-vs-code-extensions.vscode-clangd", - "ms-vscode.cmake-tools" + "llvm-vs-code-extensions.vscode-clangd", + "ms-vscode.cmake-tools", + "xaver.clang-format" ], "settings": { "clangd.arguments": [ @@ -26,23 +27,25 @@ "--clang-tidy", "--completion-style=detailed", "--header-insertion=never", - "--compile-commands-dir=/workspaces/shadPS4/Build/x64-Clang-Release" + "--compile-commands-dir=/workspaces/shadPS4/Build/x64-Clang-Release" ], - "C_Cpp.intelliSenseEngine": "Disabled" + "C_Cpp.intelliSenseEngine": "Disabled" } } }, "settings": { - "cmake.configureOnOpen": false, + "cmake.configureOnOpen": false, "cmake.generator": "Unix Makefiles", "cmake.environment": { "CC": "clang", "CXX": "clang++" }, "cmake.configureEnvironment": { - "CMAKE_CXX_STANDARD": "23", - "CMAKE_CXX_STANDARD_REQUIRED": "ON", - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" - } + "CMAKE_CXX_STANDARD": "23", + "CMAKE_CXX_STANDARD_REQUIRED": "ON", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "editor.formatOnSave": true, + "clang-format.executable": "clang-format-19" } } \ No newline at end of file diff --git a/documents/Docker Builder/.docker/Dockerfile b/documents/Docker Builder/.docker/Dockerfile index b168a6a72..6ca9b2da5 100644 --- a/documents/Docker Builder/.docker/Dockerfile +++ b/documents/Docker Builder/.docker/Dockerfile @@ -10,6 +10,7 @@ RUN pacman-key --init && \ RUN pacman -S --noconfirm \ base-devel \ clang \ + clang19 \ ninja \ git \ ca-certificates \ @@ -38,5 +39,7 @@ RUN pacman -S --noconfirm \ libxinerama \ libxss \ && pacman -Scc --noconfirm + +RUN ln -sf /usr/lib/llvm19/bin/clang-format /usr/bin/clang-format-19 WORKDIR /workspaces/shadPS4 \ No newline at end of file From 39a7738d0418a994726ba15ff0ed17591c8b9bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Thu, 29 Jan 2026 16:46:30 +0100 Subject: [PATCH 10/50] Include NpTus stub (#3970) --- CMakeLists.txt | 2 + src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + src/core/libraries/libs.cpp | 2 + src/core/libraries/np/np_tus.cpp | 1002 ++++++++++++++++++++++++++++++ src/core/libraries/np/np_tus.h | 158 +++++ 6 files changed, 1166 insertions(+) create mode 100644 src/core/libraries/np/np_tus.cpp create mode 100644 src/core/libraries/np/np_tus.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0299d242b..6deca97db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -585,6 +585,8 @@ set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/np_score.h src/core/libraries/np/np_trophy.cpp src/core/libraries/np/np_trophy.h + src/core/libraries/np/np_tus.cpp + src/core/libraries/np/np_tus.h src/core/libraries/np/trophy_ui.cpp src/core/libraries/np/trophy_ui.h src/core/libraries/np/np_web_api.cpp diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 22ee8ddb3..85946e9c4 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -109,6 +109,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, NpManager) \ SUB(Lib, NpScore) \ SUB(Lib, NpTrophy) \ + SUB(Lib, NpTus) \ SUB(Lib, NpWebApi) \ SUB(Lib, NpWebApi2) \ SUB(Lib, NpProfileDialog) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index ed7bbaa3f..127377115 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -76,6 +76,7 @@ enum class Class : u8 { Lib_NpManager, ///< The LibSceNpManager implementation Lib_NpScore, ///< The LibSceNpScore implementation Lib_NpTrophy, ///< The LibSceNpTrophy implementation + Lib_NpTus, ///< The LibSceNpTus implementation Lib_NpWebApi, ///< The LibSceWebApi implementation Lib_NpWebApi2, ///< The LibSceWebApi2 implementation Lib_NpProfileDialog, ///< The LibSceNpProfileDialog implementation diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 30fed4fee..7e8edccec 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -41,6 +41,7 @@ #include "core/libraries/np/np_score.h" #include "core/libraries/np/np_sns_facebook_dialog.h" #include "core/libraries/np/np_trophy.h" +#include "core/libraries/np/np_tus.h" #include "core/libraries/np/np_web_api.h" #include "core/libraries/np/np_web_api2.h" #include "core/libraries/pad/pad.h" @@ -107,6 +108,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Np::NpAuth::RegisterLib(sym); Libraries::Np::NpParty::RegisterLib(sym); Libraries::Np::NpPartner::RegisterLib(sym); + Libraries::Np::NpTus::RegisterLib(sym); Libraries::ScreenShot::RegisterLib(sym); Libraries::AppContent::RegisterLib(sym); Libraries::PngDec::RegisterLib(sym); diff --git a/src/core/libraries/np/np_tus.cpp b/src/core/libraries/np/np_tus.cpp new file mode 100644 index 000000000..e0d0aaad1 --- /dev/null +++ b/src/core/libraries/np/np_tus.cpp @@ -0,0 +1,1002 @@ +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" +#include "core/libraries/np/np_tus.h" + +namespace Libraries::Np::NpTus { + +s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtx() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariable() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtx() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotData() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariable() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetData() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatus() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariable() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatus() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariable() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatus() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariable() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetData() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetDataAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetDataVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetDataVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariable() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariable() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtxA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTssGetData() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTssGetDataAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTssGetSmallStorage() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTssGetSmallStorageAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTssGetStorage() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTssGetStorageAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAbortRequest() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSave() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSaveAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSaveVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusChangeModeForOtherSaveDataOwners() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtxA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusCreateRequest() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusCreateTitleCtx() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteNpTitleCtx() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusDeleteRequest() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataAVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataAVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSave() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSaveAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSaveVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSaveVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusForCrossSave() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusForCrossSaveAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableForCrossSave() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableForCrossSaveAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSave() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSave() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSaveAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSaveVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSave() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSave() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusPollAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetDataA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetDataAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetDataAVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetDataAVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetThreadParam() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusSetTimeout() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableA() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSave() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveVUser() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpTusWaitAsync() { + LOG_ERROR(Lib_NpTus, "(STUBBED) called"); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("sRVb2Cf0GHg", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTssCreateNpTitleCtx); + LIB_FUNCTION("cRVmNrJDbG8", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusAddAndGetVariable); + LIB_FUNCTION("Q2UmHdK04c8", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusAddAndGetVariableAsync); + LIB_FUNCTION("ukr6FBSrkJw", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusAddAndGetVariableVUser); + LIB_FUNCTION("lliK9T6ylJg", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusAddAndGetVariableVUserAsync); + LIB_FUNCTION("BIkMmUfNKWM", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusCreateNpTitleCtx); + LIB_FUNCTION("0DT5bP6YzBo", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusDeleteMultiSlotData); + LIB_FUNCTION("OCozl1ZtxRY", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusDeleteMultiSlotDataAsync); + LIB_FUNCTION("mYhbiRtkE1Y", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusDeleteMultiSlotVariable); + LIB_FUNCTION("0nDVqcYECoM", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusDeleteMultiSlotVariableAsync); + LIB_FUNCTION("XOzszO4ONWU", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusGetData); + LIB_FUNCTION("uHtKS5V1T5k", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusGetDataAsync); + LIB_FUNCTION("GQHCksS7aLs", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusGetDataVUser); + LIB_FUNCTION("5R6kI-8f+Hk", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusGetDataVUserAsync); + LIB_FUNCTION("DXigwIBTjWE", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetFriendsDataStatus); + LIB_FUNCTION("LUwvy0MOSqw", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetFriendsDataStatusAsync); + LIB_FUNCTION("cy+pAALkHp8", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusGetFriendsVariable); + LIB_FUNCTION("YFYWOwYI6DY", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetFriendsVariableAsync); + LIB_FUNCTION("pgcNwFHoOL4", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatus); + LIB_FUNCTION("Qyek420uZmM", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusAsync); + LIB_FUNCTION("NGCeFUl5ckM", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusVUser); + LIB_FUNCTION("bHWFSg6jvXc", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusVUserAsync); + LIB_FUNCTION("F+eQlfcka98", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariable); + LIB_FUNCTION("bcPB2rnhQqo", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableAsync); + LIB_FUNCTION("uFxVYJEkcmc", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableVUser); + LIB_FUNCTION("qp-rTrq1klk", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableVUserAsync); + LIB_FUNCTION("NvHjFkx2rnU", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatus); + LIB_FUNCTION("0zkr0T+NYvI", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusAsync); + LIB_FUNCTION("xwJIlK0bHgA", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusVUser); + LIB_FUNCTION("I5dlIKkHNkQ", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusVUserAsync); + LIB_FUNCTION("6G9+4eIb+cY", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiUserVariable); + LIB_FUNCTION("YRje5yEXS0U", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableAsync); + LIB_FUNCTION("zB0vaHTzA6g", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableVUser); + LIB_FUNCTION("xZXQuNSTC6o", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableVUserAsync); + LIB_FUNCTION("4NrufkNCkiE", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusSetData); + LIB_FUNCTION("G68xdfQuiyU", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusSetDataAsync); + LIB_FUNCTION("+RhzSuuXwxo", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusSetDataVUser); + LIB_FUNCTION("E4BCVfx-YfM", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusSetDataVUserAsync); + LIB_FUNCTION("c6aYoa47YgI", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusSetMultiSlotVariable); + LIB_FUNCTION("5J9GGMludxY", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusSetMultiSlotVariableAsync); + LIB_FUNCTION("ukC55HsotJ4", "libSceNpTusCompat", 1, "libSceNpTus", sceNpTusTryAndSetVariable); + LIB_FUNCTION("xQfR51i4kck", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusTryAndSetVariableAsync); + LIB_FUNCTION("ZbitD262GhY", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusTryAndSetVariableVUser); + LIB_FUNCTION("trZ6QGW6jHs", "libSceNpTusCompat", 1, "libSceNpTus", + sceNpTusTryAndSetVariableVUserAsync); + LIB_FUNCTION("sRVb2Cf0GHg", "libSceNpTus", 1, "libSceNpTus", sceNpTssCreateNpTitleCtx); + LIB_FUNCTION("lBtrk+7lk14", "libSceNpTus", 1, "libSceNpTus", sceNpTssCreateNpTitleCtxA); + LIB_FUNCTION("-SUR+UoLS6c", "libSceNpTus", 1, "libSceNpTus", sceNpTssGetData); + LIB_FUNCTION("DS2yu3Sjj1o", "libSceNpTus", 1, "libSceNpTus", sceNpTssGetDataAsync); + LIB_FUNCTION("lL+Z3zCKNTs", "libSceNpTus", 1, "libSceNpTus", sceNpTssGetSmallStorage); + LIB_FUNCTION("f2Pe4LGS2II", "libSceNpTus", 1, "libSceNpTus", sceNpTssGetSmallStorageAsync); + LIB_FUNCTION("IVSbAEOxJ6I", "libSceNpTus", 1, "libSceNpTus", sceNpTssGetStorage); + LIB_FUNCTION("k5NZIzggbuk", "libSceNpTus", 1, "libSceNpTus", sceNpTssGetStorageAsync); + LIB_FUNCTION("2eq1bMwgZYo", "libSceNpTus", 1, "libSceNpTus", sceNpTusAbortRequest); + LIB_FUNCTION("cRVmNrJDbG8", "libSceNpTus", 1, "libSceNpTus", sceNpTusAddAndGetVariable); + LIB_FUNCTION("wPFah4-5Xec", "libSceNpTus", 1, "libSceNpTus", sceNpTusAddAndGetVariableA); + LIB_FUNCTION("2dB427dT3Iw", "libSceNpTus", 1, "libSceNpTus", sceNpTusAddAndGetVariableAAsync); + LIB_FUNCTION("Q2UmHdK04c8", "libSceNpTus", 1, "libSceNpTus", sceNpTusAddAndGetVariableAsync); + LIB_FUNCTION("Nt1runsPVJc", "libSceNpTus", 1, "libSceNpTus", sceNpTusAddAndGetVariableAVUser); + LIB_FUNCTION("GjlEgLCh4DY", "libSceNpTus", 1, "libSceNpTus", + sceNpTusAddAndGetVariableAVUserAsync); + LIB_FUNCTION("EPeq43CQKxY", "libSceNpTus", 1, "libSceNpTus", + sceNpTusAddAndGetVariableForCrossSave); + LIB_FUNCTION("mXZi1D2xwZE", "libSceNpTus", 1, "libSceNpTus", + sceNpTusAddAndGetVariableForCrossSaveAsync); + LIB_FUNCTION("4VLlu7EIjzk", "libSceNpTus", 1, "libSceNpTus", + sceNpTusAddAndGetVariableForCrossSaveVUser); + LIB_FUNCTION("6Lu9geO5TiA", "libSceNpTus", 1, "libSceNpTus", + sceNpTusAddAndGetVariableForCrossSaveVUserAsync); + LIB_FUNCTION("ukr6FBSrkJw", "libSceNpTus", 1, "libSceNpTus", sceNpTusAddAndGetVariableVUser); + LIB_FUNCTION("lliK9T6ylJg", "libSceNpTus", 1, "libSceNpTus", + sceNpTusAddAndGetVariableVUserAsync); + LIB_FUNCTION("wjNhItL2wzg", "libSceNpTus", 1, "libSceNpTus", + sceNpTusChangeModeForOtherSaveDataOwners); + LIB_FUNCTION("BIkMmUfNKWM", "libSceNpTus", 1, "libSceNpTus", sceNpTusCreateNpTitleCtx); + LIB_FUNCTION("1n-dGukBgnY", "libSceNpTus", 1, "libSceNpTus", sceNpTusCreateNpTitleCtxA); + LIB_FUNCTION("3bh2aBvvmvM", "libSceNpTus", 1, "libSceNpTus", sceNpTusCreateRequest); + LIB_FUNCTION("hhy8+oecGac", "libSceNpTus", 1, "libSceNpTus", sceNpTusCreateTitleCtx); + LIB_FUNCTION("0DT5bP6YzBo", "libSceNpTus", 1, "libSceNpTus", sceNpTusDeleteMultiSlotData); + LIB_FUNCTION("iXzUOM9sXU0", "libSceNpTus", 1, "libSceNpTus", sceNpTusDeleteMultiSlotDataA); + LIB_FUNCTION("6-+Yqc-NppQ", "libSceNpTus", 1, "libSceNpTus", sceNpTusDeleteMultiSlotDataAAsync); + LIB_FUNCTION("OCozl1ZtxRY", "libSceNpTus", 1, "libSceNpTus", sceNpTusDeleteMultiSlotDataAsync); + LIB_FUNCTION("xutwCvsydkk", "libSceNpTus", 1, "libSceNpTus", sceNpTusDeleteMultiSlotDataVUser); + LIB_FUNCTION("zDeH4tr+0cQ", "libSceNpTus", 1, "libSceNpTus", + sceNpTusDeleteMultiSlotDataVUserAsync); + LIB_FUNCTION("mYhbiRtkE1Y", "libSceNpTus", 1, "libSceNpTus", sceNpTusDeleteMultiSlotVariable); + LIB_FUNCTION("pwnE9Oa1uF8", "libSceNpTus", 1, "libSceNpTus", sceNpTusDeleteMultiSlotVariableA); + LIB_FUNCTION("NQIw7tzo0Ow", "libSceNpTus", 1, "libSceNpTus", + sceNpTusDeleteMultiSlotVariableAAsync); + LIB_FUNCTION("0nDVqcYECoM", "libSceNpTus", 1, "libSceNpTus", + sceNpTusDeleteMultiSlotVariableAsync); + LIB_FUNCTION("o02Mtf8G6V0", "libSceNpTus", 1, "libSceNpTus", + sceNpTusDeleteMultiSlotVariableVUser); + LIB_FUNCTION("WCzd3cxhubo", "libSceNpTus", 1, "libSceNpTus", + sceNpTusDeleteMultiSlotVariableVUserAsync); + LIB_FUNCTION("H3uq7x0sZOI", "libSceNpTus", 1, "libSceNpTus", sceNpTusDeleteNpTitleCtx); + LIB_FUNCTION("CcIH40dYS88", "libSceNpTus", 1, "libSceNpTus", sceNpTusDeleteRequest); + LIB_FUNCTION("XOzszO4ONWU", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetData); + LIB_FUNCTION("yWEHUFkY1qI", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataA); + LIB_FUNCTION("xzG8mG9YlKY", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataAAsync); + LIB_FUNCTION("uHtKS5V1T5k", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataAsync); + LIB_FUNCTION("iaH+Sxlw32k", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataAVUser); + LIB_FUNCTION("uoFvgzwawAY", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataAVUserAsync); + LIB_FUNCTION("1TE3OvH61qo", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataForCrossSave); + LIB_FUNCTION("CFPx3eyaT34", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataForCrossSaveAsync); + LIB_FUNCTION("-LxFGYCJwww", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataForCrossSaveVUser); + LIB_FUNCTION("B7rBR0CoYLI", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetDataForCrossSaveVUserAsync); + LIB_FUNCTION("GQHCksS7aLs", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataVUser); + LIB_FUNCTION("5R6kI-8f+Hk", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetDataVUserAsync); + LIB_FUNCTION("DXigwIBTjWE", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetFriendsDataStatus); + LIB_FUNCTION("yixh7HDKWfk", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetFriendsDataStatusA); + LIB_FUNCTION("OheijxY5RYE", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetFriendsDataStatusAAsync); + LIB_FUNCTION("LUwvy0MOSqw", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetFriendsDataStatusAsync); + LIB_FUNCTION("TDoqRD+CE+M", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetFriendsDataStatusForCrossSave); + LIB_FUNCTION("68B6XDgSANk", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetFriendsDataStatusForCrossSaveAsync); + LIB_FUNCTION("cy+pAALkHp8", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetFriendsVariable); + LIB_FUNCTION("C8TY-UnQoXg", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetFriendsVariableA); + LIB_FUNCTION("wrImtTqUSGM", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetFriendsVariableAAsync); + LIB_FUNCTION("YFYWOwYI6DY", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetFriendsVariableAsync); + LIB_FUNCTION("mD6s8HtMdpk", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetFriendsVariableForCrossSave); + LIB_FUNCTION("FabW3QpY3gQ", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetFriendsVariableForCrossSaveAsync); + LIB_FUNCTION("pgcNwFHoOL4", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiSlotDataStatus); + LIB_FUNCTION("833Y2TnyonE", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiSlotDataStatusA); + LIB_FUNCTION("7uLPqiNvNLc", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusAAsync); + LIB_FUNCTION("Qyek420uZmM", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusAsync); + LIB_FUNCTION("azmjx3jBAZA", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusAVUser); + LIB_FUNCTION("668Ij9MYKEU", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusAVUserAsync); + LIB_FUNCTION("DgpRToHWN40", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusForCrossSave); + LIB_FUNCTION("LQ6CoHcp+ug", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusForCrossSaveAsync); + LIB_FUNCTION("KBfBmtxCdmI", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusForCrossSaveVUser); + LIB_FUNCTION("4UF2uu2eDCo", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync); + LIB_FUNCTION("NGCeFUl5ckM", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusVUser); + LIB_FUNCTION("bHWFSg6jvXc", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotDataStatusVUserAsync); + LIB_FUNCTION("F+eQlfcka98", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiSlotVariable); + LIB_FUNCTION("GDXlRTxgd+M", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiSlotVariableA); + LIB_FUNCTION("2BnPSY1Oxd8", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableAAsync); + LIB_FUNCTION("bcPB2rnhQqo", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiSlotVariableAsync); + LIB_FUNCTION("AsziNQ9X2uk", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableAVUser); + LIB_FUNCTION("y-DJK+d+leg", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableAVUserAsync); + LIB_FUNCTION("m9XZnxw9AmE", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableForCrossSave); + LIB_FUNCTION("DFlBYT+Lm2I", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableForCrossSaveAsync); + LIB_FUNCTION("wTuuw4-6HI8", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableForCrossSaveVUser); + LIB_FUNCTION("DPcu0qWsd7Q", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync); + LIB_FUNCTION("uFxVYJEkcmc", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiSlotVariableVUser); + LIB_FUNCTION("qp-rTrq1klk", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiSlotVariableVUserAsync); + LIB_FUNCTION("NvHjFkx2rnU", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiUserDataStatus); + LIB_FUNCTION("lxNDPDnWfMc", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiUserDataStatusA); + LIB_FUNCTION("kt+k6jegYZ8", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusAAsync); + LIB_FUNCTION("0zkr0T+NYvI", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusAsync); + LIB_FUNCTION("fJU2TZId210", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusAVUser); + LIB_FUNCTION("WBh3zfrjS38", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusAVUserAsync); + LIB_FUNCTION("cVeBif6zdZ4", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusForCrossSave); + LIB_FUNCTION("lq0Anwhj0wY", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusForCrossSaveAsync); + LIB_FUNCTION("w-c7U0MW2KY", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusForCrossSaveVUser); + LIB_FUNCTION("H6sQJ99usfE", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync); + LIB_FUNCTION("xwJIlK0bHgA", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusVUser); + LIB_FUNCTION("I5dlIKkHNkQ", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserDataStatusVUserAsync); + LIB_FUNCTION("6G9+4eIb+cY", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiUserVariable); + LIB_FUNCTION("Gjixv5hqRVY", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiUserVariableA); + LIB_FUNCTION("eGunerNP9n0", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableAAsync); + LIB_FUNCTION("YRje5yEXS0U", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiUserVariableAsync); + LIB_FUNCTION("fVvocpq4mG4", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableAVUser); + LIB_FUNCTION("V8ZA3hHrAbw", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableAVUserAsync); + LIB_FUNCTION("Q5uQeScvTPE", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableForCrossSave); + LIB_FUNCTION("oZ8DMeTU-50", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableForCrossSaveAsync); + LIB_FUNCTION("Djuj2+1VNL0", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableForCrossSaveVUser); + LIB_FUNCTION("82RP7itI-zI", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableForCrossSaveVUserAsync); + LIB_FUNCTION("zB0vaHTzA6g", "libSceNpTus", 1, "libSceNpTus", sceNpTusGetMultiUserVariableVUser); + LIB_FUNCTION("xZXQuNSTC6o", "libSceNpTus", 1, "libSceNpTus", + sceNpTusGetMultiUserVariableVUserAsync); + LIB_FUNCTION("t7b6dmpQNiI", "libSceNpTus", 1, "libSceNpTus", sceNpTusPollAsync); + LIB_FUNCTION("4NrufkNCkiE", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetData); + LIB_FUNCTION("VzxN3tOouj8", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetDataA); + LIB_FUNCTION("4u58d6g6uwU", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetDataAAsync); + LIB_FUNCTION("G68xdfQuiyU", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetDataAsync); + LIB_FUNCTION("kbWqOt3QjKU", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetDataAVUser); + LIB_FUNCTION("Fmx4tapJGzo", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetDataAVUserAsync); + LIB_FUNCTION("+RhzSuuXwxo", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetDataVUser); + LIB_FUNCTION("E4BCVfx-YfM", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetDataVUserAsync); + LIB_FUNCTION("c6aYoa47YgI", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetMultiSlotVariable); + LIB_FUNCTION("cf-WMA0jYCc", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetMultiSlotVariableA); + LIB_FUNCTION("ypMObSwfcns", "libSceNpTus", 1, "libSceNpTus", + sceNpTusSetMultiSlotVariableAAsync); + LIB_FUNCTION("5J9GGMludxY", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetMultiSlotVariableAsync); + LIB_FUNCTION("1Cz0hTJFyh4", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetMultiSlotVariableVUser); + LIB_FUNCTION("CJAxTxQdwHM", "libSceNpTus", 1, "libSceNpTus", + sceNpTusSetMultiSlotVariableVUserAsync); + LIB_FUNCTION("6GKDdRCFx8c", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetThreadParam); + LIB_FUNCTION("KMlHj+tgfdQ", "libSceNpTus", 1, "libSceNpTus", sceNpTusSetTimeout); + LIB_FUNCTION("ukC55HsotJ4", "libSceNpTus", 1, "libSceNpTus", sceNpTusTryAndSetVariable); + LIB_FUNCTION("0up4MP1wNtc", "libSceNpTus", 1, "libSceNpTus", sceNpTusTryAndSetVariableA); + LIB_FUNCTION("bGTjTkHPHTE", "libSceNpTus", 1, "libSceNpTus", sceNpTusTryAndSetVariableAAsync); + LIB_FUNCTION("xQfR51i4kck", "libSceNpTus", 1, "libSceNpTus", sceNpTusTryAndSetVariableAsync); + LIB_FUNCTION("oGIcxlUabSA", "libSceNpTus", 1, "libSceNpTus", sceNpTusTryAndSetVariableAVUser); + LIB_FUNCTION("uf77muc5Bog", "libSceNpTus", 1, "libSceNpTus", + sceNpTusTryAndSetVariableAVUserAsync); + LIB_FUNCTION("MGvSJEHwyL8", "libSceNpTus", 1, "libSceNpTus", + sceNpTusTryAndSetVariableForCrossSave); + LIB_FUNCTION("JKGYZ2F1yT8", "libSceNpTus", 1, "libSceNpTus", + sceNpTusTryAndSetVariableForCrossSaveAsync); + LIB_FUNCTION("fcCwKpi4CbU", "libSceNpTus", 1, "libSceNpTus", + sceNpTusTryAndSetVariableForCrossSaveVUser); + LIB_FUNCTION("CjVIpztpTNc", "libSceNpTus", 1, "libSceNpTus", + sceNpTusTryAndSetVariableForCrossSaveVUserAsync); + LIB_FUNCTION("ZbitD262GhY", "libSceNpTus", 1, "libSceNpTus", sceNpTusTryAndSetVariableVUser); + LIB_FUNCTION("trZ6QGW6jHs", "libSceNpTus", 1, "libSceNpTus", + sceNpTusTryAndSetVariableVUserAsync); + LIB_FUNCTION("hYPJFWzFPjA", "libSceNpTus", 1, "libSceNpTus", sceNpTusWaitAsync); +}; + +} // namespace Libraries::Np::NpTus \ No newline at end of file diff --git a/src/core/libraries/np/np_tus.h b/src/core/libraries/np/np_tus.h new file mode 100644 index 000000000..3c18099b2 --- /dev/null +++ b/src/core/libraries/np/np_tus.h @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Np::NpTus { + +s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtx(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariable(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAsync(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableVUser(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtx(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotData(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataAsync(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariable(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableAsync(); +s32 PS4_SYSV_ABI sceNpTusGetData(); +s32 PS4_SYSV_ABI sceNpTusGetDataAsync(); +s32 PS4_SYSV_ABI sceNpTusGetDataVUser(); +s32 PS4_SYSV_ABI sceNpTusGetDataVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatus(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusAsync(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariable(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatus(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariable(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatus(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariable(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusSetData(); +s32 PS4_SYSV_ABI sceNpTusSetDataAsync(); +s32 PS4_SYSV_ABI sceNpTusSetDataVUser(); +s32 PS4_SYSV_ABI sceNpTusSetDataVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariable(); +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableAsync(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariable(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAsync(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableVUser(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableVUserAsync(); +s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtxA(); +s32 PS4_SYSV_ABI sceNpTssGetData(); +s32 PS4_SYSV_ABI sceNpTssGetDataAsync(); +s32 PS4_SYSV_ABI sceNpTssGetSmallStorage(); +s32 PS4_SYSV_ABI sceNpTssGetSmallStorageAsync(); +s32 PS4_SYSV_ABI sceNpTssGetStorage(); +s32 PS4_SYSV_ABI sceNpTssGetStorageAsync(); +s32 PS4_SYSV_ABI sceNpTusAbortRequest(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableA(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAAsync(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAVUser(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSave(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSaveAsync(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSaveVUser(); +s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusChangeModeForOtherSaveDataOwners(); +s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtxA(); +s32 PS4_SYSV_ABI sceNpTusCreateRequest(); +s32 PS4_SYSV_ABI sceNpTusCreateTitleCtx(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataA(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataAAsync(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataVUser(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableA(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableAAsync(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableVUser(); +s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusDeleteNpTitleCtx(); +s32 PS4_SYSV_ABI sceNpTusDeleteRequest(); +s32 PS4_SYSV_ABI sceNpTusGetDataA(); +s32 PS4_SYSV_ABI sceNpTusGetDataAAsync(); +s32 PS4_SYSV_ABI sceNpTusGetDataAVUser(); +s32 PS4_SYSV_ABI sceNpTusGetDataAVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSave(); +s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSaveAsync(); +s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSaveVUser(); +s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSaveVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusA(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusAAsync(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusForCrossSave(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusForCrossSaveAsync(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableA(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableAAsync(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableForCrossSave(); +s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableForCrossSaveAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusA(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSave(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableA(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSave(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSaveAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSaveVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusA(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSave(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableA(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSave(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveAsync(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveVUser(); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusPollAsync(); +s32 PS4_SYSV_ABI sceNpTusSetDataA(); +s32 PS4_SYSV_ABI sceNpTusSetDataAAsync(); +s32 PS4_SYSV_ABI sceNpTusSetDataAVUser(); +s32 PS4_SYSV_ABI sceNpTusSetDataAVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableA(); +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableAAsync(); +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableVUser(); +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusSetThreadParam(); +s32 PS4_SYSV_ABI sceNpTusSetTimeout(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableA(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAAsync(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAVUser(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSave(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveAsync(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveVUser(); +s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(); +s32 PS4_SYSV_ABI sceNpTusWaitAsync(); + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Np::NpTus \ No newline at end of file From 4f3aabd7af0e28e45ad103d79eef64bf59398424 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:08:21 -0600 Subject: [PATCH 11/50] Delete unused fds in sceNetEpollDestroy and sys_socketclose (#3976) This issue would cause memory leaks in some EA titles, also just generally makes it harder to debug stuff when the fd table is flooded with closed sockets and epolls. --- src/core/libraries/network/net.cpp | 1 + src/core/libraries/network/sys_net.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/src/core/libraries/network/net.cpp b/src/core/libraries/network/net.cpp index 9a4f05a5e..102447952 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -803,6 +803,7 @@ int PS4_SYSV_ABI sceNetEpollDestroy(OrbisNetId epollid) { LOG_DEBUG(Lib_Net, "called, epollid = {} ({})", epollid, file->epoll->name); file->epoll->Destroy(); + FDTable::Instance()->DeleteHandle(epollid); return ORBIS_OK; } diff --git a/src/core/libraries/network/sys_net.cpp b/src/core/libraries/network/sys_net.cpp index 6d7876c06..76107d323 100644 --- a/src/core/libraries/network/sys_net.cpp +++ b/src/core/libraries/network/sys_net.cpp @@ -335,6 +335,7 @@ int PS4_SYSV_ABI sys_socketclose(OrbisNetId s) { LOG_DEBUG(Lib_Net, "s = {} ({})", s, file->m_guest_name); int returncode = file->socket->Close(); if (returncode >= 0) { + FDTable::Instance()->DeleteHandle(s); return returncode; } LOG_ERROR(Lib_Net, "error code returned: {}", (u32)*Libraries::Kernel::__Error()); From 5bc4183e36908ce249ce0c62cabd32d708bc75f1 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:22:15 -0600 Subject: [PATCH 12/50] Kernel.Vmm: Fix potential race condition involving concurrent Allocate and Free calls (#3978) * Avoid nullptr dereference on GetSocket Was gonna include this in my socket PR, but that got merged before I could push this. * Lock unmap mutex in PoolExpand and Allocate PAYDAY 2 has a rare race condition involving dmem releases. I'm not certain this commit will fix it, but this would cause a race condition that could cause asserts like what PAYDAY 2 can hit, so I'll just pray this does the job until I can prove it doesn't. --- src/core/file_sys/fs.cpp | 3 +++ src/core/memory.cpp | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index f6c34ae94..cba95fe37 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -232,6 +232,9 @@ File* HandleTable::GetSocket(int d) { return nullptr; } auto file = m_files.at(d); + if (!file) { + return nullptr; + } if (file->type != Core::FileSys::FileType::Socket) { return nullptr; } diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 1aeecebf1..90759c6cd 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -177,7 +177,7 @@ bool MemoryManager::TryWriteBacking(void* address, const void* data, u64 size) { } PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, u64 size, u64 alignment) { - std::scoped_lock lk{mutex}; + std::scoped_lock lk{mutex, unmap_mutex}; alignment = alignment > 0 ? alignment : 64_KB; auto dmem_area = FindDmemArea(search_start); @@ -219,7 +219,7 @@ PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, u64 size, PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, u64 size, u64 alignment, s32 memory_type) { - std::scoped_lock lk{mutex}; + std::scoped_lock lk{mutex, unmap_mutex}; alignment = alignment > 0 ? alignment : 16_KB; auto dmem_area = FindDmemArea(search_start); From 4f11a8c979de05cc97a236c2346c0f3343dc4034 Mon Sep 17 00:00:00 2001 From: CrazyBloo Date: Fri, 30 Jan 2026 03:53:09 -0500 Subject: [PATCH 13/50] pngenc hle (#3957) * PngEnc hle * format * formatting + fix scePngEncDelete * fix cmake + misc improvements i think the setjmp is right according to the libpng manual, works fine from my testing * fixes fix an issue with how alpha was handled, and PngEncode() now properly sets the processed_height in outputInfo. * format * Update pngenc.cpp * set outputInfo->processed_height during png write i assume some games will use this for error handling --- CMakeLists.txt | 3 + src/core/libraries/libpng/pngenc.cpp | 266 +++++++++++++++++++++++ src/core/libraries/libpng/pngenc.h | 67 ++++++ src/core/libraries/libpng/pngenc_error.h | 14 ++ src/emulator.cpp | 3 +- 5 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 src/core/libraries/libpng/pngenc.cpp create mode 100644 src/core/libraries/libpng/pngenc.h create mode 100644 src/core/libraries/libpng/pngenc_error.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6deca97db..08e8fe161 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -524,6 +524,9 @@ set(SYSTEM_GESTURE_LIB set(PNG_LIB src/core/libraries/libpng/pngdec.cpp src/core/libraries/libpng/pngdec.h src/core/libraries/libpng/pngdec_error.h + src/core/libraries/libpng/pngenc.cpp + src/core/libraries/libpng/pngenc.h + src/core/libraries/libpng/pngenc_error.h ) set(JPEG_LIB src/core/libraries/jpeg/jpeg_error.h diff --git a/src/core/libraries/libpng/pngenc.cpp b/src/core/libraries/libpng/pngenc.cpp new file mode 100644 index 000000000..a17adbf71 --- /dev/null +++ b/src/core/libraries/libpng/pngenc.cpp @@ -0,0 +1,266 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libpng/pngenc.h" +#include "core/libraries/libs.h" + +#include "pngenc_error.h" + +namespace Libraries::PngEnc { + +struct PngHandler { + png_structp png_ptr; + png_infop info_ptr; +}; + +struct PngWriter { + u8* cursor; + u8* start; + size_t capacity; + bool cancel_write; +}; + +static inline int MapPngFilter(u16 filter) { + if (filter == (u16)OrbisPngEncFilterType::All) { + return PNG_ALL_FILTERS; + } + + int f = 0; + + if (filter & (u16)OrbisPngEncFilterType::None) + f |= PNG_FILTER_NONE; + if (filter & (u16)OrbisPngEncFilterType::Sub) + f |= PNG_FILTER_SUB; + if (filter & (u16)OrbisPngEncFilterType::Up) + f |= PNG_FILTER_UP; + if (filter & (u16)OrbisPngEncFilterType::Average) + f |= PNG_FILTER_AVG; + if (filter & (u16)OrbisPngEncFilterType::Paeth) + f |= PNG_FILTER_PAETH; + + return f; +} + +void PngWriteFn(png_structp png_ptr, png_bytep data, size_t length) { + PngWriter* ctx = (PngWriter*)png_get_io_ptr(png_ptr); + + if ((size_t)(ctx->cursor - ctx->start) + length > ctx->capacity) { + LOG_ERROR(Lib_Png, "PNG output buffer too small"); + ctx->cancel_write = true; + return; + } + + memcpy(ctx->cursor, data, length); + ctx->cursor += length; +} + +void PngFlushFn(png_structp png_ptr) {} + +void PngEncError(png_structp png_ptr, png_const_charp error_message) { + LOG_ERROR(Lib_Png, "PNG error {}", error_message); +} + +void PngEncWarning(png_structp png_ptr, png_const_charp error_message) { + LOG_ERROR(Lib_Png, "PNG warning {}", error_message); +} + +s32 PS4_SYSV_ABI scePngEncCreate(const OrbisPngEncCreateParam* param, void* memoryAddress, + u32 memorySize, OrbisPngEncHandle* handle) { + if (param == nullptr || param->attribute != 0) { + LOG_ERROR(Lib_Png, "Invalid param"); + return ORBIS_PNG_ENC_ERROR_INVALID_ADDR; + } + + if (memoryAddress == nullptr) { + LOG_ERROR(Lib_Png, "Invalid memory address"); + return ORBIS_PNG_ENC_ERROR_INVALID_ADDR; + } + + if (param->max_image_width - 1 > 1000000) { + LOG_ERROR(Lib_Png, "Invalid Size, width = {}", param->max_image_width); + return ORBIS_PNG_ENC_ERROR_INVALID_SIZE; + } + + auto pngh = (PngHandler*)memoryAddress; + + pngh->png_ptr = + png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, PngEncError, PngEncWarning); + + if (pngh->png_ptr == nullptr) + return ORBIS_PNG_ENC_ERROR_FATAL; + + pngh->info_ptr = png_create_info_struct(pngh->png_ptr); + if (pngh->info_ptr == nullptr) { + png_destroy_write_struct(&pngh->png_ptr, nullptr); + return false; + } + + *handle = pngh; + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI scePngEncDelete(OrbisPngEncHandle handle) { + auto pngh = (PngHandler*)handle; + png_destroy_write_struct(&pngh->png_ptr, &pngh->info_ptr); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI scePngEncEncode(OrbisPngEncHandle handle, const OrbisPngEncEncodeParam* param, + OrbisPngEncOutputInfo* outputInfo) { + LOG_TRACE(Lib_Png, "called png addr = {}, image addr = {}, image size = {}", + (void*)param->png_mem_addr, (void*)param->image_mem_addr, param->image_mem_size); + + if (handle == nullptr) { + LOG_ERROR(Lib_Png, "Invalid handle"); + return ORBIS_PNG_ENC_ERROR_INVALID_HANDLE; + } + + if (param == nullptr) { + LOG_ERROR(Lib_Png, "Invalid param"); + return ORBIS_PNG_ENC_ERROR_INVALID_PARAM; + } + + if (param->image_mem_addr == nullptr || param->png_mem_addr == nullptr) { + LOG_ERROR(Lib_Png, "Invalid input or output address"); + return ORBIS_PNG_ENC_ERROR_INVALID_ADDR; + } + + if (param->png_mem_size == 0 || param->image_mem_size == 0 || param->image_height == 0 || + param->image_width == 0) { + LOG_ERROR(Lib_Png, "Invalid Size"); + return ORBIS_PNG_ENC_ERROR_INVALID_SIZE; + } + + auto pngh = (PngHandler*)handle; + + if (setjmp(png_jmpbuf(pngh->png_ptr))) { + LOG_ERROR(Lib_Png, "LibPNG aborted encode"); + return ORBIS_PNG_ENC_ERROR_FATAL; + } + + int png_color_type = PNG_COLOR_TYPE_RGB; + + if (param->color_space == OrbisPngEncColorSpace::RGBA) { + png_color_type |= PNG_COLOR_MASK_ALPHA; + } + + int png_interlace_type = PNG_INTERLACE_NONE; + int png_compression_type = PNG_COMPRESSION_TYPE_DEFAULT; + int png_filter_method = PNG_FILTER_TYPE_DEFAULT; + + PngWriter writer{}; + writer.cursor = param->png_mem_addr; + writer.start = param->png_mem_addr; + writer.capacity = param->png_mem_size; + + png_set_write_fn(pngh->png_ptr, &writer, PngWriteFn, PngFlushFn); + + png_set_IHDR(pngh->png_ptr, pngh->info_ptr, param->image_width, param->image_height, + param->bit_depth, png_color_type, png_interlace_type, png_compression_type, + png_filter_method); + + if (param->pixel_format == OrbisPngEncPixelFormat::B8G8R8A8) { + png_set_bgr(pngh->png_ptr); + } + + png_set_compression_level(pngh->png_ptr, std::clamp(param->compression_level, 0, 9)); + png_set_filter(pngh->png_ptr, 0, MapPngFilter(param->filter_type)); + + png_write_info(pngh->png_ptr, pngh->info_ptr); + + int channels = 4; + size_t row_stride = param->image_width * channels; + + uint32_t processed_height = 0; + + if (param->color_space == OrbisPngEncColorSpace::RGBA) { + for (; processed_height < param->image_height; ++processed_height) { + png_bytep row = (png_bytep)param->image_mem_addr + processed_height * row_stride; + png_write_row(pngh->png_ptr, row); + + if (outputInfo != nullptr) { + outputInfo->processed_height = processed_height; + } + + if (writer.cancel_write) { + LOG_ERROR(Lib_Png, "Ran out of room to write PNG"); + return ORBIS_PNG_ENC_ERROR_DATA_OVERFLOW; + } + } + } else { + // our input data is always rgba but when outputting without an alpha channel, libpng + // expects the input to not have alpha either, i couldn't find a way around this easily? + // png_strip_alpha is for reading and set_background wasn't working, this seems fine...? + std::vector rgb_row(param->image_width * 3); + + for (; processed_height < param->image_height; ++processed_height) { + const unsigned char* src = + param->image_mem_addr + processed_height * param->image_pitch; + + uint8_t* dst = rgb_row.data(); + + for (uint32_t x = 0; x < param->image_width; ++x) { + dst[0] = src[0]; + dst[1] = src[1]; + dst[2] = src[2]; + src += 4; // skip reading alpha channel + dst += 3; + } + + png_write_row(pngh->png_ptr, rgb_row.data()); + + if (outputInfo != nullptr) { + outputInfo->processed_height = processed_height; + } + + if (writer.cancel_write) { + LOG_ERROR(Lib_Png, "Ran out of room to write PNG"); + return ORBIS_PNG_ENC_ERROR_DATA_OVERFLOW; + } + } + } + + png_write_flush(pngh->png_ptr); + + png_write_end(pngh->png_ptr, pngh->info_ptr); + + if (outputInfo != nullptr) { + outputInfo->data_size = writer.cursor - writer.start; + outputInfo->processed_height = processed_height; + } + + return writer.cursor - writer.start; +} + +s32 PS4_SYSV_ABI scePngEncQueryMemorySize(const OrbisPngEncCreateParam* param) { + if (param == nullptr) { + LOG_ERROR(Lib_Png, "Invalid Address"); + return ORBIS_PNG_ENC_ERROR_INVALID_ADDR; + } + + if (param->attribute != 0 || param->max_filter_number > 5) { + LOG_ERROR(Lib_Png, "Invalid Param, attribute = {}, max_filter_number = {}", + param->attribute, param->max_filter_number); + return ORBIS_PNG_ENC_ERROR_INVALID_PARAM; + } + + if (param->max_image_width - 1 > 1000000) { + LOG_ERROR(Lib_Png, "Invalid Size, width = {}", param->max_image_width); + return ORBIS_PNG_ENC_ERROR_INVALID_SIZE; + } + + return sizeof(PngHandler); +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("7aGTPfrqT9s", "libScePngEnc", 1, "libScePngEnc", scePngEncCreate); + LIB_FUNCTION("RUrWdwTWZy8", "libScePngEnc", 1, "libScePngEnc", scePngEncDelete); + LIB_FUNCTION("xgDjJKpcyHo", "libScePngEnc", 1, "libScePngEnc", scePngEncEncode); + LIB_FUNCTION("9030RnBDoh4", "libScePngEnc", 1, "libScePngEnc", scePngEncQueryMemorySize); +}; + +} // namespace Libraries::PngEnc \ No newline at end of file diff --git a/src/core/libraries/libpng/pngenc.h b/src/core/libraries/libpng/pngenc.h new file mode 100644 index 000000000..1e6549340 --- /dev/null +++ b/src/core/libraries/libpng/pngenc.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::PngEnc { + +enum class OrbisPngEncAttribute { None = 0 }; + +enum class OrbisPngEncColorSpace : u16 { RGB = 3, RGBA = 19 }; + +enum class OrbisPngEncPixelFormat : u16 { R8G8B8A8 = 0, B8G8R8A8 }; + +enum class OrbisPngEncFilterType : u16 { + None = 0, + Sub = 1, + Up = 2, + Average = 4, + Paeth = 8, + All = 15 +}; + +struct OrbisPngEncCreateParam { + u32 this_size; + u32 attribute; + u32 max_image_width; + u32 max_filter_number; +}; + +struct OrbisPngEncEncodeParam { + const u8* image_mem_addr; + u8* png_mem_addr; + u32 image_mem_size; + u32 png_mem_size; + u32 image_width; + u32 image_height; + u32 image_pitch; + OrbisPngEncPixelFormat pixel_format; + OrbisPngEncColorSpace color_space; + u16 bit_depth; + u16 clut_number; + u16 filter_type; + u16 compression_level; +}; + +struct OrbisPngEncOutputInfo { + u32 data_size; + u32 processed_height; +}; + +using OrbisPngEncHandle = void*; + +s32 PS4_SYSV_ABI scePngEncCreate(const OrbisPngEncCreateParam* param, void* memoryAddress, + u32 memorySize, OrbisPngEncHandle* handle); +s32 PS4_SYSV_ABI scePngEncDelete(OrbisPngEncHandle handle); +s32 PS4_SYSV_ABI scePngEncEncode(OrbisPngEncHandle, const OrbisPngEncEncodeParam* param, + OrbisPngEncOutputInfo* outputInfo); +s32 PS4_SYSV_ABI scePngEncQueryMemorySize(const OrbisPngEncCreateParam* param); + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::PngEnc \ No newline at end of file diff --git a/src/core/libraries/libpng/pngenc_error.h b/src/core/libraries/libpng/pngenc_error.h new file mode 100644 index 000000000..a07b1317f --- /dev/null +++ b/src/core/libraries/libpng/pngenc_error.h @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/libraries/error_codes.h" + +// PngEnc library +constexpr int ORBIS_PNG_ENC_ERROR_INVALID_ADDR = 0x80690101; +constexpr int ORBIS_PNG_ENC_ERROR_INVALID_SIZE = 0x80690102; +constexpr int ORBIS_PNG_ENC_ERROR_INVALID_PARAM = 0x80690103; +constexpr int ORBIS_PNG_ENC_ERROR_INVALID_HANDLE = 0x80690104; +constexpr int ORBIS_PNG_ENC_ERROR_DATA_OVERFLOW = 0x80690110; +constexpr int ORBIS_PNG_ENC_ERROR_FATAL = 0x80690120; \ No newline at end of file diff --git a/src/emulator.cpp b/src/emulator.cpp index 9044ed027..6ba80b096 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -36,6 +36,7 @@ #include "core/libraries/font/fontft.h" #include "core/libraries/jpeg/jpegenc.h" #include "core/libraries/libc_internal/libc_internal.h" +#include "core/libraries/libpng/pngenc.h" #include "core/libraries/libs.h" #include "core/libraries/ngs2/ngs2.h" #include "core/libraries/np/np_trophy.h" @@ -527,7 +528,7 @@ void Emulator::LoadSystemModules(const std::string& game_serial) { {"libSceRtc.sprx", &Libraries::Rtc::RegisterLib}, {"libSceJpegDec.sprx", nullptr}, {"libSceJpegEnc.sprx", &Libraries::JpegEnc::RegisterLib}, - {"libScePngEnc.sprx", nullptr}, + {"libScePngEnc.sprx", &Libraries::PngEnc::RegisterLib}, {"libSceJson.sprx", nullptr}, {"libSceJson2.sprx", nullptr}, {"libSceLibcInternal.sprx", &Libraries::LibcInternal::RegisterLib}, From 8b46650fb20e41948cd0a697b9b1d81a9e52fdaf Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 30 Jan 2026 13:59:52 +0200 Subject: [PATCH 14/50] Misc (#3979) * improved trophy key save on new keymanager * updated CLI11 and sdl3 * delete so stupid git will know * fixed? * fix2 * fixed sdl3 * Fix missing CLI11 submodule --- .gitmodules | 15 ++--- externals/CLI11 | 1 + externals/CMakeLists.txt | 2 +- externals/ext-CLI11 | 1 - src/main.cpp | 138 ++++----------------------------------- 5 files changed, 22 insertions(+), 135 deletions(-) create mode 160000 externals/CLI11 delete mode 160000 externals/ext-CLI11 diff --git a/.gitmodules b/.gitmodules index 82c40f4f9..e54658932 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,10 +2,6 @@ path = externals/zlib-ng url = https://github.com/shadps4-emu/ext-zlib-ng.git shallow = true -[submodule "externals/sdl3"] - path = externals/sdl3 - url = https://github.com/shadps4-emu/ext-SDL.git - shallow = true [submodule "externals/fmt"] path = externals/fmt url = https://github.com/shadps4-emu/ext-fmt.git @@ -123,7 +119,10 @@ [submodule "externals/aacdec/fdk-aac"] path = externals/aacdec/fdk-aac url = https://android.googlesource.com/platform/external/aac -[submodule "externals/ext-CLI11"] - path = externals/ext-CLI11 - url = https://github.com/shadexternals/ext-CLI11.git - branch = main +[submodule "externals/CLI11"] + path = externals/CLI11 + url = https://github.com/shadexternals/CLI11.git +[submodule "externals/sdl3"] + path = externals/sdl3 + url = https://github.com/shadexternals/sdl3.git + diff --git a/externals/CLI11 b/externals/CLI11 new file mode 160000 index 000000000..bf5a16a26 --- /dev/null +++ b/externals/CLI11 @@ -0,0 +1 @@ +Subproject commit bf5a16a26a34a9a7ad75f4a7705585e44675fef0 diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index e243f63db..db03e7679 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -274,4 +274,4 @@ add_subdirectory(miniz) set(CLI11_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -add_subdirectory(ext-CLI11) \ No newline at end of file +add_subdirectory(CLI11) \ No newline at end of file diff --git a/externals/ext-CLI11 b/externals/ext-CLI11 deleted file mode 160000 index 1cce14833..000000000 --- a/externals/ext-CLI11 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1cce1483345e60997b87720948c37d6a34db2658 diff --git a/src/main.cpp b/src/main.cpp index 9b263e250..1229fdfe2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,11 +39,16 @@ int main(int argc, char* argv[]) { // ---- Trophy key migration ---- auto key_manager = KeyManager::GetInstance(); + key_manager->LoadFromFile(); if (key_manager->GetAllKeys().TrophyKeySet.ReleaseTrophyKey.empty() && !Config::getTrophyKey().empty()) { - key_manager->SetAllKeys({.TrophyKeySet = {.ReleaseTrophyKey = KeyManager::HexStringToBytes( - Config::getTrophyKey())}}); - key_manager->SaveToFile(); + auto keys = key_manager->GetAllKeys(); + if (keys.TrophyKeySet.ReleaseTrophyKey.empty() && !Config::getTrophyKey().empty()) { + keys.TrophyKeySet.ReleaseTrophyKey = + KeyManager::HexStringToBytes(Config::getTrophyKey()); + key_manager->SetAllKeys(keys); + key_manager->SaveToFile(); + } } CLI::App app{"shadPS4 Emulator CLI"}; @@ -66,128 +71,11 @@ int main(int argc, char* argv[]) { std::optional setAddonFolder; std::optional patchFile; - // ---- Options ---- - app.add_option("-g,--game", gamePath, "Game path or ID"); - app.add_option("-p,--patch", patchFile, "Patch file to apply"); - app.add_flag("-i,--ignore-game-patch", ignoreGamePatch, - "Disable automatic loading of game patches"); - - // FULLSCREEN: behavior-identical - app.add_option("-f,--fullscreen", fullscreenStr, "Fullscreen mode (true|false)"); - - app.add_option("--override-root", overrideRoot)->check(CLI::ExistingDirectory); - - app.add_flag("--wait-for-debugger", waitForDebugger); - app.add_option("--wait-for-pid", waitPid); - - app.add_flag("--show-fps", showFps); - app.add_flag("--config-clean", configClean); - app.add_flag("--config-global", configGlobal); - app.add_flag("--log-append", logAppend); - - app.add_option("--add-game-folder", addGameFolder)->check(CLI::ExistingDirectory); - app.add_option("--set-addon-folder", setAddonFolder)->check(CLI::ExistingDirectory); - - // ---- Capture args after `--` verbatim ---- - app.allow_extras(); - app.parse_complete_callback([&]() { - const auto& extras = app.remaining(); - if (!extras.empty()) { - gameArgs = extras; - } - }); - - // ---- No-args behavior ---- - if (argc == 1) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "shadPS4", - "This is a CLI application. Please use the QTLauncher for a GUI:\n" - "https://github.com/shadps4-emu/shadps4-qtlauncher/releases", - nullptr); - std::cout << app.help(); - return -1; - } - - try { - app.parse(argc, argv); - } catch (const CLI::ParseError& e) { - return app.exit(e); - } - - // ---- Utility commands ---- - if (addGameFolder) { - Config::addGameInstallDir(*addGameFolder); - Config::save(user_dir / "config.toml"); - std::cout << "Game folder successfully saved.\n"; - return 0; - } - - if (setAddonFolder) { - Config::setAddonInstallDir(*setAddonFolder); - Config::save(user_dir / "config.toml"); - std::cout << "Addon folder successfully saved.\n"; - return 0; - } - - if (!gamePath.has_value()) { - if (!gameArgs.empty()) { - gamePath = gameArgs.front(); - gameArgs.erase(gameArgs.begin()); - } else { - std::cerr << "Error: Please provide a game path or ID.\n"; - return 1; - } - } - - // ---- Apply flags ---- - if (patchFile) - MemoryPatcher::patch_file = *patchFile; - - if (ignoreGamePatch) - Core::FileSys::MntPoints::ignore_game_patches = true; - - if (fullscreenStr) { - if (*fullscreenStr == "true") { - Config::setIsFullscreen(true); - } else if (*fullscreenStr == "false") { - Config::setIsFullscreen(false); - } else { - std::cerr << "Error: Invalid argument for --fullscreen (use true|false)\n"; - return 1; - } - } - - if (showFps) - Config::setShowFpsCounter(true); - - if (configClean) - Config::setConfigMode(Config::ConfigMode::Clean); - - if (configGlobal) - Config::setConfigMode(Config::ConfigMode::Global); - - if (logAppend) - Common::Log::SetAppend(); - - // ---- Resolve game path or ID ---- - std::filesystem::path ebootPath(*gamePath); - if (!std::filesystem::exists(ebootPath)) { - bool found = false; - constexpr int maxDepth = 5; - for (const auto& installDir : Config::getGameInstallDirs()) { - if (auto foundPath = Common::FS::FindGameByID(installDir, *gamePath, maxDepth)) { - ebootPath = *foundPath; - found = true; - break; - } - } - if (!found) { - std::cerr << "Error: Game ID or file path not found: " << *gamePath << "\n"; - return 1; - } - } - - if (waitPid) - Core::Debugger::WaitForPid(*waitPid); + // const char* const ebootPath = "M:/PS4/dumpedgames/CUSA00207/eboot.bin"; // bloodborne + // const char* const eboot_path = "D:/ps4/shadps4games/CUSA07010/eboot.bin";//sonic mania + // const char* const eboot_path = "C:/ps4tests/CUSA03318/eboot.bin";//carmageddon + const char* const ebootPath = "C:/ps4tests/CUSA04518/eboot.bin"; // project diva x + // const char* const ebootPath = "D:/ps4sdk/rain.elf"; auto* emulator = Common::Singleton::Instance(); emulator->executableName = argv[0]; From d1150ad3030cb7665ba0ff4a37f45556a3fe4bb3 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:55:26 +0100 Subject: [PATCH 15/50] this is why you don't push local changes, shadow --- src/main.cpp | 127 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 5 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 1229fdfe2..d3799e2ec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -71,11 +71,128 @@ int main(int argc, char* argv[]) { std::optional setAddonFolder; std::optional patchFile; - // const char* const ebootPath = "M:/PS4/dumpedgames/CUSA00207/eboot.bin"; // bloodborne - // const char* const eboot_path = "D:/ps4/shadps4games/CUSA07010/eboot.bin";//sonic mania - // const char* const eboot_path = "C:/ps4tests/CUSA03318/eboot.bin";//carmageddon - const char* const ebootPath = "C:/ps4tests/CUSA04518/eboot.bin"; // project diva x - // const char* const ebootPath = "D:/ps4sdk/rain.elf"; + // ---- Options ---- + app.add_option("-g,--game", gamePath, "Game path or ID"); + app.add_option("-p,--patch", patchFile, "Patch file to apply"); + app.add_flag("-i,--ignore-game-patch", ignoreGamePatch, + "Disable automatic loading of game patches"); + + // FULLSCREEN: behavior-identical + app.add_option("-f,--fullscreen", fullscreenStr, "Fullscreen mode (true|false)"); + + app.add_option("--override-root", overrideRoot)->check(CLI::ExistingDirectory); + + app.add_flag("--wait-for-debugger", waitForDebugger); + app.add_option("--wait-for-pid", waitPid); + + app.add_flag("--show-fps", showFps); + app.add_flag("--config-clean", configClean); + app.add_flag("--config-global", configGlobal); + app.add_flag("--log-append", logAppend); + + app.add_option("--add-game-folder", addGameFolder)->check(CLI::ExistingDirectory); + app.add_option("--set-addon-folder", setAddonFolder)->check(CLI::ExistingDirectory); + + // ---- Capture args after `--` verbatim ---- + app.allow_extras(); + app.parse_complete_callback([&]() { + const auto& extras = app.remaining(); + if (!extras.empty()) { + gameArgs = extras; + } + }); + + // ---- No-args behavior ---- + if (argc == 1) { + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "shadPS4", + "This is a CLI application. Please use the QTLauncher for a GUI:\n" + "https://github.com/shadps4-emu/shadps4-qtlauncher/releases", + nullptr); + std::cout << app.help(); + return -1; + } + + try { + app.parse(argc, argv); + } catch (const CLI::ParseError& e) { + return app.exit(e); + } + + // ---- Utility commands ---- + if (addGameFolder) { + Config::addGameInstallDir(*addGameFolder); + Config::save(user_dir / "config.toml"); + std::cout << "Game folder successfully saved.\n"; + return 0; + } + + if (setAddonFolder) { + Config::setAddonInstallDir(*setAddonFolder); + Config::save(user_dir / "config.toml"); + std::cout << "Addon folder successfully saved.\n"; + return 0; + } + + if (!gamePath.has_value()) { + if (!gameArgs.empty()) { + gamePath = gameArgs.front(); + gameArgs.erase(gameArgs.begin()); + } else { + std::cerr << "Error: Please provide a game path or ID.\n"; + return 1; + } + } + + // ---- Apply flags ---- + if (patchFile) + MemoryPatcher::patch_file = *patchFile; + + if (ignoreGamePatch) + Core::FileSys::MntPoints::ignore_game_patches = true; + + if (fullscreenStr) { + if (*fullscreenStr == "true") { + Config::setIsFullscreen(true); + } else if (*fullscreenStr == "false") { + Config::setIsFullscreen(false); + } else { + std::cerr << "Error: Invalid argument for --fullscreen (use true|false)\n"; + return 1; + } + } + + if (showFps) + Config::setShowFpsCounter(true); + + if (configClean) + Config::setConfigMode(Config::ConfigMode::Clean); + + if (configGlobal) + Config::setConfigMode(Config::ConfigMode::Global); + + if (logAppend) + Common::Log::SetAppend(); + + // ---- Resolve game path or ID ---- + std::filesystem::path ebootPath(*gamePath); + if (!std::filesystem::exists(ebootPath)) { + bool found = false; + constexpr int maxDepth = 5; + for (const auto& installDir : Config::getGameInstallDirs()) { + if (auto foundPath = Common::FS::FindGameByID(installDir, *gamePath, maxDepth)) { + ebootPath = *foundPath; + found = true; + break; + } + } + if (!found) { + std::cerr << "Error: Game ID or file path not found: " << *gamePath << "\n"; + return 1; + } + } + + if (waitPid) + Core::Debugger::WaitForPid(*waitPid); auto* emulator = Common::Singleton::Instance(); emulator->executableName = argv[0]; From c0987ecc73821a7b8ef8d1b87a215b5a9c819808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Fri, 30 Jan 2026 22:42:37 +0100 Subject: [PATCH 16/50] Fake Matching2 (#3959) --- CMakeLists.txt | 2 + src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + src/core/libraries/libs.cpp | 2 + src/core/libraries/np/np_manager.cpp | 87 +++ src/core/libraries/np/np_manager.h | 32 +- src/core/libraries/np/np_matching2.cpp | 812 +++++++++++++++++++++++++ src/core/libraries/np/np_matching2.h | 48 ++ src/core/libraries/np/np_types.h | 17 + 9 files changed, 994 insertions(+), 8 deletions(-) create mode 100644 src/core/libraries/np/np_matching2.cpp create mode 100644 src/core/libraries/np/np_matching2.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 08e8fe161..8e123f3d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -584,6 +584,8 @@ set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/np_commerce.h src/core/libraries/np/np_manager.cpp src/core/libraries/np/np_manager.h + src/core/libraries/np/np_matching2.cpp + src/core/libraries/np/np_matching2.h src/core/libraries/np/np_score.cpp src/core/libraries/np/np_score.h src/core/libraries/np/np_trophy.cpp diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 85946e9c4..9a3fe0aa1 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -107,6 +107,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, NpCommon) \ SUB(Lib, NpCommerce) \ SUB(Lib, NpManager) \ + SUB(Lib, NpMatching2) \ SUB(Lib, NpScore) \ SUB(Lib, NpTrophy) \ SUB(Lib, NpTus) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 127377115..9e176c698 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -74,6 +74,7 @@ enum class Class : u8 { Lib_NpCommerce, ///< The LibSceNpCommerce implementation Lib_NpAuth, ///< The LibSceNpAuth implementation Lib_NpManager, ///< The LibSceNpManager implementation + Lib_NpMatching2, ///< The LibSceNpMatching2 implementation Lib_NpScore, ///< The LibSceNpScore implementation Lib_NpTrophy, ///< The LibSceNpTrophy implementation Lib_NpTus, ///< The LibSceNpTus implementation diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 7e8edccec..67c3d4b7d 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -35,6 +35,7 @@ #include "core/libraries/np/np_commerce.h" #include "core/libraries/np/np_common.h" #include "core/libraries/np/np_manager.h" +#include "core/libraries/np/np_matching2.h" #include "core/libraries/np/np_partner.h" #include "core/libraries/np/np_party.h" #include "core/libraries/np/np_profile_dialog.h" @@ -99,6 +100,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Np::NpCommerce::RegisterLib(sym); Libraries::Np::NpCommon::RegisterLib(sym); Libraries::Np::NpManager::RegisterLib(sym); + Libraries::Np::NpMatching2::RegisterLib(sym); Libraries::Np::NpScore::RegisterLib(sym); Libraries::Np::NpTrophy::RegisterLib(sym); Libraries::Np::NpWebApi::RegisterLib(sym); diff --git a/src/core/libraries/np/np_manager.cpp b/src/core/libraries/np/np_manager.cpp index ebc940bf3..229ae33af 100644 --- a/src/core/libraries/np/np_manager.cpp +++ b/src/core/libraries/np/np_manager.cpp @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include +#include #include "common/config.h" #include "common/logging/log.h" @@ -17,6 +19,9 @@ static bool g_signed_in = false; static s32 g_active_requests = 0; static std::mutex g_request_mutex; +static std::map> g_np_callbacks; +static std::mutex g_np_callbacks_mutex; + // Internal types for storing request-related information enum class NpRequestState { None = 0, @@ -665,6 +670,19 @@ s32 PS4_SYSV_ABI sceNpGetState(Libraries::UserService::OrbisUserServiceUserId us return ORBIS_OK; } +s32 PS4_SYSV_ABI +sceNpGetUserIdByAccountId(u64 account_id, Libraries::UserService::OrbisUserServiceUserId* user_id) { + if (user_id == nullptr) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + if (!g_signed_in) { + return ORBIS_NP_ERROR_SIGNED_OUT; + } + *user_id = 1; + LOG_DEBUG(Lib_NpManager, "userid({}) = {}", account_id, *user_id); + return ORBIS_OK; +} + s32 PS4_SYSV_ABI sceNpHasSignedUp(Libraries::UserService::OrbisUserServiceUserId user_id, bool* has_signed_up) { LOG_DEBUG(Lib_NpManager, "called"); @@ -682,8 +700,22 @@ struct NpStateCallbackForNpToolkit { NpStateCallbackForNpToolkit NpStateCbForNp; +struct NpStateCallback { + std::variant func; + void* userdata; +}; + +NpStateCallback NpStateCb; + s32 PS4_SYSV_ABI sceNpCheckCallback() { LOG_DEBUG(Lib_NpManager, "(STUBBED) called"); + + std::scoped_lock lk{g_np_callbacks_mutex}; + + for (auto i : g_np_callbacks) { + (i.second)(); + } + return ORBIS_OK; } @@ -692,6 +724,40 @@ s32 PS4_SYSV_ABI sceNpCheckCallbackForLib() { return ORBIS_OK; } +s32 PS4_SYSV_ABI sceNpRegisterStateCallback(OrbisNpStateCallback callback, void* userdata) { + static s32 id = 0; + LOG_ERROR(Lib_NpManager, "(STUBBED) called, userdata = {}", userdata); + NpStateCb.func = callback; + NpStateCb.userdata = userdata; + + return id; +} + +s32 PS4_SYSV_ABI sceNpRegisterStateCallbackA(OrbisNpStateCallbackA callback, void* userdata) { + static s32 id = 0; + LOG_ERROR(Lib_NpManager, "(STUBBED) called, userdata = {}", userdata); + NpStateCb.func = callback; + NpStateCb.userdata = userdata; + + return id; +} + +struct NpReachabilityStateCallback { + OrbisNpReachabilityStateCallback func; + void* userdata; +}; + +NpReachabilityStateCallback NpReachabilityCb; + +s32 PS4_SYSV_ABI sceNpRegisterNpReachabilityStateCallback(OrbisNpReachabilityStateCallback callback, + void* userdata) { + static s32 id = 0; + LOG_ERROR(Lib_NpManager, "(STUBBED) called"); + NpReachabilityCb.func = callback; + NpReachabilityCb.userdata = userdata; + return id; +} + s32 PS4_SYSV_ABI sceNpRegisterStateCallbackForToolkit(OrbisNpStateCallbackForNpToolkit callback, void* userdata) { static s32 id = 0; @@ -701,6 +767,22 @@ s32 PS4_SYSV_ABI sceNpRegisterStateCallbackForToolkit(OrbisNpStateCallbackForNpT return id; } +void RegisterNpCallback(std::string key, std::function cb) { + std::scoped_lock lk{g_np_callbacks_mutex}; + + LOG_DEBUG(Lib_NpManager, "registering callback processing for {}", key); + + g_np_callbacks.emplace(key, cb); +} + +void DeregisterNpCallback(std::string key) { + std::scoped_lock lk{g_np_callbacks_mutex}; + + LOG_DEBUG(Lib_NpManager, "deregistering callback processing for {}", key); + + g_np_callbacks.erase(key); +} + void RegisterLib(Core::Loader::SymbolsResolver* sym) { g_signed_in = Config::getPSNSignedIn(); @@ -739,9 +821,14 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("p-o74CnoNzY", "libSceNpManager", 1, "libSceNpManager", sceNpGetNpId); LIB_FUNCTION("XDncXQIJUSk", "libSceNpManager", 1, "libSceNpManager", sceNpGetOnlineId); LIB_FUNCTION("eQH7nWPcAgc", "libSceNpManager", 1, "libSceNpManager", sceNpGetState); + LIB_FUNCTION("VgYczPGB5ss", "libSceNpManager", 1, "libSceNpManager", sceNpGetUserIdByAccountId); LIB_FUNCTION("Oad3rvY-NJQ", "libSceNpManager", 1, "libSceNpManager", sceNpHasSignedUp); LIB_FUNCTION("3Zl8BePTh9Y", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallback); LIB_FUNCTION("JELHf4xPufo", "libSceNpManager", 1, "libSceNpManager", sceNpCheckCallbackForLib); + LIB_FUNCTION("VfRSmPmj8Q8", "libSceNpManager", 1, "libSceNpManager", + sceNpRegisterStateCallback); + LIB_FUNCTION("hw5KNqAAels", "libSceNpManager", 1, "libSceNpManager", + sceNpRegisterNpReachabilityStateCallback); LIB_FUNCTION("JELHf4xPufo", "libSceNpManagerForToolkit", 1, "libSceNpManager", sceNpCheckCallbackForLib); LIB_FUNCTION("0c7HbXRKUt4", "libSceNpManagerForToolkit", 1, "libSceNpManager", diff --git a/src/core/libraries/np/np_manager.h b/src/core/libraries/np/np_manager.h index 59864c173..078fa804a 100644 --- a/src/core/libraries/np/np_manager.h +++ b/src/core/libraries/np/np_manager.h @@ -3,6 +3,8 @@ #pragma once +#include + #include "common/types.h" #include "core/libraries/np/np_error.h" #include "core/libraries/np/np_types.h" @@ -23,20 +25,28 @@ enum class OrbisNpState : u32 { SignedIn = 2, }; -using OrbisNpStateCallbackForNpToolkit = PS4_SYSV_ABI void (*)( - Libraries::UserService::OrbisUserServiceUserId userId, OrbisNpState state, void* userdata); - -enum class OrbisNpGamePresenseStatus { - Offline = 0, - Online = 1, -}; - enum class OrbisNpReachabilityState { Unavailable = 0, Available = 1, Reachable = 2, }; +using OrbisNpStateCallback = + PS4_SYSV_ABI void (*)(Libraries::UserService::OrbisUserServiceUserId userId, OrbisNpState state, + OrbisNpId* npId, void* userdata); +using OrbisNpStateCallbackA = PS4_SYSV_ABI void (*)( + Libraries::UserService::OrbisUserServiceUserId userId, OrbisNpState state, void* userdata); +using OrbisNpStateCallbackForNpToolkit = PS4_SYSV_ABI void (*)( + Libraries::UserService::OrbisUserServiceUserId userId, OrbisNpState state, void* userdata); +using OrbisNpReachabilityStateCallback = + PS4_SYSV_ABI void (*)(Libraries::UserService::OrbisUserServiceUserId userId, + OrbisNpReachabilityState state, void* userdata); + +enum class OrbisNpGamePresenseStatus { + Offline = 0, + Online = 1, +}; + struct OrbisNpCountryCode { char country_code[2]; char end; @@ -80,5 +90,11 @@ struct OrbisNpCreateAsyncRequestParameter { u8 padding[4]; }; +void RegisterNpCallback(std::string key, std::function cb); +void DeregisterNpCallback(std::string key); + +s32 PS4_SYSV_ABI sceNpGetOnlineId(Libraries::UserService::OrbisUserServiceUserId user_id, + OrbisNpOnlineId* online_id); + void RegisterLib(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::Np::NpManager diff --git a/src/core/libraries/np/np_matching2.cpp b/src/core/libraries/np/np_matching2.cpp new file mode 100644 index 000000000..cf4faea39 --- /dev/null +++ b/src/core/libraries/np/np_matching2.cpp @@ -0,0 +1,812 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "common/config.h" +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" +#include "core/libraries/np/np_manager.h" +#include "core/libraries/np/np_matching2.h" +#include "core/libraries/np/np_types.h" +#include "core/libraries/system/userservice.h" + +namespace Libraries::Np::NpMatching2 { + +static bool g_initialized = false; +static OrbisNpMatching2ContextId contextId = 1; + +struct NpMatching2ContextEvent { + OrbisNpMatching2ContextId contextId; + OrbisNpMatching2Event event; + OrbisNpMatching2EventCause cause; + int errorCode; +}; + +struct NpMatching2LobbyEvent { + OrbisNpMatching2ContextId contextId; + OrbisNpMatching2LobbyId lobbyId; + OrbisNpMatching2Event event; + void* data; +}; + +struct NpMatching2RoomEvent { + OrbisNpMatching2ContextId contextId; + OrbisNpMatching2RoomId roomId; + OrbisNpMatching2Event event; + void* data; +}; + +static std::mutex g_events_mutex; +static std::deque g_ctx_events; +static std::deque g_lobby_events; +static std::deque g_room_events; +static std::mutex g_responses_mutex; +static std::deque> g_responses; + +struct OrbisNpMatching2CreateContextParameter { + Libraries::Np::OrbisNpId* npId; + void* npCommunicationId; + void* npPassphrase; + Libraries::Np::OrbisNpServiceLabel serviceLabel; + u64 size; +}; + +static_assert(sizeof(OrbisNpMatching2CreateContextParameter) == 0x28); + +int PS4_SYSV_ABI sceNpMatching2CreateContext(const OrbisNpMatching2CreateContextParameter* param, + OrbisNpMatching2ContextId* ctxId) { + LOG_DEBUG(Lib_NpMatching2, "called, npId = {}, serviceLabel = {}, size = {}", + param->npId->handle.data, param->serviceLabel, param->size); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!param || param->size != 0x28 || !ctxId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + *ctxId = contextId++; + + return ORBIS_OK; +} + +struct OrbisNpMatching2CreateContextParameterA { + Libraries::UserService::OrbisUserServiceUserId userId; + Libraries::Np::OrbisNpServiceLabel serviceLabel; + u64 size; +}; + +static_assert(sizeof(OrbisNpMatching2CreateContextParameterA) == 16); + +int PS4_SYSV_ABI sceNpMatching2CreateContextA(const OrbisNpMatching2CreateContextParameterA* param, + OrbisNpMatching2ContextId* ctxId) { + LOG_DEBUG(Lib_NpMatching2, "called, userId = {}, serviceLabel = {}, size = {}", param->userId, + param->serviceLabel, param->size); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!param || param->size != 0x10 || !ctxId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + *ctxId = contextId++; + + return ORBIS_OK; +} + +using OrbisNpMatching2RequestCallback = PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId, + OrbisNpMatching2RequestId, + OrbisNpMatching2Event, int, void*, + void*); + +struct OrbisNpMatching2RequestOptParam { + OrbisNpMatching2RequestCallback callback; + void* arg; + u32 timeout; + u16 appId; + u8 dummy[2]; +}; + +static std::optional defaultRequestOptParam = std::nullopt; + +auto GetOptParam(OrbisNpMatching2RequestOptParam* requestOpt) { + return requestOpt ? *requestOpt + : (defaultRequestOptParam ? defaultRequestOptParam + : std::optional{}); +} + +struct OrbisNpMatching2CreateJoinRoomRequestA { + u16 maxSlot; + OrbisNpMatching2TeamId teamId; + u8 pad[5]; + OrbisNpMatching2Flags flags; + OrbisNpMatching2WorldId worldId; + OrbisNpMatching2LobbyId lobbyId; + void* roomPasswd; + void* passwdSlotMask; + void* groupConfig; + u64 groupConfigs; + void* joinGroupLabel; + Libraries::Np::OrbisNpAccountId* allowedUser; + u64 allowedUsers; + Libraries::Np::OrbisNpAccountId* blockedUser; + u64 blockedUsers; + void* internalBinAttr; + u64 internalBinAttrs; + void* externalSearchIntAttr; + u64 externalSearchIntAttrs; + void* externalSearchBinAttr; + u64 externalSearchBinAttrs; + void* externalBinAttr; + u64 externalBinAttrs; + void* memberInternalBinAttr; + u64 memberInternalBinAttrs; + void* signalingParam; +}; + +static_assert(sizeof(OrbisNpMatching2CreateJoinRoomRequestA) == 184); + +struct OrbisNpMatching2RoomDataInternal { + u16 publicSlots; + u16 privateSlots; + u16 openPublicSlots; + u16 openPrivateSlots; + u16 maxSlot; + OrbisNpMatching2ServerId serverId; + OrbisNpMatching2WorldId worldId; + OrbisNpMatching2LobbyId lobbyId; + OrbisNpMatching2RoomId roomId; + u64 passwdSlotMask; + u64 joinedSlotMask; + void* roomGroup; + u64 roomGroups; + OrbisNpMatching2Flags flags; + u8 pad[4]; + void* internalBinAttr; + u64 internalBinAttrs; +}; + +struct OrbisNpMatching2RoomMemberDataInternalA { + OrbisNpMatching2RoomMemberDataInternalA* next; + u64 joinDateTicks; + Libraries::Np::OrbisNpPeerAddressA user; + Libraries::Np::OrbisNpOnlineId onlineId; + u8 pad[4]; + OrbisNpMatching2RoomMemberId memberId; + OrbisNpMatching2TeamId teamId; + OrbisNpMatching2NatType natType; + OrbisNpMatching2Flags flags; + void* roomGroup; + void* roomMemberInternalBinAttr; + u64 roomMemberInternalBinAttrs; +}; + +struct OrbisNpMatching2RoomMemberDataInternalListA { + OrbisNpMatching2RoomMemberDataInternalA* members; + u64 membersNum; + OrbisNpMatching2RoomMemberDataInternalA* me; + OrbisNpMatching2RoomMemberDataInternalA* owner; +}; + +struct OrbisNpMatching2CreateJoinRoomResponseA { + OrbisNpMatching2RoomDataInternal* roomData; + OrbisNpMatching2RoomMemberDataInternalListA members; +}; + +int PS4_SYSV_ABI sceNpMatching2CreateJoinRoomA(OrbisNpMatching2ContextId ctxId, + OrbisNpMatching2CreateJoinRoomRequestA* request, + OrbisNpMatching2RequestOptParam* requestOpt, + OrbisNpMatching2RequestId* requestId) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt)); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!request || !requestId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + LOG_DEBUG(Lib_NpMatching2, + "maxSlot = {}, teamId = {}, worldId = {}, lobbyId = {}, groupConfig = {}, " + "joinGroupLabel = {}", + request->maxSlot, request->teamId, request->worldId, request->lobbyId, + request->groupConfig, request->joinGroupLabel); + + static OrbisNpMatching2RequestId id = 10; + *requestId = id++; + + if (auto optParam = GetOptParam(requestOpt); optParam) { + LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout, + optParam->appId); + std::scoped_lock lk{g_responses_mutex}; + auto reqIdCopy = *requestId; + auto requestCopy = *request; + g_responses.emplace_back([=]() { + Libraries::Np::OrbisNpOnlineId onlineId{}; + if (NpManager::sceNpGetOnlineId(1, &onlineId) != ORBIS_OK) { + return; + } + + OrbisNpMatching2RoomMemberDataInternalA me{ + nullptr, + 0, + {0xace104e, Libraries::Np::OrbisNpPlatformType::ORBIS_NP_PLATFORM_TYPE_PS4}, + onlineId, + {0, 0, 0, 0}, + 1, + requestCopy.teamId, + 1, + 0, + nullptr, + nullptr, + 0}; + OrbisNpMatching2RoomDataInternal room{requestCopy.maxSlot, + 0, + static_cast(requestCopy.maxSlot - 1u), + 0, + 15, + 0xac, + requestCopy.worldId, + requestCopy.lobbyId, + 0x10, + 0, + 0, + nullptr, + 0, + 0, + {0, 0, 0, 0}, + nullptr, + 0}; + OrbisNpMatching2CreateJoinRoomResponseA resp{&room, {&me, 1, &me, &me}}; + optParam->callback(ctxId, reqIdCopy, + ORBIS_NP_MATCHING2_REQUEST_EVENT_CREATE_JOIN_ROOM_A, 0, &resp, + optParam->arg); + }); + } + return ORBIS_OK; +} + +using OrbisNpMatching2ContextCallback = PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId contextId, + OrbisNpMatching2Event event, + OrbisNpMatching2EventCause cause, + int errorCode, void* userdata); + +std::function npMatching2ContextCallback = nullptr; + +int PS4_SYSV_ABI sceNpMatching2RegisterContextCallback(OrbisNpMatching2ContextCallback callback, + void* userdata) { + LOG_DEBUG(Lib_NpMatching2, "called, userdata = {}", userdata); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + + npMatching2ContextCallback = [callback, userdata](auto arg) { + callback(arg->contextId, arg->event, arg->cause, arg->errorCode, userdata); + }; + + return ORBIS_OK; +} + +using OrbisNpMatching2LobbyEventCallback = + PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId contextId, OrbisNpMatching2LobbyId lobbyId, + OrbisNpMatching2Event event, void* data, void* userdata); + +std::function npMatching2LobbyCallback = nullptr; + +int PS4_SYSV_ABI sceNpMatching2RegisterLobbyEventCallback( + OrbisNpMatching2ContextId ctxId, OrbisNpMatching2LobbyEventCallback callback, void* userdata) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + + npMatching2LobbyCallback = [callback, userdata](auto arg) { + callback(arg->contextId, arg->lobbyId, arg->event, arg->data, userdata); + }; + + return ORBIS_OK; +} + +using OrbisNpMatching2RoomEventCallback = PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId contextId, + OrbisNpMatching2RoomId roomId, + OrbisNpMatching2Event event, + void* data, void* userdata); + +std::function npMatching2RoomCallback = nullptr; + +int PS4_SYSV_ABI sceNpMatching2RegisterRoomEventCallback(OrbisNpMatching2ContextId ctxId, + OrbisNpMatching2RoomEventCallback callback, + void* userdata) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + + npMatching2RoomCallback = [callback, userdata](auto arg) { + callback(arg->contextId, arg->roomId, arg->event, arg->data, userdata); + }; + + return ORBIS_OK; +} + +struct OrbisNpMatching2SignalingEvent { + OrbisNpMatching2ContextId contextId; + OrbisNpMatching2RoomId roomId; + OrbisNpMatching2RoomMemberId roomMemberId; + OrbisNpMatching2Event event; + int errorCode; +}; + +using OrbisNpMatching2SignalingCallback = + PS4_SYSV_ABI void (*)(OrbisNpMatching2ContextId contextId, OrbisNpMatching2RoomId roomId, + OrbisNpMatching2RoomMemberId roomMemberId, OrbisNpMatching2Event event, + int errorCode, void* userdata); + +std::function npMatching2SignalingCallback = nullptr; + +int PS4_SYSV_ABI sceNpMatching2RegisterSignalingCallback(OrbisNpMatching2ContextId ctxId, + OrbisNpMatching2SignalingCallback callback, + void* userdata) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, userdata = {}", ctxId, userdata); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + + npMatching2SignalingCallback = [callback, userdata](auto arg) { + callback(arg->contextId, arg->roomId, arg->roomMemberId, arg->event, arg->errorCode, + userdata); + }; + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpMatching2ContextStart(OrbisNpMatching2ContextId ctxId, u64 timeout) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, timeout = {}", ctxId, timeout); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + + std::scoped_lock lk{g_events_mutex}; + if (Config::getIsConnectedToNetwork() && Config::getPSNSignedIn()) { + g_ctx_events.emplace_back(ctxId, ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED, + ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ACTION, 0); + } else { + // error confirmed with a real console disconnected from the internet + constexpr int ORBIS_NET_ERROR_RESOLVER_ETIMEDOUT = 0x804101e2; + g_ctx_events.emplace_back(ctxId, ORBIS_NP_MATCHING2_CONTEXT_EVENT_START_OVER, + ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ERROR, + ORBIS_NET_ERROR_RESOLVER_ETIMEDOUT); + } + + return ORBIS_OK; +} + +void ProcessEvents() { + { + std::scoped_lock lk{g_events_mutex}; + + if (npMatching2ContextCallback) { + while (!g_ctx_events.empty()) { + npMatching2ContextCallback(&g_ctx_events.front()); + g_ctx_events.pop_front(); + } + } + if (npMatching2LobbyCallback) { + while (!g_lobby_events.empty()) { + npMatching2LobbyCallback(&g_lobby_events.front()); + g_lobby_events.pop_front(); + } + } + if (npMatching2RoomCallback) { + while (!g_room_events.empty()) { + npMatching2RoomCallback(&g_room_events.front()); + g_room_events.pop_front(); + } + } + } + + std::scoped_lock lk{g_responses_mutex}; + while (!g_responses.empty()) { + (g_responses.front())(); + g_responses.pop_front(); + } +} + +struct OrbisNpMatching2InitializeParameter { + u64 poolSize; + // +}; + +int PS4_SYSV_ABI sceNpMatching2Initialize(OrbisNpMatching2InitializeParameter* param) { + LOG_DEBUG(Lib_NpMatching2, "called"); + + if (g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_ALREADY_INITIALIZED; + } + + g_initialized = true; + Libraries::Np::NpManager::RegisterNpCallback("NpMatching2", ProcessEvents); + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpMatching2Terminate() { + LOG_DEBUG(Lib_NpMatching2, "called"); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + + g_initialized = false; + Libraries::Np::NpManager::DeregisterNpCallback("NpMatching2"); + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpMatching2SetDefaultRequestOptParam( + OrbisNpMatching2ContextId ctxId, OrbisNpMatching2RequestOptParam* requestOpt) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}", ctxId); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!requestOpt) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + defaultRequestOptParam = *requestOpt; + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpMatching2GetServerId(OrbisNpMatching2ContextId ctxId, + OrbisNpMatching2ServerId* serverId) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}", ctxId); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!serverId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + *serverId = 0xac; + + return ORBIS_OK; +} + +struct OrbisNpMatching2GetWorldInfoListRequest { + OrbisNpMatching2ServerId serverId; +}; + +struct OrbisNpMatching2World { + OrbisNpMatching2World* next; + OrbisNpMatching2WorldId worldId; + u32 lobbiesNum; + u32 maxLobbyMembersNum; + u32 lobbyMembersNum; + u32 roomsNum; + u32 roomMembersNum; + u8 pad[3]; +}; + +struct OrbisNpMatching2GetWorldInfoListResponse { + OrbisNpMatching2World* world; + u64 worldNum; +}; + +int PS4_SYSV_ABI sceNpMatching2GetWorldInfoList(OrbisNpMatching2ContextId ctxId, + OrbisNpMatching2GetWorldInfoListRequest* request, + OrbisNpMatching2RequestOptParam* requestOpt, + OrbisNpMatching2RequestId* requestId) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, request.serverId = {}, requestOpt = {}", ctxId, + request ? request->serverId : 0xFFFF, fmt::ptr(requestOpt)); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!request || !requestId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + static OrbisNpMatching2RequestId id = 1; + *requestId = id++; + + if (auto optParam = GetOptParam(requestOpt); optParam) { + LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout, + optParam->appId); + auto reqIdCopy = *requestId; + std::scoped_lock lk{g_responses_mutex}; + g_responses.emplace_back([=]() { + OrbisNpMatching2World w{nullptr, 1, 10, 0, 10, 0, {}}; + OrbisNpMatching2GetWorldInfoListResponse resp{&w, 1}; + optParam->callback(ctxId, reqIdCopy, + ORBIS_NP_MATCHING2_REQUEST_EVENT_GET_WORLD_INFO_LIST, 0, &resp, + optParam->arg); + }); + } + + return ORBIS_OK; +} + +struct OrbisNpMatching2PresenceOptionData { + u8 data[16]; + u64 len; +}; + +struct OrbisNpMatching2LeaveRoomRequest { + OrbisNpMatching2RoomId roomId; + OrbisNpMatching2PresenceOptionData optData; +}; + +int PS4_SYSV_ABI sceNpMatching2LeaveRoom(OrbisNpMatching2ContextId ctxId, + OrbisNpMatching2LeaveRoomRequest* request, + OrbisNpMatching2RequestOptParam* requestOpt, + OrbisNpMatching2RequestId* requestId) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt)); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!request || !requestId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + static OrbisNpMatching2RequestId id = 500; + *requestId = id++; + + if (auto optParam = GetOptParam(requestOpt); optParam) { + LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout, + optParam->appId); + std::scoped_lock lk{g_responses_mutex}; + auto reqIdCopy = *requestId; + g_responses.emplace_back([=]() { + optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_LEAVE_ROOM, 0, + nullptr, optParam->arg); + }); + } + + return ORBIS_OK; +} + +struct OrbisNpMatching2RangeFilter { + u32 start; + u32 max; +}; + +struct OrbisNpMatching2SearchRoomRequest { + int option; + OrbisNpMatching2WorldId worldId; + OrbisNpMatching2LobbyId lobbyId; + OrbisNpMatching2RangeFilter rangeFilter; + OrbisNpMatching2Flags flags1; + OrbisNpMatching2Flags flags2; + void* intFilter; + u64 intFilters; + void* binFilter; + u64 binFilters; + OrbisNpMatching2AttributeId* attr; + u64 attrs; +}; + +struct OrbisNpMatching2Range { + u32 start; + u32 total; + u32 results; + u8 pad[4]; +}; + +struct OrbisNpMatching2SearchRoomResponseA { + OrbisNpMatching2Range range; + void* roomDataExt; +}; + +int PS4_SYSV_ABI sceNpMatching2SearchRoom(OrbisNpMatching2ContextId ctxId, + OrbisNpMatching2SearchRoomRequest* request, + OrbisNpMatching2RequestOptParam* requestOpt, + OrbisNpMatching2RequestId* requestId) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt)); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!request || !requestId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + static OrbisNpMatching2RequestId id = 1; + *requestId = id++; + + if (auto optParam = GetOptParam(requestOpt); optParam) { + LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout, + optParam->appId); + std::scoped_lock lk{g_responses_mutex}; + auto reqIdCopy = *requestId; + auto requestCopy = *request; + g_responses.emplace_back([=]() { + OrbisNpMatching2SearchRoomResponseA resp{{0, 0, 0, {}}, nullptr}; + optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_SEARCH_ROOM_A, 0, + &resp, optParam->arg); + }); + } + + return ORBIS_OK; +} + +struct OrbisNpMatching2SetUserInfoRequest { + OrbisNpMatching2ServerId serverId; + u8 padding[6]; + void* userBinAttr; + u64 userBinAttrs; +}; + +int PS4_SYSV_ABI sceNpMatching2SetUserInfo(OrbisNpMatching2ContextId ctxId, + OrbisNpMatching2SetUserInfoRequest* request, + OrbisNpMatching2RequestOptParam* requestOpt, + OrbisNpMatching2RequestId* requestId) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt)); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!request || !requestId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + static OrbisNpMatching2RequestId id = 100; + *requestId = id++; + + if (auto optParam = GetOptParam(requestOpt); optParam) { + LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout, + optParam->appId); + std::scoped_lock lk{g_responses_mutex}; + auto reqIdCopy = *requestId; + g_responses.emplace_back([=]() { + optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_USER_INFO, 0, + nullptr, optParam->arg); + }); + } + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpMatching2SendRoomMessage(OrbisNpMatching2ContextId ctxId, void* request, + OrbisNpMatching2RequestOptParam* requestOpt, + OrbisNpMatching2RequestId* requestId) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt)); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!request || !requestId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + static OrbisNpMatching2RequestId id = 1000; + *requestId = id++; + + if (auto optParam = GetOptParam(requestOpt); optParam) { + LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout, + optParam->appId); + std::scoped_lock lk{g_responses_mutex}; + auto reqIdCopy = *requestId; + g_responses.emplace_back([=]() { + optParam->callback(ctxId, reqIdCopy, ORBIS_NP_MATCHING2_REQUEST_EVENT_SEND_ROOM_MESSAGE, + 0, nullptr, optParam->arg); + }); + } + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpMatching2SetRoomDataExternal(OrbisNpMatching2ContextId ctxId, void* request, + OrbisNpMatching2RequestOptParam* requestOpt, + OrbisNpMatching2RequestId* requestId) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt)); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!request || !requestId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + static OrbisNpMatching2RequestId id = 800; + *requestId = id++; + + if (auto optParam = GetOptParam(requestOpt); optParam) { + LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout, + optParam->appId); + std::scoped_lock lk{g_responses_mutex}; + auto reqIdCopy = *requestId; + g_responses.emplace_back([=]() { + optParam->callback(ctxId, reqIdCopy, + ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_EXTERNAL, 0, nullptr, + optParam->arg); + }); + } + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceNpMatching2SetRoomDataInternal(OrbisNpMatching2ContextId ctxId, void* request, + OrbisNpMatching2RequestOptParam* requestOpt, + OrbisNpMatching2RequestId* requestId) { + LOG_DEBUG(Lib_NpMatching2, "called, ctxId = {}, requestOpt = {}", ctxId, fmt::ptr(requestOpt)); + + if (!g_initialized) { + return ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED; + } + if (!request || !requestId) { + return ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + } + + static OrbisNpMatching2RequestId id = 200; + *requestId = id++; + + if (auto optParam = GetOptParam(requestOpt); optParam) { + LOG_DEBUG(Lib_NpMatching2, "optParam.timeout = {}, optParam.appId = {}", optParam->timeout, + optParam->appId); + std::scoped_lock lk{g_responses_mutex}; + auto reqIdCopy = *requestId; + g_responses.emplace_back([=]() { + optParam->callback(ctxId, reqIdCopy, + ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_INTERNAL, 0, nullptr, + optParam->arg); + }); + } + + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("10t3e5+JPnU", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2Initialize); + LIB_FUNCTION("Mqp3lJ+sjy4", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2Terminate); + LIB_FUNCTION("YfmpW719rMo", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2CreateContext); + LIB_FUNCTION("ajvzc8e2upo", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2CreateContextA); + LIB_FUNCTION("V6KSpKv9XJE", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2CreateJoinRoomA); + LIB_FUNCTION("fQQfP87I7hs", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2RegisterContextCallback); + LIB_FUNCTION("4Nj7u5B5yCA", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2RegisterLobbyEventCallback); + LIB_FUNCTION("p+2EnxmaAMM", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2RegisterRoomEventCallback); + LIB_FUNCTION("0UMeWRGnZKA", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2RegisterSignalingCallback); + LIB_FUNCTION("7vjNQ6Z1op0", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2ContextStart); + LIB_FUNCTION("LhCPctIICxQ", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2GetServerId); + LIB_FUNCTION("rJNPJqDCpiI", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2GetWorldInfoList); + LIB_FUNCTION("BD6kfx442Do", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2LeaveRoom); + LIB_FUNCTION("+8e7wXLmjds", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2SetDefaultRequestOptParam); + LIB_FUNCTION("VqZX7POg2Mk", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2SearchRoom); + LIB_FUNCTION("Iw2h0Jrrb5U", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2SendRoomMessage); + LIB_FUNCTION("meEjIdbjAA0", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2SetUserInfo); + LIB_FUNCTION("q7GK98-nYSE", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2SetRoomDataExternal); + LIB_FUNCTION("S9D8JSYIrjE", "libSceNpMatching2", 1, "libSceNpMatching2", + sceNpMatching2SetRoomDataInternal); +}; + +} // namespace Libraries::Np::NpMatching2 \ No newline at end of file diff --git a/src/core/libraries/np/np_matching2.h b/src/core/libraries/np/np_matching2.h new file mode 100644 index 000000000..6f7fca900 --- /dev/null +++ b/src/core/libraries/np/np_matching2.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Np::NpMatching2 { + +using OrbisNpMatching2AttributeId = u16; +using OrbisNpMatching2ContextId = u16; +using OrbisNpMatching2Event = u16; +using OrbisNpMatching2EventCause = u8; +using OrbisNpMatching2Flags = u32; +using OrbisNpMatching2LobbyId = u64; +using OrbisNpMatching2NatType = u8; +using OrbisNpMatching2RequestId = u16; +using OrbisNpMatching2RoomId = u64; +using OrbisNpMatching2RoomMemberId = u16; +using OrbisNpMatching2ServerId = u16; +using OrbisNpMatching2TeamId = u8; +using OrbisNpMatching2WorldId = u32; + +constexpr int ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED = 0x80550c01; +constexpr int ORBIS_NP_MATCHING2_ERROR_ALREADY_INITIALIZED = 0x80550c02; +constexpr int ORBIS_NP_MATCHING2_ERROR_INVALID_ARGUMENT = 0x80550c15; + +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_CREATE_JOIN_ROOM = 0x0101; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_CREATE_JOIN_ROOM_A = 0x7101; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_GET_WORLD_INFO_LIST = 0x0002; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SEARCH_ROOM_A = 0x7106; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_LEAVE_ROOM = 0x0103; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SEND_ROOM_MESSAGE = 0x0108; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_EXTERNAL = 0x0004; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_ROOM_DATA_INTERNAL = 0x1106; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_REQUEST_EVENT_SET_USER_INFO = 0x0007; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_CONTEXT_EVENT_START_OVER = 0x6F01; +constexpr OrbisNpMatching2Event ORBIS_NP_MATCHING2_CONTEXT_EVENT_STARTED = 0x6F02; + +constexpr OrbisNpMatching2EventCause ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ERROR = 10; +constexpr OrbisNpMatching2EventCause ORBIS_NP_MATCHING2_EVENT_CAUSE_CONTEXT_ACTION = 11; + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Np::NpMatching2 \ No newline at end of file diff --git a/src/core/libraries/np/np_types.h b/src/core/libraries/np/np_types.h index 8388ae47f..cc37b5a3d 100644 --- a/src/core/libraries/np/np_types.h +++ b/src/core/libraries/np/np_types.h @@ -9,6 +9,8 @@ // For structs and constants shared between multiple Np libraries. namespace Libraries::Np { +using OrbisNpAccountId = u64; + constexpr s32 ORBIS_NP_ONLINEID_MAX_LENGTH = 16; struct OrbisNpOnlineId { @@ -43,4 +45,19 @@ struct OrbisNpIdToken { u8 padding[7]; }; +using OrbisNpServiceLabel = u32; + +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, +}; + +struct OrbisNpPeerAddressA { + OrbisNpAccountId accountId; + OrbisNpPlatformType platformType; + u8 padding[4]; +}; + }; // namespace Libraries::Np \ No newline at end of file From 196143eb9633c005cb069862bf51071e9a25b879 Mon Sep 17 00:00:00 2001 From: Berk Date: Sat, 31 Jan 2026 22:42:12 +0300 Subject: [PATCH 17/50] [building-windows] reworked the windows builder documentation (#3980) * [building-windows] reworked the windows builder documentation * reuse * fix double quotes and more clearer language --- REUSE.toml | 1 + .../Screenshots/Windows/vscode-ext-1.png | Bin 0 -> 34370 bytes .../Screenshots/Windows/vscode-ext-2.png | Bin 0 -> 30300 bytes .../Screenshots/Windows/vscode-ext-3.png | Bin 0 -> 43535 bytes documents/building-windows.md | 165 +++++++++++++++++- 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 documents/Screenshots/Windows/vscode-ext-1.png create mode 100644 documents/Screenshots/Windows/vscode-ext-2.png create mode 100644 documents/Screenshots/Windows/vscode-ext-3.png diff --git a/REUSE.toml b/REUSE.toml index 18200ab28..22bed2a50 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -20,6 +20,7 @@ path = [ "documents/Quickstart/2.png", "documents/Screenshots/*", "documents/Screenshots/Linux/*", + "documents/Screenshots/Windows/*", "externals/MoltenVK/MoltenVK_icd.json", "scripts/ps4_names.txt", "src/images/bronze.png", diff --git a/documents/Screenshots/Windows/vscode-ext-1.png b/documents/Screenshots/Windows/vscode-ext-1.png new file mode 100644 index 0000000000000000000000000000000000000000..b8427b80b4756464d7f3d4229bd9ce289cd3e6a4 GIT binary patch literal 34370 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 MNhOJLal@eh3z$>s_y7O^ literal 0 HcmV?d00001 diff --git a/documents/Screenshots/Windows/vscode-ext-2.png b/documents/Screenshots/Windows/vscode-ext-2.png new file mode 100644 index 0000000000000000000000000000000000000000..082e478a4d4a29f0b28e057a98b2337733fec779 GIT binary patch literal 30300 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

Y6tbvM8kXv#(SEFBs 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 Environment Variables -> System Variables -> Path -> Edit -> New -> C:\ninja` + +### Validate the installs + +```bash +git --version +# git version 2.49.0.windows.1 + +cmake --version +# cmake version 4.2.3 + +ninja --version +# 1.13.2 + +clang --version +# clang version 19.1.1 +``` + +### Install Visual Studio Build Tools + +1. Download [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe) +2. Select `MSVC - Windows SDK` and install (you don't need to install an IDE) + +* Or you can install via `.vsconfig` file: + +``` +{ + "version": "1.0", + "components": [ + "Microsoft.VisualStudio.Component.Roslyn.Compiler", + "Microsoft.Component.MSBuild", + "Microsoft.VisualStudio.Component.CoreBuildTools", + "Microsoft.VisualStudio.Workload.MSBuildTools", + "Microsoft.VisualStudio.Component.Windows10SDK", + "Microsoft.VisualStudio.Component.VC.CoreBuildTools", + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "Microsoft.VisualStudio.Component.VC.Redist.14.Latest", + "Microsoft.VisualStudio.Component.Windows11SDK.26100", + "Microsoft.VisualStudio.Component.TestTools.BuildTools", + "Microsoft.VisualStudio.Component.VC.ASAN", + "Microsoft.VisualStudio.Component.TextTemplating", + "Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Core", + "Microsoft.VisualStudio.Workload.VCTools" + ], + "extensions": [] +} + +Save the file as `.vsconfig` and run the following command: + +%userprofile%\Downloads\vs_BuildTools.exe --passive --config ".vsconfig" + +Be carefull path to vs_BuildTools.exe and .vsconfig file. +``` + +__This will install the necessary components to build shadPS4.__ + +### Project structure + +``` +shadps4/ + ├── shared (shadps4 main files) + └── shadps4.code-workspace +``` + +### Content of `shadps4.code-workspace` + +```json +{ + "folders": [ + { + "path": "shared" + } + ], + "settings": { + "cmake.generator": "Ninja", + + "cmake.configureEnvironment": { + "CMAKE_CXX_STANDARD": "23", + "CMAKE_CXX_STANDARD_REQUIRED": "ON", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + + "cmake.configureOnOpen": false, + + "C_Cpp.intelliSenseEngine": "Disabled", + + "clangd.arguments": [ + "--background-index", + "--clang-tidy", + "--completion-style=detailed", + "--header-insertion=never", + "--compile-commands-dir=Build/x64-Clang-Release" + ], + + "editor.formatOnSave": true, + "clang-format.executable": "clang-format" + }, + + "extensions": { + "recommendations": [ + "llvm-vs-code-extensions.vscode-clangd", + "ms-vscode.cmake-tools", + "xaver.clang-format" + ] + } +} +``` + +### Cloning the source code + +1. Open your terminal and where to shadPS4 folder: `cd shadps4\shared` +3. Clone the repository by running + `git clone --depth 1 --recursive https://github.com/shadps4-emu/shadPS4 .` + +_or fork link_ + +* If you have already cloned repo: +```bash +git submodule update --init --recursive +``` + +### Requirements VSCode extensions +1. CMake Tools +2. Clangd +3. Clang-Format + +_These plugins are suggested in the workspace file above and are already configured._ + +![CMake Tools](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/windows/vscode-ext-1.png) + +![Clangd](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/windows/vscode-ext-2.png) + +![Clang Format](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/windows/vscode-ext-3.png) + +### Building +1. Open VS Code, `File > Open workspace from file > shadps4.code-workspace` +2. Go to the CMake Tools extension on left side bar +3. Change Clang x64 Debug to Clang x64 Release if you want a regular, non-debug build. +4. Click build. + +Your shadps4.exe will be in `shadps4\shared\Build\x64-Clang-Release\` + +## Option 3: MSYS2/MinGW > [!IMPORTANT] -> Building with MSYS2 is broken as of right now, the only way to build on Windows is to use [Option 1: Visual Studio 2022](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-windows.md#option-1-visual-studio-2022). +> Building with MSYS2 is broken as of right now, the only way to build on Windows is to use [Option 1: Visual Studio 2022](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-windows.md#option-1-visual-studio-2022) or [Option 2: VSCode with Visual Studio Build Tools](#option-2-vscode-with-visual-studio-build-tools). ### (Prerequisite) Download [**MSYS2**](https://www.msys2.org/) From 1d36cc7df3d93fe0fbe84e774bfc98d80bcaf910 Mon Sep 17 00:00:00 2001 From: Berk Date: Sat, 31 Jan 2026 22:42:37 +0300 Subject: [PATCH 18/50] .md, documents/ ignored from workflow (#3982) --- .github/workflows/build.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8cf5efbf0..ffe7c22fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,16 @@ name: Build and Release -on: [push, pull_request] +on: + push: + paths-ignore: + - "documents/**" + - "**/*.md" + + pull_request: + paths-ignore: + - "documents/**" + - "**/*.md" concurrency: group: ci-${{ github.event_name }}-${{ github.ref }} From 3bd6a0f9f80e264e4ebbe54bfdc6a3b9ba0137e2 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:31:58 -0600 Subject: [PATCH 19/50] Skip stdin fd for want_read in select (#3983) UE uses this to signal if it should read from stdin, real hardware never marks it as as readable --- src/core/libraries/kernel/file_system.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index 5330b90fd..4b5a53266 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -1262,7 +1262,8 @@ s32 PS4_SYSV_ABI posix_select(s32 nfds, fd_set_posix* readfds, fd_set_posix* wri if (file->type == Core::FileSys::FileType::Regular || file->type == Core::FileSys::FileType::Device) { // Disk files always ready - if (want_read) { + // For devices, stdin (fd 0) is never read-ready. + if (want_read && i != 0) { FD_SET_POSIX(i, &read_ready); } if (want_write) { From 9144b2974268e44a732e3ede9b710f817739ad0e Mon Sep 17 00:00:00 2001 From: psucien <168137814+psucien@users.noreply.github.com> Date: Mon, 2 Feb 2026 07:07:47 +0100 Subject: [PATCH 20/50] shader_recompiler: fix for incorrectly outputted attribute if cdist emulation is not needed (#3986) --- .../backend/spirv/spirv_emit_context.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 4600d30af..261155ab5 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -564,13 +564,12 @@ void EmitContext::DefineVertexBlock() { const bool needs_clip_distance_emulation = l_stage == LogicalStage::Vertex && stage == Stage::Vertex && profile.needs_clip_distance_emulation; - if (!needs_clip_distance_emulation) { - if (info.stores.GetAny(IR::Attribute::ClipDistance)) { - const Id type{TypeArray(F32[1], ConstU32(8U))}; - const Id initializer{ConstantComposite(type, zero)}; - clip_distances = DefineVariable(type, spv::BuiltIn::ClipDistance, - spv::StorageClass::Output, initializer); - } + const auto has_clip_distance_outputs = info.stores.GetAny(IR::Attribute::ClipDistance); + if (has_clip_distance_outputs && !needs_clip_distance_emulation) { + const Id type{TypeArray(F32[1], ConstU32(8U))}; + const Id initializer{ConstantComposite(type, zero)}; + clip_distances = DefineVariable(type, spv::BuiltIn::ClipDistance, spv::StorageClass::Output, + initializer); } if (info.stores.GetAny(IR::Attribute::CullDistance)) { const Id type{TypeArray(F32[1], ConstU32(8U))}; @@ -603,7 +602,9 @@ void EmitContext::DefineOutputs() { Name(output_attr_array, "out_attrs"); } } else { - const auto has_clip_distance_outputs = info.stores.GetAny(IR::Attribute::ClipDistance); + const bool needs_clip_distance_emulation = + stage == Stage::Vertex && profile.needs_clip_distance_emulation && + info.stores.GetAny(IR::Attribute::ClipDistance); u32 num_attrs = 0u; for (u32 i = 0; i < IR::NumParams; i++) { const IR::Attribute param{IR::Attribute::Param0 + i}; @@ -612,14 +613,14 @@ void EmitContext::DefineOutputs() { } const u32 num_components = info.stores.NumComponents(param); const Id id{ - DefineOutput(F32[num_components], i + (has_clip_distance_outputs ? 1 : 0))}; + DefineOutput(F32[num_components], i + (needs_clip_distance_emulation ? 1 : 0))}; Name(id, fmt::format("out_attr{}", i)); output_params[i] = GetAttributeInfo(AmdGpu::NumberFormat::Float, id, num_components, true); ++num_attrs; } - if (has_clip_distance_outputs) { + if (needs_clip_distance_emulation) { clip_distances = Id{DefineOutput(F32[MaxEmulatedClipDistances], 0)}; output_params[num_attrs] = GetAttributeInfo( AmdGpu::NumberFormat::Float, clip_distances, MaxEmulatedClipDistances, true); From d3c6abac4ee788832fad4bfeadadc12a417faf1e Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:16:59 -0600 Subject: [PATCH 21/50] Fix default pthread attributes (#3987) This matches libkernel decomp + old fpPS4 code. Fixes Attack On Titan 2 audio --- src/core/libraries/kernel/threads/pthread.cpp | 8 +------- src/core/libraries/kernel/threads/pthread.h | 6 ++++++ src/core/libraries/kernel/threads/pthread_attr.cpp | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index f1107ef30..20bd20f4b 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -14,12 +14,6 @@ namespace Libraries::Kernel { -constexpr int PthreadInheritSched = 4; - -constexpr int ORBIS_KERNEL_PRIO_FIFO_DEFAULT = 700; -constexpr int ORBIS_KERNEL_PRIO_FIFO_HIGHEST = 256; -constexpr int ORBIS_KERNEL_PRIO_FIFO_LOWEST = 767; - extern PthreadAttr PthreadAttrDefault; void _thread_cleanupspecific(); @@ -231,7 +225,7 @@ int PS4_SYSV_ABI posix_pthread_create_name_np(PthreadT* thread, const PthreadAtt new_thread->attr = *(*attr); new_thread->attr.cpusetsize = 0; } - if (new_thread->attr.sched_inherit == PthreadInheritSched) { + if (curthread != nullptr && new_thread->attr.sched_inherit == PthreadInheritSched) { if (True(curthread->attr.flags & PthreadAttrFlags::ScopeSystem)) { new_thread->attr.flags |= PthreadAttrFlags::ScopeSystem; } else { diff --git a/src/core/libraries/kernel/threads/pthread.h b/src/core/libraries/kernel/threads/pthread.h index cc1cb3829..fed3b96fe 100644 --- a/src/core/libraries/kernel/threads/pthread.h +++ b/src/core/libraries/kernel/threads/pthread.h @@ -24,6 +24,12 @@ class SymbolsResolver; namespace Libraries::Kernel { +constexpr int PthreadInheritSched = 4; + +constexpr int ORBIS_KERNEL_PRIO_FIFO_DEFAULT = 700; +constexpr int ORBIS_KERNEL_PRIO_FIFO_HIGHEST = 256; +constexpr int ORBIS_KERNEL_PRIO_FIFO_LOWEST = 767; + struct Pthread; enum class PthreadMutexFlags : u32 { diff --git a/src/core/libraries/kernel/threads/pthread_attr.cpp b/src/core/libraries/kernel/threads/pthread_attr.cpp index 4fe9e72f2..aa5781f4f 100644 --- a/src/core/libraries/kernel/threads/pthread_attr.cpp +++ b/src/core/libraries/kernel/threads/pthread_attr.cpp @@ -24,9 +24,9 @@ static constexpr std::array ThrPriorities = {{ }}; PthreadAttr PthreadAttrDefault = { - .sched_policy = SchedPolicy::Fifo, - .sched_inherit = 0, - .prio = 0, + .sched_policy = SchedPolicy::Other, + .sched_inherit = PthreadInheritSched, + .prio = ORBIS_KERNEL_PRIO_FIFO_DEFAULT, .suspend = false, .flags = PthreadAttrFlags::ScopeSystem, .stackaddr_attr = nullptr, From e2f3a0f750cb86aae2549f03c1f6510944d32c73 Mon Sep 17 00:00:00 2001 From: Dasaav <111015245+Dasaav-dsv@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:51:50 +0100 Subject: [PATCH 22/50] Prevent `Rasterizer::IsMapped` from returning `true` for memory ranges that wrap the address space (#3989) Signed-off-by: Dasaav-dsv --- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 214d6d697..737c9feed 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1015,6 +1015,10 @@ bool Rasterizer::IsMapped(VAddr addr, u64 size) { // There is no memory, so not mapped. return false; } + if (static_cast(addr) > std::numeric_limits::max() - size) { + // Memory range wrapped the address space, cannot be mapped. + return false; + } const auto range = decltype(mapped_ranges)::interval_type::right_open(addr, addr + size); Common::RecursiveSharedLock lock{mapped_ranges_mutex}; From 8da0b58aaaeaa6e00b6b05e9c68d692bcf8bfffa Mon Sep 17 00:00:00 2001 From: jas0n098 <21629908+jas0n098@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:58:22 +0000 Subject: [PATCH 23/50] Fix thread names being set to garbage (#3985) SetThreadName gets passed an std::string's c_str whose pointer gets invalidated by the assignment of g_curthread->name, resulting in broken thread names further down the line --- src/common/thread.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/thread.cpp b/src/common/thread.cpp index d6daaa852..e56953fb6 100644 --- a/src/common/thread.cpp +++ b/src/common/thread.cpp @@ -175,7 +175,7 @@ bool AccurateSleep(const std::chrono::nanoseconds duration, std::chrono::nanosec // Sets the debugger-visible name of the current thread. void SetCurrentThreadName(const char* name) { if (Libraries::Kernel::g_curthread) { - Libraries::Kernel::g_curthread->name = std::string{name}; + Libraries::Kernel::g_curthread->name = name; } SetThreadDescription(GetCurrentThread(), UTF8ToUTF16W(name).data()); } @@ -190,7 +190,7 @@ void SetThreadName(void* thread, const char* name) { #if !defined(_WIN32) || defined(_MSC_VER) void SetCurrentThreadName(const char* name) { if (Libraries::Kernel::g_curthread) { - Libraries::Kernel::g_curthread->name = std::string{name}; + Libraries::Kernel::g_curthread->name = name; } #ifdef __APPLE__ pthread_setname_np(name); @@ -219,7 +219,7 @@ void SetThreadName(void* thread, const char* name) { #if defined(_WIN32) void SetCurrentThreadName(const char*) { if (Libraries::Kernel::g_curthread) { - Libraries::Kernel::g_curthread->name = std::string{name}; + Libraries::Kernel::g_curthread->name = name; } // Do Nothing on MinGW } From b43573112cf03a1d805e5b9b41d2c74624368ab1 Mon Sep 17 00:00:00 2001 From: Spirtix Date: Tue, 3 Feb 2026 16:05:45 +0100 Subject: [PATCH 24/50] Prevent posix_pthread_mutexattr_settype from setting invalid mutex types (#3991) --- src/core/libraries/kernel/threads/mutex.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/threads/mutex.cpp b/src/core/libraries/kernel/threads/mutex.cpp index 5d97c5dc1..7a046e973 100644 --- a/src/core/libraries/kernel/threads/mutex.cpp +++ b/src/core/libraries/kernel/threads/mutex.cpp @@ -378,7 +378,8 @@ int PS4_SYSV_ABI posix_pthread_mutexattr_getkind_np(PthreadMutexAttrT attr) { } int PS4_SYSV_ABI posix_pthread_mutexattr_settype(PthreadMutexAttrT* attr, PthreadMutexType type) { - if (attr == nullptr || *attr == nullptr || type >= PthreadMutexType::Max) { + if (attr == nullptr || *attr == nullptr || type < PthreadMutexType::ErrorCheck || + type >= PthreadMutexType::Max) { return POSIX_EINVAL; } (*attr)->m_type = type; 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 25/50] 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 26/50] 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 27/50] 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 28/50] 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 29/50] 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 30/50] 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 31/50] 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 32/50] 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 33/50] 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 34/50] 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 35/50] 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 36/50] 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 37/50] 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 38/50] 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 39/50] 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 40/50] 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 41/50] 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 42/50] 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 43/50] [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 44/50] 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 From 99661aa6b3ba7da57eec720d41bca55ae631f8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 10 Feb 2026 22:59:07 +0100 Subject: [PATCH 45/50] RE encountered NpTus functions (#4018) --- CMakeLists.txt | 1 + src/core/libraries/np/np_error.h | 10 +- src/core/libraries/np/np_manager.h | 2 + src/core/libraries/np/np_tus.cpp | 573 ++++++++++++++++++++++--- src/core/libraries/np/np_tus.h | 213 +++------ src/core/libraries/np/object_manager.h | 56 +++ 6 files changed, 647 insertions(+), 208 deletions(-) create mode 100644 src/core/libraries/np/object_manager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cef2df24..ba16732ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -613,6 +613,7 @@ set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/np_sns_facebook_dialog.h src/core/libraries/np/np_partner.cpp src/core/libraries/np/np_partner.h + src/core/libraries/np/object_manager.h ) set(ZLIB_LIB src/core/libraries/zlib/zlib.cpp diff --git a/src/core/libraries/np/np_error.h b/src/core/libraries/np/np_error.h index cf2014148..518344ba3 100644 --- a/src/core/libraries/np/np_error.h +++ b/src/core/libraries/np/np_error.h @@ -13,4 +13,12 @@ constexpr int ORBIS_NP_ERROR_INVALID_SIZE = 0x80550011; constexpr int ORBIS_NP_ERROR_ABORTED = 0x80550012; constexpr int ORBIS_NP_ERROR_REQUEST_MAX = 0x80550013; constexpr int ORBIS_NP_ERROR_REQUEST_NOT_FOUND = 0x80550014; -constexpr int ORBIS_NP_ERROR_INVALID_ID = 0x80550015; \ No newline at end of file +constexpr int ORBIS_NP_ERROR_INVALID_ID = 0x80550015; + +constexpr int ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT = 0x80550704; +constexpr int ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS = 0x80550706; +constexpr int ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = 0x8055070c; +constexpr int ORBIS_NP_COMMUNITY_ERROR_INVALID_ID = 0x8055070e; +constexpr int ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT = 0x80550714; +constexpr int ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID = 0x80550718; +constexpr int ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_NPID = 0x80550719; \ No newline at end of file diff --git a/src/core/libraries/np/np_manager.h b/src/core/libraries/np/np_manager.h index 078fa804a..49250db03 100644 --- a/src/core/libraries/np/np_manager.h +++ b/src/core/libraries/np/np_manager.h @@ -93,6 +93,8 @@ struct OrbisNpCreateAsyncRequestParameter { void RegisterNpCallback(std::string key, std::function cb); void DeregisterNpCallback(std::string key); +s32 PS4_SYSV_ABI sceNpGetNpId(Libraries::UserService::OrbisUserServiceUserId user_id, + OrbisNpId* np_id); s32 PS4_SYSV_ABI sceNpGetOnlineId(Libraries::UserService::OrbisUserServiceUserId user_id, OrbisNpOnlineId* online_id); diff --git a/src/core/libraries/np/np_tus.cpp b/src/core/libraries/np/np_tus.cpp index e0d0aaad1..d18cf4ec0 100644 --- a/src/core/libraries/np/np_tus.cpp +++ b/src/core/libraries/np/np_tus.cpp @@ -1,15 +1,87 @@ // SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include + #include "common/logging/log.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" +#include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_manager.h" #include "core/libraries/np/np_tus.h" +#include "core/libraries/np/np_types.h" +#include "core/libraries/np/object_manager.h" +#include "core/libraries/system/userservice.h" namespace Libraries::Np::NpTus { -s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtx() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +int PackReqId(int libCtxId, int reqId) { + return ((libCtxId & 0xFFFF) << 16) | (reqId & 0xFFFF); +} + +std::pair UnpackReqId(int reqId) { + return {reqId >> 16, reqId & 0xFFFF}; +} + +bool IsReqId(int id) { + return id > (1 << 16); +} + +struct NpTusRequest { + std::future task; + + void Start(auto lambda) { + this->task = std::async(std::launch::async, lambda); + } +}; + +using NpTusRequestsManager = + ObjectManager; + +struct NpTusTitleContext { + u32 serviceLabel; + OrbisNpId npId; + NpTusRequestsManager requestsManager; + + s32 GetRequest(int reqId, NpTusRequest** out) { + NpTusRequest* req = nullptr; + if (auto ret = requestsManager.GetObject(reqId, &req); ret < 0) { + return ret; + } + + *out = req; + + return ORBIS_OK; + } + + s32 DeleteRequest(int reqId) { + return requestsManager.DeleteObject(reqId); + } +}; + +using NpTusContextManager = + ObjectManager; + +static NpTusContextManager ctxManager; + +s32 GetRequest(int requestId, NpTusRequest** out) { + auto [ctxId, reqId] = UnpackReqId(requestId); + + NpTusTitleContext* ctx = nullptr; + if (auto ret = ctxManager.GetObject(ctxId, &ctx); ret < 0) { + return ret; + } + + NpTusRequest* req = nullptr; + if (auto ret = ctx->GetRequest(reqId, &req); ret < 0) { + return ret; + } + + *out = req; + return ORBIS_OK; } @@ -33,9 +105,34 @@ s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableVUserAsync() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtx() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtx(OrbisNpServiceLabel serviceLabel, OrbisNpId* npId) { + if (!npId) { + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (serviceLabel == ORBIS_NP_INVALID_SERVICE_LABEL) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + LOG_ERROR(Lib_NpTus, "serviceLabel = {}, npId->data = {}", serviceLabel, npId->handle.data); + + return ctxManager.CreateObject(serviceLabel, *npId); +} + +s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtxA(OrbisNpServiceLabel serviceLabel, + Libraries::UserService::OrbisUserServiceUserId userId) { + LOG_ERROR(Lib_NpTus, "serviceLabel = {}, userId = {}", serviceLabel, userId); + OrbisNpId npId; + auto ret = NpManager::sceNpGetNpId(userId, &npId); + + if (ret < 0) { + return ret; + } + + return sceNpTusCreateNpTitleCtx(serviceLabel, &npId); +} + +s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtx(OrbisNpServiceLabel serviceLabel, OrbisNpId* npId) { + LOG_INFO(Lib_NpTus, "redirecting"); + return sceNpTusCreateNpTitleCtx(serviceLabel, npId); } s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotData() { @@ -58,14 +155,33 @@ s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableAsync() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusGetData() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusGetDataAsync(int reqId, OrbisNpId* npId, OrbisNpTusSlotId slotId, + OrbisNpTusDataStatus* dataStatus, u64 dataStatusSize, + void* data, u64 dataSize, void* option) { + LOG_INFO( + Lib_NpTus, + "reqId = {:#x}, slotId = {}, dataStatusSize = {}, data = {}, dataSize = {}, option = {}", + reqId, slotId, dataStatusSize, data, dataSize, fmt::ptr(option)); return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusGetDataAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceNpTusGetData(int reqId, OrbisNpId* npId, OrbisNpTusSlotId slotId, + OrbisNpTusDataStatus* dataStatus, u64 dataStatusSize, void* data, + u64 dataSize, void* option) { + LOG_ERROR( + Lib_NpTus, + "reqId = {:#x}, slotId = {}, dataStatusSize = {}, data = {}, dataSize = {}, option = {}", + reqId, slotId, dataStatusSize, data, dataSize, fmt::ptr(option)); + + auto ret = sceNpTusGetDataAsync(reqId, npId, slotId, dataStatus, dataStatusSize, data, dataSize, + option); + if (ret < 0) { + return ret; + } + + sceNpTusWaitAsync(reqId, &ret); + + return ret; } s32 PS4_SYSV_ABI sceNpTusGetDataVUser() { @@ -118,13 +234,45 @@ s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusVUserAsync() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariable() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAsync(int reqId, OrbisNpId* npId, + OrbisNpTusSlotId* slotIds, s64* variables, + u64 variablesSize, int arrayLen, void* option) { + LOG_INFO(Lib_NpTus, + "reqId = {}, npId = {}, slotIds = {}, variables = {}, variablesSize = {}, arrayLen = " + "{}, option = {}", + reqId, npId ? npId->handle.data : "", fmt::ptr(slotIds), fmt::ptr(variables), + variablesSize, arrayLen, fmt::ptr(option)); + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + + req->Start([=]() { + // + return 0; + }); + return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariable(int reqId, OrbisNpId* npId, OrbisNpTusSlotId* slotIds, + s64* variables, u64 variablesSize, int arrayLen, + void* option) { + LOG_INFO(Lib_NpTus, + "reqId = {}, npId = {}, slotIds = {}, variables = {}, variablesSize = {}, arrayLen = " + "{}, option = {}", + reqId, npId ? npId->handle.data : "", fmt::ptr(slotIds), fmt::ptr(variables), + variablesSize, arrayLen, fmt::ptr(option)); + + auto ret = sceNpTusGetMultiSlotVariableAsync(reqId, npId, slotIds, variables, variablesSize, + arrayLen, option); + if (ret < 0) { + return ret; + } + + sceNpTusWaitAsync(reqId, &ret); + return ORBIS_OK; } @@ -143,8 +291,24 @@ s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatus() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAsync(int reqId, OrbisNpId* npIds, + OrbisNpTusSlotId slotId, + OrbisNpTusDataStatus* statuses, + u64 statusesBytes, int arrayLen, + void* option) { + LOG_ERROR(Lib_NpTus, "(STUBBED) reqId = {:#x}, slotId = {}, arrayLen = {}, option = {}", reqId, + slotId, arrayLen, fmt::ptr(option)); + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + + req->Start([=]() { + // + return 0; + }); + return ORBIS_OK; } @@ -178,13 +342,49 @@ s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableVUserAsync() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusSetData() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusSetDataAsync(int reqId, OrbisNpId* npId, OrbisNpTusSlotId slotId, + u64 totalSize, u64 sendSize, const void* data, + const OrbisNpTusDataInfo* info, u64 infoSize, + const OrbisNpId* lastChangedAuthor, + Libraries::Rtc::OrbisRtcTick lastChanged, void* option) { + LOG_INFO(Lib_NpTus, + "reqId = {:#x}, npId = {}, slotId = {}, totalSize = {}, sendSize = {}, " + "info->size = {}, infoSize = {}, lastChangedAuthor = {}", + reqId, npId ? npId->handle.data : "", slotId, totalSize, sendSize, + info ? info->size : 0, infoSize, + lastChangedAuthor ? lastChangedAuthor->handle.data : ""); + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + req->Start([=]() { + // + return 0; + }); + return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusSetDataAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusSetData(int reqId, OrbisNpId* npId, OrbisNpTusSlotId slotId, u64 totalSize, + u64 sendSize, const void* data, const OrbisNpTusDataInfo* info, + u64 infoSize, const OrbisNpId* lastChangedAuthor, + Libraries::Rtc::OrbisRtcTick lastChanged, void* option) { + LOG_INFO(Lib_NpTus, + "reqId = {:#x}, npId = {}, slotId = {}, totalSize = {}, sendSize = {}, " + "info->size = {}, infoSize = {}, lastChangedAuthor = {}", + reqId, npId ? npId->handle.data : "", slotId, totalSize, sendSize, + info ? info->size : 0, infoSize, + lastChangedAuthor ? lastChangedAuthor->handle.data : ""); + + auto ret = sceNpTusSetDataAsync(reqId, npId, slotId, totalSize, sendSize, data, info, infoSize, + lastChangedAuthor, lastChanged, option); + if (ret < 0) { + return ret; + } + + sceNpTusWaitAsync(reqId, &ret); + return ORBIS_OK; } @@ -203,8 +403,39 @@ s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariable() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableAsync(int reqId, OrbisNpId* npId, + OrbisNpTusSlotId* slotIds, s64* variables, + int arrayLen, void* option) { + LOG_INFO(Lib_NpTus, + "reqId = {}, npId = {}, slotIds = {}, variables = {}, arrayLen = {}, option = {}", + reqId, npId ? npId->handle.data : "", fmt::ptr(slotIds), fmt::ptr(variables), arrayLen, + fmt::ptr(option)); + + if (!slotIds || !variables) { + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (arrayLen < 1 || option) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (arrayLen > 64) { + return ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + } + if (std::ranges::any_of( + std::vector>(slotIds, slotIds + arrayLen), + [](auto id) { return id < 0; })) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + + req->Start([=]() { + // + return 0; + }); + return ORBIS_OK; } @@ -228,19 +459,59 @@ s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableVUserAsync() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtxA() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtxA(OrbisNpServiceLabel serviceLabel, + Libraries::UserService::OrbisUserServiceUserId userId) { + LOG_DEBUG(Lib_NpTus, "redirecting"); + return sceNpTusCreateNpTitleCtxA(serviceLabel, userId); +} + +s32 PS4_SYSV_ABI sceNpTssGetDataAsync(int reqId, OrbisNpTssSlotId slotId, + OrbisNpTssDataStatus* dataStatus, u64 dataStatusSize, + void* data, u64 dataSize, OrbisNpTssGetDataOptParam* option) { + LOG_INFO(Lib_NpTus, "reqId = {:#x}, slotId = {}, dataStatusSize = {}, dataSize = {}", reqId, + slotId, dataStatusSize, dataSize); + + if (option && option->size != 0x20) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + if (dataStatus && dataStatusSize != 0x18) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + if (slotId < 0 || slotId > 15) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + + req->Start([=]() { + if (dataStatus) { + dataStatus->status = OrbisNpTssStatus::Ok; + dataStatus->contentLength = 0; + } + return 0; + }); + return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTssGetData() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; -} +s32 PS4_SYSV_ABI sceNpTssGetData(int reqId, OrbisNpTssSlotId slotId, + OrbisNpTssDataStatus* dataStatus, u64 dataStatusSize, void* data, + u64 dataSize, OrbisNpTssGetDataOptParam* option) { + LOG_INFO(Lib_NpTus, "reqId = {:#x}, slotId = {}, dataStatusSize = {}, dataSize = {}", reqId, + slotId, dataStatusSize, dataSize); -s32 PS4_SYSV_ABI sceNpTssGetDataAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; + auto ret = + sceNpTssGetDataAsync(reqId, slotId, dataStatus, dataStatusSize, data, dataSize, option); + if (ret < 0) { + return ret; + } + + sceNpTusWaitAsync(reqId, &ret); + + return ret; } s32 PS4_SYSV_ABI sceNpTssGetSmallStorage() { @@ -313,14 +584,20 @@ s32 PS4_SYSV_ABI sceNpTusChangeModeForOtherSaveDataOwners() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtxA() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; -} +s32 PS4_SYSV_ABI sceNpTusCreateRequest(int libCtxId) { + LOG_INFO(Lib_NpTus, "libCtxId = {}", libCtxId); -s32 PS4_SYSV_ABI sceNpTusCreateRequest() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; + NpTusTitleContext* ctx = nullptr; + if (auto ret = ctxManager.GetObject(libCtxId, &ctx); ret < 0) { + return ret; + } + + auto req = ctx->requestsManager.CreateObject(); + if (req < 0) { + return req; + } + + return PackReqId(libCtxId, req); } s32 PS4_SYSV_ABI sceNpTusCreateTitleCtx() { @@ -368,24 +645,70 @@ s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableVUserAsync() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusDeleteNpTitleCtx() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusDeleteNpTitleCtx(int ctxId) { + LOG_INFO(Lib_NpTus, "ctxId = {}", ctxId); + + return ctxManager.DeleteObject(ctxId); +} + +s32 PS4_SYSV_ABI sceNpTusDeleteRequest(int requestId) { + LOG_INFO(Lib_NpTus, "requestId = {:#x}", requestId); + + auto [ctxId, reqId] = UnpackReqId(requestId); + + NpTusTitleContext* ctx = nullptr; + if (auto ret = ctxManager.GetObject(ctxId, &ctx); ret < 0) { + return ret; + } + + return ctx->DeleteRequest(reqId); +} + +s32 PS4_SYSV_ABI sceNpTusGetDataAAsync(int reqId, OrbisNpAccountId accountId, + OrbisNpTusSlotId slotId, OrbisNpTusDataStatusA* dataStatus, + u64 dataStatusSize, void* data, u64 dataSize, void* option) { + LOG_INFO(Lib_NpTus, + "reqId = {:#x}, accountId = {:#x}, slotId = {}, dataStatus = {}, dataStatusSize = {}, " + "dataSize = {}", + reqId, accountId, slotId, fmt::ptr(dataStatus), dataStatusSize, dataSize); + + if (slotId < 0 || option) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (dataStatusSize != sizeof(OrbisNpTusDataStatusA)) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + + req->Start([=]() { + // + return 0; + }); + return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusDeleteRequest() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; -} +s32 PS4_SYSV_ABI sceNpTusGetDataA(int reqId, OrbisNpAccountId accountId, OrbisNpTusSlotId slotId, + OrbisNpTusDataStatusA* dataStatus, u64 dataStatusSize, void* data, + u64 dataSize, void* option) { + LOG_INFO(Lib_NpTus, + "reqId = {:#x}, accountId = {:#x}, slotId = {}, dataStatus = {}, dataStatusSize = {}, " + "dataSize = {}", + reqId, accountId, slotId, fmt::ptr(dataStatus), dataStatusSize, dataSize); -s32 PS4_SYSV_ABI sceNpTusGetDataA() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; -} + auto ret = sceNpTusGetDataAAsync(reqId, accountId, slotId, dataStatus, dataStatusSize, data, + dataSize, option); + if (ret < 0) { + return ret; + } -s32 PS4_SYSV_ABI sceNpTusGetDataAAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; + sceNpTusWaitAsync(reqId, &ret); + + return ret; } s32 PS4_SYSV_ABI sceNpTusGetDataAVUser() { @@ -458,13 +781,62 @@ s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableForCrossSaveAsync() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusA() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAAsync(int reqId, OrbisNpAccountId accountId, + OrbisNpTusSlotId* slotIds, + OrbisNpTusDataStatusA* statuses, + u64 statusesSize, int arrayLen, + void* option) { + LOG_ERROR(Lib_NpTus, "reqId = {:#x}, accountId = {}, arrayLen = {}, option = {}", reqId, + accountId, arrayLen, fmt::ptr(option)); + + if (!slotIds || !statuses) { + return ORBIS_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + } + if (arrayLen < 1 || option) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + if (arrayLen * sizeof(OrbisNpTusDataStatusA) != statusesSize) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + } + if (arrayLen > 64) { + return ORBIS_NP_COMMUNITY_ERROR_TOO_MANY_SLOTID; + } + if (std::ranges::any_of( + std::vector>(slotIds, slotIds + arrayLen), + [](auto id) { return id < 0; })) { + return ORBIS_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } + + // if sdk_ver >= 5.50 clear the statuses array + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + + req->Start([=]() { + // + return 0; + }); + return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusA(int reqId, OrbisNpAccountId accountId, + OrbisNpTusSlotId* slotIds, + OrbisNpTusDataStatusA* statuses, u64 statusesSize, + int arrayLen, void* option) { + LOG_ERROR(Lib_NpTus, "reqId = {:#x}, accountId = {}, arrayLen = {}, option = {}", reqId, + accountId, arrayLen, fmt::ptr(option)); + + auto ret = sceNpTusGetMultiSlotDataStatusAAsync(reqId, accountId, slotIds, statuses, + statusesSize, arrayLen, option); + if (ret < 0) { + return ret; + } + + sceNpTusWaitAsync(reqId, &ret); + return ORBIS_OK; } @@ -618,18 +990,72 @@ s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusPollAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusPollAsync(int reqId, int* result) { + LOG_INFO(Lib_NpTus, "reqId = {:#x}", reqId); + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + + if (!req->task.valid()) { + LOG_ERROR(Lib_NpTus, "request not started"); + return 1; + } + if (req->task.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + LOG_DEBUG(Lib_NpTus, "request finished"); + if (result) { + *result = req->task.get(); + } + return 0; + } + + return 1; +} + +s32 PS4_SYSV_ABI sceNpTusSetDataAAsync(int reqId, OrbisNpAccountId accountId, + OrbisNpTusSlotId slotId, u64 totalSize, u64 sendSize, + const void* data, const OrbisNpTusDataInfo* info, + u64 infoSize, const OrbisNpAccountId* lastChangedAuthor, + Libraries::Rtc::OrbisRtcTick* lastChanged, void* option) { + LOG_INFO(Lib_NpTus, + "reqId = {:#x}, accountId = {}, slotId = {}, totalSize = {}, sendSize = {}, " + "info->size = {}, infoSize = {}, lastChangedAuthor = {}", + reqId, accountId, slotId, totalSize, sendSize, info ? info->size : 0, infoSize, + lastChangedAuthor ? *lastChangedAuthor : 0); + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + + req->Start([=]() { + // + return 0; + }); + return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusSetDataA() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); - return ORBIS_OK; -} +s32 PS4_SYSV_ABI sceNpTusSetDataA(int reqId, OrbisNpAccountId accountId, OrbisNpTusSlotId slotId, + u64 totalSize, u64 sendSize, const void* data, + const OrbisNpTusDataInfo* info, u64 infoSize, + const OrbisNpAccountId* lastChangedAuthor, + Libraries::Rtc::OrbisRtcTick* lastChanged, void* option) { + LOG_INFO(Lib_NpTus, + "reqId = {:#x}, accountId = {}, slotId = {}, totalSize = {}, sendSize = {}, " + "info->size = {}, infoSize = {}, lastChangedAuthor = {}", + reqId, accountId, slotId, totalSize, sendSize, info ? info->size : 0, infoSize, + lastChangedAuthor ? *lastChangedAuthor : 0); + + auto ret = sceNpTusSetDataAAsync(reqId, accountId, slotId, totalSize, sendSize, data, info, + infoSize, lastChangedAuthor, lastChanged, option); + if (ret < 0) { + return ret; + } + + sceNpTusWaitAsync(reqId, &ret); -s32 PS4_SYSV_ABI sceNpTusSetDataAAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); return ORBIS_OK; } @@ -713,8 +1139,25 @@ s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTusWaitAsync() { - LOG_ERROR(Lib_NpTus, "(STUBBED) called"); +s32 PS4_SYSV_ABI sceNpTusWaitAsync(int reqId, int* result) { + LOG_INFO(Lib_NpTus, "reqId = {:#x}", reqId); + + NpTusRequest* req = nullptr; + if (auto ret = GetRequest(reqId, &req); ret < 0) { + return ret; + } + + if (!req->task.valid()) { + LOG_ERROR(Lib_NpTus, "request not started"); + return 1; + } + + req->task.wait(); + + LOG_DEBUG(Lib_NpTus, "request finished"); + if (result) { + *result = req->task.get(); + } return ORBIS_OK; } diff --git a/src/core/libraries/np/np_tus.h b/src/core/libraries/np/np_tus.h index 3c18099b2..ef4554ec3 100644 --- a/src/core/libraries/np/np_tus.h +++ b/src/core/libraries/np/np_tus.h @@ -4,6 +4,7 @@ #pragma once #include "common/types.h" +#include "core/libraries/rtc/rtc.h" namespace Core::Loader { class SymbolsResolver; @@ -11,148 +12,76 @@ class SymbolsResolver; namespace Libraries::Np::NpTus { -s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtx(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariable(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAsync(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableVUser(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtx(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotData(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataAsync(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariable(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableAsync(); -s32 PS4_SYSV_ABI sceNpTusGetData(); -s32 PS4_SYSV_ABI sceNpTusGetDataAsync(); -s32 PS4_SYSV_ABI sceNpTusGetDataVUser(); -s32 PS4_SYSV_ABI sceNpTusGetDataVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatus(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusAsync(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsVariable(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatus(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariable(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatus(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariable(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusSetData(); -s32 PS4_SYSV_ABI sceNpTusSetDataAsync(); -s32 PS4_SYSV_ABI sceNpTusSetDataVUser(); -s32 PS4_SYSV_ABI sceNpTusSetDataVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariable(); -s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableAsync(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariable(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAsync(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableVUser(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableVUserAsync(); -s32 PS4_SYSV_ABI sceNpTssCreateNpTitleCtxA(); -s32 PS4_SYSV_ABI sceNpTssGetData(); -s32 PS4_SYSV_ABI sceNpTssGetDataAsync(); -s32 PS4_SYSV_ABI sceNpTssGetSmallStorage(); -s32 PS4_SYSV_ABI sceNpTssGetSmallStorageAsync(); -s32 PS4_SYSV_ABI sceNpTssGetStorage(); -s32 PS4_SYSV_ABI sceNpTssGetStorageAsync(); -s32 PS4_SYSV_ABI sceNpTusAbortRequest(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableA(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAAsync(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAVUser(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableAVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSave(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSaveAsync(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSaveVUser(); -s32 PS4_SYSV_ABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusChangeModeForOtherSaveDataOwners(); -s32 PS4_SYSV_ABI sceNpTusCreateNpTitleCtxA(); -s32 PS4_SYSV_ABI sceNpTusCreateRequest(); -s32 PS4_SYSV_ABI sceNpTusCreateTitleCtx(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataA(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataAAsync(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataVUser(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotDataVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableA(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableAAsync(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableVUser(); -s32 PS4_SYSV_ABI sceNpTusDeleteMultiSlotVariableVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusDeleteNpTitleCtx(); -s32 PS4_SYSV_ABI sceNpTusDeleteRequest(); -s32 PS4_SYSV_ABI sceNpTusGetDataA(); -s32 PS4_SYSV_ABI sceNpTusGetDataAAsync(); -s32 PS4_SYSV_ABI sceNpTusGetDataAVUser(); -s32 PS4_SYSV_ABI sceNpTusGetDataAVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSave(); -s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSaveAsync(); -s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSaveVUser(); -s32 PS4_SYSV_ABI sceNpTusGetDataForCrossSaveVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusA(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusAAsync(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusForCrossSave(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsDataStatusForCrossSaveAsync(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableA(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableAAsync(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableForCrossSave(); -s32 PS4_SYSV_ABI sceNpTusGetFriendsVariableForCrossSaveAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusA(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusAVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSave(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableA(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableAVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSave(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSaveAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSaveVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusA(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusAVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSave(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableA(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableAVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSave(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveAsync(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveVUser(); -s32 PS4_SYSV_ABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusPollAsync(); -s32 PS4_SYSV_ABI sceNpTusSetDataA(); -s32 PS4_SYSV_ABI sceNpTusSetDataAAsync(); -s32 PS4_SYSV_ABI sceNpTusSetDataAVUser(); -s32 PS4_SYSV_ABI sceNpTusSetDataAVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableA(); -s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableAAsync(); -s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableVUser(); -s32 PS4_SYSV_ABI sceNpTusSetMultiSlotVariableVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusSetThreadParam(); -s32 PS4_SYSV_ABI sceNpTusSetTimeout(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableA(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAAsync(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAVUser(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableAVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSave(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveAsync(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveVUser(); -s32 PS4_SYSV_ABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(); -s32 PS4_SYSV_ABI sceNpTusWaitAsync(); +using OrbisNpTssSlotId = s32; +using OrbisNpTusSlotId = s32; + +struct OrbisNpTusVariable { + OrbisNpId npId; + int set; + Libraries::Rtc::OrbisRtcTick lastChanged; + u8 pad1[4]; + OrbisNpId lastChangedAuthor; + s64 variable; + s64 oldVariable; + OrbisNpAccountId owner; + OrbisNpAccountId lastChangedAuthorId; +}; + +struct OrbisNpTusDataInfo { + u64 size; + u8 data[384]; +}; + +struct OrbisNpTusDataStatus { + OrbisNpId npId; + int set; + Libraries::Rtc::OrbisRtcTick lastChanged; + OrbisNpId lastChangedAuthor; + u8 pad2[4]; + void* data; + u64 dataSize; + OrbisNpTusDataInfo info; +}; + +static_assert(sizeof(OrbisNpTusDataStatus) == 0x1F0); + +struct OrbisNpTusDataStatusA { + OrbisNpOnlineId onlineId; + u8 pad[16]; + int set; + Libraries::Rtc::OrbisRtcTick lastChanged; + OrbisNpOnlineId lastChangedAuthor; + u8 pad2[20]; + void* data; + u64 dataSize; + OrbisNpTusDataInfo info; + OrbisNpAccountId owner; + OrbisNpAccountId lastChangedAuthorId; + u8 pad3[16]; +}; + +static_assert(sizeof(OrbisNpTusDataStatusA) == 0x210); + +enum class OrbisNpTssStatus : int { + Ok = 0, + Partial = 1, + NotModified = 2, +}; + +struct OrbisNpTssDataStatus { + Libraries::Rtc::OrbisRtcTick modified; + OrbisNpTssStatus status; + u64 contentLength; +}; + +struct OrbisNpTssGetDataOptParam { + u64 size; + u64* offset; + u64* last; + void* param; +}; + +s32 PS4_SYSV_ABI sceNpTusWaitAsync(int reqId, int* result); void RegisterLib(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::Np::NpTus \ No newline at end of file diff --git a/src/core/libraries/np/object_manager.h b/src/core/libraries/np/object_manager.h new file mode 100644 index 000000000..5f6ed9663 --- /dev/null +++ b/src/core/libraries/np/object_manager.h @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +template +struct ObjectManager { + s32 GetObject(int objectId, T** out) { + std::scoped_lock lk{mutex}; + if (objectId < 1 || objectId > N) { + return INVALID_OBJECT_ID_ERROR; + } + auto obj = objects[objectId - 1]; + if (!obj) { + return OBJECT_NOT_FOUND_ERROR; + } + *out = obj; + return ORBIS_OK; + } + + template + s32 CreateObject(Args&&... args) { + std::scoped_lock lk{mutex}; + + if (auto slot = std::ranges::find(objects, nullptr); slot != objects.end()) { + *slot = new T{args...}; + + return std::ranges::distance(objects.begin(), slot) + 1; + } + + return MAX_OBJECTS_ERROR; + } + + s32 DeleteObject(int objectId) { + std::scoped_lock lk{mutex}; + + if (objectId < 1 || objectId > N) { + return INVALID_OBJECT_ID_ERROR; + } + auto obj = objects[objectId - 1]; + if (!obj) { + return OBJECT_NOT_FOUND_ERROR; + } + + delete obj; + objects[objectId - 1] = nullptr; + + return ORBIS_OK; + } + +private: + std::mutex mutex; + std::array objects = {nullptr}; +}; \ No newline at end of file From 6a7f8a9e5aa9a5dd78222b2cc2aba2ac97c0526b Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:13:28 -0600 Subject: [PATCH 46/50] Libs.LibcInternal: _Fofind, _Lockfilelock, _Unlockfilelock, _Foprep, _Fopen, fopen, fflush, _Nnl, _Fspos, fseek, _Frprep, fread, _Fofree, fclose, _Mtxinit, _Mtxlock, _Mtxunlock, _Mtxdst implementations (#4019) * Initial definitions * internal__Fofind * Libcinternal threads fopen stores a valid pthread mutex in the FILE struct. Since this is exposed to the game/app, we need to handle this accurately. * internal__Foprep (and various other functions called in it) * Actual fopen implementation At long last, an actual function I'm supposed to implement. * fflush + compile fixes * fseek implementation Comes with functions fseek calls, aside from fflush which I pushed earlier. * fread, _Frprep Also changed some parameter names a tad to match how I named things in my decomp. And fixed some bugs with how I was handling the weird offseted mode thing * fclose, _Fofree Not confident on this one, but we'll see I guess. * Bug fixing No more crashes at least, fread seems to be broken though. * fopen bugfixes Behavior now matches LLE, at least in how LLE font seems to use it. * Fix _Frprep Seems like everything works now? * Logging Probably going to need to swap lseek and read logs to debug/trace later but this is for debugging. * Remove alignment check Seems I must've misinterpreted some of what Ghidra spat out, since libSceNgs2 is calling with size 1, nmemb 4. * Reduce fseek, fread logs to trace * Clang --- CMakeLists.txt | 2 + src/core/libraries/kernel/file_system.h | 3 + src/core/libraries/kernel/threads.h | 10 + .../libraries/libc_internal/libc_internal.cpp | 7 + .../libraries/libc_internal/libc_internal.h | 1 + .../libc_internal/libc_internal_io.cpp | 467 +++++++++++++++++- .../libc_internal/libc_internal_io.h | 84 ++++ .../libc_internal/libc_internal_threads.cpp | 65 +++ .../libc_internal/libc_internal_threads.h | 22 + src/core/libraries/libs.cpp | 1 + 10 files changed, 660 insertions(+), 2 deletions(-) create mode 100644 src/core/libraries/libc_internal/libc_internal_threads.cpp create mode 100644 src/core/libraries/libc_internal/libc_internal_threads.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ba16732ff..484a1d4d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -496,6 +496,8 @@ set(HLE_LIBC_INTERNAL_LIB src/core/libraries/libc_internal/libc_internal.cpp src/core/libraries/libc_internal/libc_internal_str.h src/core/libraries/libc_internal/libc_internal_math.cpp src/core/libraries/libc_internal/libc_internal_math.h + src/core/libraries/libc_internal/libc_internal_threads.cpp + src/core/libraries/libc_internal/libc_internal_threads.h src/core/libraries/libc_internal/printf.h ) diff --git a/src/core/libraries/kernel/file_system.h b/src/core/libraries/kernel/file_system.h index 507f0952c..d8989828a 100644 --- a/src/core/libraries/kernel/file_system.h +++ b/src/core/libraries/kernel/file_system.h @@ -65,6 +65,9 @@ constexpr int ORBIS_KERNEL_O_DSYNC = 0x1000; constexpr int ORBIS_KERNEL_O_DIRECT = 0x00010000; constexpr int ORBIS_KERNEL_O_DIRECTORY = 0x00020000; +s32 PS4_SYSV_ABI posix_open(const char* path, s32 flags, u16 mode); +s32 PS4_SYSV_ABI posix_close(s32 fd); +s64 PS4_SYSV_ABI posix_lseek(s32 fd, s64 offset, s32 whence); s64 PS4_SYSV_ABI sceKernelWrite(s32 fd, const void* buf, u64 nbytes); s64 PS4_SYSV_ABI sceKernelRead(s32 fd, void* buf, u64 nbytes); s64 PS4_SYSV_ABI sceKernelPread(s32 fd, void* buf, u64 nbytes, s64 offset); diff --git a/src/core/libraries/kernel/threads.h b/src/core/libraries/kernel/threads.h index bcccf1695..42ab0b13f 100644 --- a/src/core/libraries/kernel/threads.h +++ b/src/core/libraries/kernel/threads.h @@ -28,6 +28,16 @@ int PS4_SYSV_ABI posix_pthread_create(PthreadT* thread, const PthreadAttrT* attr int PS4_SYSV_ABI posix_pthread_join(PthreadT pthread, void** thread_return); +int PS4_SYSV_ABI posix_pthread_mutexattr_init(PthreadMutexAttrT* attr); +int PS4_SYSV_ABI posix_pthread_mutexattr_settype(PthreadMutexAttrT* attr, PthreadMutexType type); +int PS4_SYSV_ABI posix_pthread_mutexattr_destroy(PthreadMutexAttrT* attr); + +int PS4_SYSV_ABI scePthreadMutexInit(PthreadMutexT* mutex, const PthreadMutexAttrT* mutex_attr, + const char* name); +int PS4_SYSV_ABI posix_pthread_mutex_lock(PthreadMutexT* mutex); +int PS4_SYSV_ABI posix_pthread_mutex_unlock(PthreadMutexT* mutex); +int PS4_SYSV_ABI posix_pthread_mutex_destroy(PthreadMutexT* mutex); + void RegisterThreads(Core::Loader::SymbolsResolver* sym); class Thread { diff --git a/src/core/libraries/libc_internal/libc_internal.cpp b/src/core/libraries/libc_internal/libc_internal.cpp index df35680a4..6a92d2317 100644 --- a/src/core/libraries/libc_internal/libc_internal.cpp +++ b/src/core/libraries/libc_internal/libc_internal.cpp @@ -11,6 +11,7 @@ #include "libc_internal_math.h" #include "libc_internal_memory.h" #include "libc_internal_str.h" +#include "libc_internal_threads.h" #include "printf.h" namespace Libraries::LibcInternal { @@ -20,5 +21,11 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { RegisterlibSceLibcInternalStr(sym); RegisterlibSceLibcInternalMemory(sym); RegisterlibSceLibcInternalIo(sym); + RegisterlibSceLibcInternalThreads(sym); +} + +void ForceRegisterLib(Core::Loader::SymbolsResolver* sym) { + // Used to forcibly enable HLEs for broken LLE functions. + ForceRegisterlibSceLibcInternalIo(sym); } } // namespace Libraries::LibcInternal \ No newline at end of file diff --git a/src/core/libraries/libc_internal/libc_internal.h b/src/core/libraries/libc_internal/libc_internal.h index 9b00b72d8..1494a2ab2 100644 --- a/src/core/libraries/libc_internal/libc_internal.h +++ b/src/core/libraries/libc_internal/libc_internal.h @@ -15,4 +15,5 @@ namespace Libraries::LibcInternal { // so everything is just in the .cpp file void RegisterLib(Core::Loader::SymbolsResolver* sym); +void ForceRegisterLib(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::LibcInternal \ No newline at end of file diff --git a/src/core/libraries/libc_internal/libc_internal_io.cpp b/src/core/libraries/libc_internal/libc_internal_io.cpp index 8105b66cc..504ba5b48 100644 --- a/src/core/libraries/libc_internal/libc_internal_io.cpp +++ b/src/core/libraries/libc_internal/libc_internal_io.cpp @@ -3,21 +3,484 @@ #include #include +#include #include +#include "common/alignment.h" #include "common/assert.h" #include "common/logging/log.h" #include "core/libraries/error_codes.h" +#include "core/libraries/kernel/file_system.h" +#include "core/libraries/kernel/kernel.h" +#include "core/libraries/kernel/posix_error.h" +#include "core/libraries/libc_internal/libc_internal_io.h" +#include "core/libraries/libc_internal/libc_internal_threads.h" #include "core/libraries/libs.h" -#include "libc_internal_io.h" #include "printf.h" namespace Libraries::LibcInternal { -int PS4_SYSV_ABI internal_snprintf(char* s, size_t n, VA_ARGS) { + +s32 PS4_SYSV_ABI internal_snprintf(char* s, u64 n, VA_ARGS) { VA_CTX(ctx); return snprintf_ctx(s, n, &ctx); } + +std::map g_files{}; +// Constants for tracking accurate file indexes. +// Since the file struct is exposed to the application, accuracy is important. +static constexpr s32 g_initial_files = 5; +static constexpr s32 g_max_files = 0x100; + +OrbisFILE* PS4_SYSV_ABI internal__Fofind() { + u64 index = g_initial_files; + while (index != g_max_files) { + OrbisFILE* file = g_files[index]; + // If file doesn't exist, create it. + if (file == nullptr) { + file = new OrbisFILE(); + if (file == nullptr) { + return nullptr; + } + // Store new file in the array, initialize default values, and return it. + g_files[index] = file; + file->_Mode = 0x80; + file->_Idx = index; + return file; + } + // Special case, files with mode 0 are returned? + if (file->_Mode == 0) { + file->_Mode = 0xff7f; + return file; + } + index++; + } + return nullptr; +} + +void PS4_SYSV_ABI internal__Lockfilelock(OrbisFILE* file) { + if (file != nullptr && file->_Mutex != nullptr) { + internal__Mtxlock(&file->_Mutex); + } +} + +void PS4_SYSV_ABI internal__Unlockfilelock(OrbisFILE* file) { + if (file != nullptr && file->_Mutex != nullptr) { + internal__Mtxunlock(&file->_Mutex); + } +} + +OrbisFILE* PS4_SYSV_ABI internal__Foprep(const char* path, const char* mode, OrbisFILE* file, + s32 fd, s32 s_mode, s32 flag) { + if (file == nullptr) { + *Kernel::__Error() = POSIX_ENOMEM; + } + + // Preserve mode and index + Libraries::Kernel::PthreadMutexT mtx = file->_Mutex; + Libraries::Kernel::PthreadMutexT* mtx_ptr = &file->_Mutex; + u8 file_index = file->_Idx; + u16 file_mode = file->_Mode & 0x80; + + // Real library does a memcpy using a static global FILE object. + // This stored file is just zeros, with the only exception being a handle of -1. + memset(file, 0, sizeof(OrbisFILE)); + file->_Handle = -1; + + // Not sure what this magic is for, but I'll replicate it. + u8* ptr = &file->_Cbuf; + // Note: this field is supposed to be a pthread mutex. + // Since we don't export pthread HLEs for other functions, I'll avoid handling this for now. + file->_Mutex = nullptr; + file->_Idx = file_index; + file->_Buf = ptr; + file->_Bend = &file->unk2; + file->_Next = ptr; + file->_Rend = ptr; + file->_WRend = ptr; + file->_Wend = ptr; + file->_WWend = ptr; + file->_Rback = ptr; + file->_WRback = &file->unk1; + + // Parse inputted mode string + const char* mode_str = mode; + u16 calc_mode = 0; + u16 access_mode = 0; + if (mode_str[0] == 'r') { + calc_mode = 1 | file_mode; + } else if (mode_str[0] == 'w') { + calc_mode = 0x1a | file_mode; + } else if (mode_str[0] == 'a') { + calc_mode = 0x16 | file_mode; + } else { + // Closes the file and returns EINVAL. + file->_Mode = file_mode; + if (flag == 0) { + internal__Mtxinit(mtx_ptr, nullptr); + } else { + file->_Mutex = mtx; + internal__Unlockfilelock(file); + } + internal_fclose(file); + *Kernel::__Error() = POSIX_EINVAL; + return nullptr; + } + file->_Mode = calc_mode; + + do { + // This is all basically straight from decomp, need to cleanup at some point. + if (mode_str[1] == '+') { + file_mode = 3; + if ((~calc_mode & 3) == 0) { + break; + } + } else if (mode_str[1] != 'b') { + file_mode = 0x20; + if ((calc_mode & 0x20) != 0) { + break; + } + } + mode_str++; + calc_mode = file_mode | calc_mode; + file->_Mode = calc_mode; + } while (true); + + if (path == nullptr && fd >= 0) { + // I guess this is for some internal behavior? + file->_Handle = fd; + } else { + fd = internal__Fopen(path, calc_mode, s_mode == 0x55); + file->_Handle = fd; + } + + // Error case + if (fd < 0) { + // Closes the file, but ensures errno is unchanged. + if (flag == 0) { + internal__Mtxinit(mtx_ptr, nullptr); + } else { + file->_Mutex = mtx; + internal__Unlockfilelock(file); + } + s32 old_errno = *Kernel::__Error(); + internal_fclose(file); + *Kernel::__Error() = old_errno; + return nullptr; + } + + if (flag == 0) { + char mtx_name[0x20]; + std::snprintf(mtx_name, 0x20, "FileFD:0x%08X", fd); + internal__Mtxinit(mtx_ptr, mtx_name); + } else { + file->_Mutex = mtx; + } + return file; +} + +s32 PS4_SYSV_ABI internal__Fopen(const char* path, u16 mode, bool flag) { + u32 large_mode = mode; + u16 open_mode = 0600; + if (!flag) { + open_mode = 0666; + } + // Straight from decomp, should probably get cleaned up at some point. + s32 creat_flag = large_mode << 5 & 0x200; + s32 excl_flag = large_mode << 5 & 0x800; + s32 misc_flags = (large_mode & 8) * 0x80 + (large_mode & 4) * 2; + // Real library has an array for this, where large_mode & 3 is used as an index. + // That array has values [0, 0, 1, 2], so this call should match the result. + s32 access_flag = std::max((large_mode & 3) - 1, 0); + s32 open_flags = creat_flag | misc_flags | excl_flag | access_flag; + return Libraries::Kernel::posix_open(path, open_flags, open_mode); +} + +OrbisFILE* PS4_SYSV_ABI internal_fopen(const char* path, const char* mode) { + std::scoped_lock lk{g_file_mtx}; + LOG_INFO(Lib_LibcInternal, "called, path {}, mode {}", path, mode); + OrbisFILE* file = internal__Fofind(); + return internal__Foprep(path, mode, file, -1, 0, 0); +} + +s32 PS4_SYSV_ABI internal_fflush(OrbisFILE* file) { + if (file == nullptr) { + std::scoped_lock lk{g_file_mtx}; + s32 fflush_result = 0; + for (auto& file : g_files) { + s32 res = internal_fflush(file.second); + if (res < 0) { + fflush_result = -1; + } + } + return fflush_result; + } + if ((file->_Mode & 0x2000) != 0) { + internal__Lockfilelock(file); + u16 file_mode = file->_Mode; + u8* file_buf_start = file->_Buf; + u8* file_buf_end = file->_Next; + while (file_buf_start < file_buf_end) { + u64 size_to_write = static_cast(file_buf_end - file_buf_start); + s32 write_bytes = + Libraries::Kernel::sceKernelWrite(file->_Handle, file_buf_start, size_to_write); + if (write_bytes < 1) { + file_buf_start = file->_Buf; + file->_Next = file_buf_start; + file->_Wend = file_buf_start; + file->_WWend = file_buf_start; + u8* off_mode = reinterpret_cast(&file->_Mode) + 1; + *off_mode = *off_mode | 2; + internal__Unlockfilelock(file); + return -1; + } + file_buf_end = file->_Next; + file_buf_start += write_bytes; + } + file->_Next = file_buf_start; + file->_Wend = file_buf_start; + file->_WWend = file_buf_start; + file->_Mode = file_mode & 0xdfff; + internal__Unlockfilelock(file); + } + return 0; +} + +s64 PS4_SYSV_ABI internal__Nnl(OrbisFILE* file, u8* val1, u8* val2) { + if (val1 < val2) { + return val2 - val1; + } + return 0; +} + +s32 PS4_SYSV_ABI internal__Fspos(OrbisFILE* file, Orbisfpos_t* file_pos, s64 offset, s32 whence) { + if ((file->_Mode & 3) == 0) { + return -1; + } + if (internal_fflush(file) != 0) { + return -1; + } + if (whence >= 3) { + *Libraries::Kernel::__Error() = POSIX_EINVAL; + return -1; + } + if (file_pos != nullptr) { + offset = offset + file_pos->_Off; + } + if (whence == 1 && (file->_Mode & 0x1000) != 0) { + s64 val1 = internal__Nnl(file, file->_Rback, &file->_Cbuf); + u8* rsave_ptr = file->_Rsave; + if (rsave_ptr == nullptr) { + rsave_ptr = file->_Rend; + } + s64 val2 = internal__Nnl(file, file->_Next, rsave_ptr); + s64 val3 = internal__Nnl(file, file->_Next, file->_WRend); + offset = offset - (val1 + val2 + val3); + } + s64 result = 0; + if (whence == 2 || (whence == 1 && offset != 0) || (whence == 0 && offset != -1)) { + result = Libraries::Kernel::posix_lseek(file->_Handle, offset, whence); + } + if (result == -1) { + return -1; + } + + u16 file_mode = file->_Mode; + if ((file_mode & 0x3000) != 0) { + u8* file_buf = file->_Buf; + file->_Next = file_buf; + file->_Rend = file_buf; + file->_WRend = file_buf; + file->_Wend = file_buf; + file->_WWend = file_buf; + file->_Rback = &file->_Cbuf; + file->_WRback = &file->unk1; + file->_Rsave = nullptr; + } + if (file_pos != nullptr) { + std::memcpy(&file->_Wstate, &file_pos->_Wstate, sizeof(Orbis_Mbstatet)); + } + file->_Mode = file_mode & 0xceff; + return 0; +} + +s32 PS4_SYSV_ABI internal_fseek(OrbisFILE* file, s64 offset, s32 whence) { + internal__Lockfilelock(file); + LOG_TRACE(Lib_LibcInternal, "called, file handle {:#x}, offset {:#x}, whence {:#x}", + file->_Handle, offset, whence); + s32 result = internal__Fspos(file, nullptr, offset, whence); + internal__Unlockfilelock(file); + return result; +} + +s32 PS4_SYSV_ABI internal__Frprep(OrbisFILE* file) { + if (file->_Rend > file->_Next) { + return 1; + } + if ((file->_Mode & 0x100) != 0) { + return 0; + } + u16 mode = file->_Mode; + if ((mode & 0xa001) != 1) { + // Lot of magic here, might be valuable to figure out what this does. + file->_Mode = (((mode ^ 0x8000) >> 0xf) << 0xe) | mode | 0x200; + return -1; + } + + u8* file_buf = file->_Buf; + if ((mode & 0x800) == 0 && file_buf == &file->_Cbuf) { + // Allocate a new file buffer, for now, we'll use host malloc to create it. + // When we have an HLE for malloc, that should be used instead. + u8* new_buffer = std::bit_cast(std::malloc(0x10000)); + if (new_buffer == nullptr) { + file->_Buf = file_buf; + file->_Bend = file_buf + 1; + } else { + file->_Mode = file->_Mode | 0x40; + file->_Buf = new_buffer; + file->_Bend = new_buffer + 0x10000; + file->_WRend = new_buffer; + file->_WWend = new_buffer; + file_buf = new_buffer; + } + } + file->_Next = file_buf; + file->_Rend = file_buf; + file->_Wend = file_buf; + // Intentional shrinking here, library treats value as 32-bit. + s32 read_result = + Libraries::Kernel::sceKernelRead(file->_Handle, file_buf, file->_Bend - file_buf); + if (read_result < 0) { + u8* off_mode = reinterpret_cast(&file->_Mode) + 1; + *off_mode = *off_mode | 0x42; + return -1; + } else if (read_result != 0) { + file->_Mode = file->_Mode | 0x5000; + file->_Rend = file->_Rend + read_result; + return 1; + } + file->_Mode = (file->_Mode & 0xaeff) | 0x4100; + return 0; +} + +u64 PS4_SYSV_ABI internal_fread(char* ptr, u64 size, u64 nmemb, OrbisFILE* file) { + if (size == 0 || nmemb == 0) { + return 0; + } + internal__Lockfilelock(file); + LOG_TRACE(Lib_LibcInternal, "called, file handle {:#x}, size {:#x}, nmemb {:#x}", file->_Handle, + size, nmemb); + s64 total_size = size * nmemb; + s64 remaining_size = total_size; + if ((file->_Mode & 0x4000) != 0) { + while (remaining_size != 0) { + u8* rback_ptr = file->_Rback; + if (&file->_Cbuf <= rback_ptr) { + break; + } + file->_Rback = rback_ptr + 1; + *ptr = *rback_ptr; + ptr++; + remaining_size--; + } + } + + while (remaining_size != 0) { + u8* file_ptr = file->_Rsave; + if (file_ptr == nullptr) { + file_ptr = file->_Rend; + } else { + file->_Rend = file_ptr; + file->_Rsave = nullptr; + } + u8* src = file->_Next; + if (file_ptr <= src) { + s32 res = internal__Frprep(file); + if (res < 1) { + internal__Unlockfilelock(file); + return (total_size - remaining_size) / size; + } + src = file->_Next; + file_ptr = file->_Rend; + } + u64 copy_bytes = std::min(file_ptr - src, remaining_size); + std::memcpy(ptr, src, copy_bytes); + file->_Next += copy_bytes; + ptr += copy_bytes; + remaining_size -= copy_bytes; + } + internal__Unlockfilelock(file); + return (total_size - remaining_size) / size; +} + +void PS4_SYSV_ABI internal__Fofree(OrbisFILE* file) { + u8* cbuf_ptr = &file->_Cbuf; + s8 trunc_mode = static_cast(file->_Mode); + + file->_Mode = 0; + file->_Handle = -1; + file->_Buf = cbuf_ptr; + file->_Next = cbuf_ptr; + file->_Rend = cbuf_ptr; + file->_WRend = cbuf_ptr; + file->_Wend = cbuf_ptr; + file->_WWend = cbuf_ptr; + file->_Rback = cbuf_ptr; + file->_WRback = &file->unk1; + if (trunc_mode < 0) { + // Remove file from vector + g_files.erase(file->_Idx); + internal__Mtxdst(&file->_Mutex); + free(file); + } +} + +s32 PS4_SYSV_ABI internal_fclose(OrbisFILE* file) { + if (file == nullptr) { + return -1; + } + + LOG_INFO(Lib_LibcInternal, "called, file handle {:#x}", file->_Handle); + if ((file->_Mode & 3) == 0 || file->_Handle < 0) { + std::scoped_lock lk{g_file_mtx}; + internal__Fofree(file); + *Libraries::Kernel::__Error() = POSIX_EBADF; + } else { + s32 fflush_result = internal_fflush(file); + std::scoped_lock lk{g_file_mtx}; + if ((file->_Mode & 0x40) != 0) { + std::free(file->_Buf); + } + file->_Buf = nullptr; + s32 close_result = Libraries::Kernel::posix_close(file->_Handle); + internal__Fofree(file); + // Need to figure out what exactly this means. + return ~-(close_result == 0) | fflush_result; + } + return 0; +} + void RegisterlibSceLibcInternalIo(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("eLdDw6l0-bU", "libSceLibcInternal", 1, "libSceLibcInternal", internal_snprintf); + LIB_FUNCTION("MUjC4lbHrK4", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fflush); + LIB_FUNCTION("xGT4Mc55ViQ", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Fofind); + LIB_FUNCTION("dREVnZkAKRE", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Foprep); + LIB_FUNCTION("sQL8D-jio7U", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Fopen); + LIB_FUNCTION("A+Y3xfrWLLo", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Fspos); + LIB_FUNCTION("Ss3108pBuZY", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Nnl); + LIB_FUNCTION("9s3P+LCvWP8", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Frprep); + LIB_FUNCTION("jVDuvE3s5Bs", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Fofree); + LIB_FUNCTION("vZkmJmvqueY", "libSceLibcInternal", 1, "libSceLibcInternal", + internal__Lockfilelock); + LIB_FUNCTION("0x7rx8TKy2Y", "libSceLibcInternal", 1, "libSceLibcInternal", + internal__Unlockfilelock); } + +void ForceRegisterlibSceLibcInternalIo(Core::Loader::SymbolsResolver* sym) { + // Goal is to be minimally intrusive here to allow LLE for printf/stdout writes. + LIB_FUNCTION("xeYO4u7uyJ0", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fopen); + LIB_FUNCTION("rQFVBXp-Cxg", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fseek); + LIB_FUNCTION("lbB+UlZqVG0", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fread); + LIB_FUNCTION("uodLYyUip20", "libSceLibcInternal", 1, "libSceLibcInternal", internal_fclose); +} + } // namespace Libraries::LibcInternal \ No newline at end of file diff --git a/src/core/libraries/libc_internal/libc_internal_io.h b/src/core/libraries/libc_internal/libc_internal_io.h index f5291526b..27915d8fa 100644 --- a/src/core/libraries/libc_internal/libc_internal_io.h +++ b/src/core/libraries/libc_internal/libc_internal_io.h @@ -3,12 +3,96 @@ #pragma once +#include #include "common/types.h" +#include "core/libraries/kernel/threads.h" namespace Core::Loader { class SymbolsResolver; } namespace Libraries::LibcInternal { + +static std::recursive_mutex g_file_mtx{}; + +union Orbis__mbstate_t { + u8 __mbstate8[128]; + s64 _mbstateL; +}; + +struct Orbis_Mbstatet { + u64 _Wchar; + u16 _Byte, _State; + s32 : 32; +}; + +struct Orbisfpos_t { + s64 _Off; + Orbis_Mbstatet _Wstate; +}; + +struct Orbis__sbuf { + u8* _base; + s32 _size; +}; + +struct OrbisFILE { + u16 _Mode; + u8 _Idx; + s32 _Handle; + u8 *_Buf, *_Bend, *_Next; + u8 *_Rend, *_Wend, *_Rback; + u16 *_WRback, _WBack[2]; + u16 unk1; + u8 *_Rsave, *_WRend, *_WWend; + Orbis_Mbstatet _Wstate; + u8* _Tmpnam; + u8 _Back[6], _Cbuf; + u8 unk2; + Libraries::Kernel::PthreadMutexT _Mutex; + u8* _p; + s32 _r; + s32 _w; + s16 _flags; + s16 _file; + Orbis__sbuf _bf; + s32 _lbfsize; + void* _cookie; + s32 PS4_SYSV_ABI (*_close)(void*); + s32 PS4_SYSV_ABI (*_read)(void*, char*, s32); + Orbisfpos_t PS4_SYSV_ABI (*_seek)(void*, Orbisfpos_t, s32); + s32 (*_write)(void*, const char*, s32); + Orbis__sbuf _ub; + u8* _up; + s32 _ur; + u8 _ubuf[3]; + u8 _nbuf[1]; + Orbis__sbuf _lb; + s32 _blksize; + Orbisfpos_t _offset; + void* _fl_mutex; + void* _fl_owner; + s32 _fl_count; + s32 _orientation; + Orbis__mbstate_t _mbstate; +}; + +s32 PS4_SYSV_ABI internal_snprintf(char* s, u64 n, VA_ARGS); +void PS4_SYSV_ABI internal__Lockfilelock(OrbisFILE* file); +void PS4_SYSV_ABI internal__Unlockfilelock(OrbisFILE* file); +OrbisFILE* PS4_SYSV_ABI internal__Fofind(); +OrbisFILE* PS4_SYSV_ABI internal__Foprep(const char* path, const char* mode, OrbisFILE* file, + s32 fd, s32 flag1, s32 flag2); +s32 PS4_SYSV_ABI internal__Fopen(const char* path, u16 mode, bool flag); +OrbisFILE* PS4_SYSV_ABI internal_fopen(const char* path, const char* mode); +s64 PS4_SYSV_ABI internal__Nnl(OrbisFILE* file, u8* val1, u8* val2); +s32 PS4_SYSV_ABI internal__Fspos(OrbisFILE* file, Orbisfpos_t* file_pos, s64 offset, s32 whence); +s32 PS4_SYSV_ABI internal_fflush(OrbisFILE* file); +s32 PS4_SYSV_ABI internal_fseek(OrbisFILE* file, s64 offset, s32 whence); +s32 PS4_SYSV_ABI internal__Frprep(OrbisFILE* file); +u64 PS4_SYSV_ABI internal_fread(char* ptr, u64 size, u64 nmemb, OrbisFILE* file); +s32 PS4_SYSV_ABI internal_fclose(OrbisFILE* file); + void RegisterlibSceLibcInternalIo(Core::Loader::SymbolsResolver* sym); +void ForceRegisterlibSceLibcInternalIo(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::LibcInternal \ No newline at end of file diff --git a/src/core/libraries/libc_internal/libc_internal_threads.cpp b/src/core/libraries/libc_internal/libc_internal_threads.cpp new file mode 100644 index 000000000..2d8ddaccb --- /dev/null +++ b/src/core/libraries/libc_internal/libc_internal_threads.cpp @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/libraries/kernel/threads.h" +#include "core/libraries/libc_internal/libc_internal_threads.h" +#include "core/libraries/libs.h" + +namespace Libraries::LibcInternal { + +void getMutexName(char* buf, u64 size, const char* name) { + if (name != nullptr) { + std::snprintf(buf, size, "SceLibcI_%s", name); + } else { + std::snprintf(buf, size, "SceLibcI"); + } +} + +s32 PS4_SYSV_ABI internal__Mtxinit(Libraries::Kernel::PthreadMutexT* mtx, const char* name) { + char mtx_name[0x20]; + getMutexName(mtx_name, sizeof(mtx_name), name); + + Libraries::Kernel::PthreadMutexAttrT attr{}; + s32 result = Libraries::Kernel::posix_pthread_mutexattr_init(&attr); + if (result != 0) { + return 1; + } + + result = Libraries::Kernel::posix_pthread_mutexattr_settype( + &attr, Libraries::Kernel::PthreadMutexType::Recursive); + if (result == 0) { + s32 mtx_init_result = Libraries::Kernel::scePthreadMutexInit(mtx, &attr, mtx_name); + result = Libraries::Kernel::posix_pthread_mutexattr_destroy(&attr); + if (mtx_init_result == 0 && result == 0) { + return 0; + } + } else { + Libraries::Kernel::posix_pthread_mutexattr_destroy(&attr); + } + + return 1; +} + +s32 PS4_SYSV_ABI internal__Mtxlock(Libraries::Kernel::PthreadMutexT* mtx) { + s32 result = Libraries::Kernel::posix_pthread_mutex_lock(mtx); + return result != 0; +} + +s32 PS4_SYSV_ABI internal__Mtxunlock(Libraries::Kernel::PthreadMutexT* mtx) { + s32 result = Libraries::Kernel::posix_pthread_mutex_unlock(mtx); + return result != 0; +} + +s32 PS4_SYSV_ABI internal__Mtxdst(Libraries::Kernel::PthreadMutexT* mtx) { + s32 result = Libraries::Kernel::posix_pthread_mutex_destroy(mtx); + return result != 0; +} + +void RegisterlibSceLibcInternalThreads(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("z7STeF6abuU", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Mtxinit); + LIB_FUNCTION("pE4Ot3CffW0", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Mtxlock); + LIB_FUNCTION("cMwgSSmpE5o", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Mtxunlock); + LIB_FUNCTION("LaPaA6mYA38", "libSceLibcInternal", 1, "libSceLibcInternal", internal__Mtxdst); +} + +} // namespace Libraries::LibcInternal \ No newline at end of file diff --git a/src/core/libraries/libc_internal/libc_internal_threads.h b/src/core/libraries/libc_internal/libc_internal_threads.h new file mode 100644 index 000000000..74e6d41b1 --- /dev/null +++ b/src/core/libraries/libc_internal/libc_internal_threads.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "common/types.h" +#include "core/libraries/kernel/threads.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::LibcInternal { + +s32 PS4_SYSV_ABI internal__Mtxinit(Libraries::Kernel::PthreadMutexT* mtx, const char* name); +s32 PS4_SYSV_ABI internal__Mtxlock(Libraries::Kernel::PthreadMutexT* mtx); +s32 PS4_SYSV_ABI internal__Mtxunlock(Libraries::Kernel::PthreadMutexT* mtx); +s32 PS4_SYSV_ABI internal__Mtxdst(Libraries::Kernel::PthreadMutexT* mtx); + +void RegisterlibSceLibcInternalThreads(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::LibcInternal \ No newline at end of file diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 67c3d4b7d..eebb991dc 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -80,6 +80,7 @@ namespace Libraries { void InitHLELibs(Core::Loader::SymbolsResolver* sym) { LOG_INFO(Lib_Kernel, "Initializing HLE libraries"); Libraries::Kernel::RegisterLib(sym); + Libraries::LibcInternal::ForceRegisterLib(sym); Libraries::GnmDriver::RegisterLib(sym); Libraries::VideoOut::RegisterLib(sym); Libraries::UserService::RegisterLib(sym); From 68679b24aae061f8ef5747afd8d03ead5ac61a55 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:31:23 -0600 Subject: [PATCH 47/50] Fix log errors (#4020) 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.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/np/np_web_api.cpp b/src/core/libraries/np/np_web_api.cpp index e51b79a3c..0c633e0d1 100644 --- a/src/core/libraries/np/np_web_api.cpp +++ b/src/core/libraries/np/np_web_api.cpp @@ -54,12 +54,12 @@ s32 PS4_SYSV_ABI sceNpWebApiCreateServicePushEventFilter( } s32 PS4_SYSV_ABI sceNpWebApiDeletePushEventFilter(s32 libCtxId, s32 filterId) { - LOG_INFO(Lib_NpWebApi, "called, libCtxId = {:#x}, filterId = {:#x}"); + LOG_INFO(Lib_NpWebApi, "called, libCtxId = {:#x}, filterId = {:#x}", libCtxId, filterId); return deletePushEventFilter(libCtxId, filterId); } s32 PS4_SYSV_ABI sceNpWebApiDeleteServicePushEventFilter(s32 libCtxId, s32 filterId) { - LOG_INFO(Lib_NpWebApi, "called, libCtxId = {:#x}, filterId = {:#x}"); + LOG_INFO(Lib_NpWebApi, "called, libCtxId = {:#x}, filterId = {:#x}", libCtxId, filterId); return deleteServicePushEventFilter(libCtxId, filterId); } From 8c59571961df10f5e05805ad6aa4e9b47b7db68f Mon Sep 17 00:00:00 2001 From: Vladislav Mikhalin Date: Wed, 11 Feb 2026 12:15:33 +0300 Subject: [PATCH 48/50] pad: refactoring (#4016) * pad: refactoring * clang is not my friend --- src/core/libraries/pad/pad.cpp | 154 +++++++-------------------------- src/input/controller.cpp | 144 +++++++----------------------- src/input/controller.h | 59 +++++++++---- src/input/input_handler.cpp | 12 +-- src/input/input_mouse.cpp | 6 +- src/sdl_window.cpp | 6 +- 6 files changed, 116 insertions(+), 265 deletions(-) diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index f433a87cc..5f50b8a7d 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/config.h" @@ -94,18 +94,6 @@ int PS4_SYSV_ABI scePadGetCapability() { int PS4_SYSV_ABI scePadGetControllerInformation(s32 handle, OrbisPadControllerInformation* pInfo) { LOG_DEBUG(Lib_Pad, "called handle = {}", handle); - if (handle < 0) { - pInfo->touchPadInfo.pixelDensity = 1; - pInfo->touchPadInfo.resolution.x = 1920; - pInfo->touchPadInfo.resolution.y = 950; - pInfo->stickInfo.deadZoneLeft = 1; - pInfo->stickInfo.deadZoneRight = 1; - pInfo->connectionType = ORBIS_PAD_PORT_TYPE_STANDARD; - pInfo->connectedCount = 1; - pInfo->connected = false; - pInfo->deviceClass = OrbisPadDeviceClass::Standard; - return ORBIS_OK; - } pInfo->touchPadInfo.pixelDensity = 1; pInfo->touchPadInfo.resolution.x = 1920; pInfo->touchPadInfo.resolution.y = 950; @@ -113,8 +101,12 @@ int PS4_SYSV_ABI scePadGetControllerInformation(s32 handle, OrbisPadControllerIn pInfo->stickInfo.deadZoneRight = 1; pInfo->connectionType = ORBIS_PAD_PORT_TYPE_STANDARD; pInfo->connectedCount = 1; - pInfo->connected = true; pInfo->deviceClass = OrbisPadDeviceClass::Standard; + if (handle < 0) { + pInfo->connected = false; + return ORBIS_OK; + } + pInfo->connected = true; if (Config::getUseSpecialPad()) { pInfo->connectionType = ORBIS_PAD_PORT_TYPE_SPECIAL; pInfo->deviceClass = (OrbisPadDeviceClass)Config::getSpecialPadClass(); @@ -302,20 +294,16 @@ int PS4_SYSV_ABI scePadOutputReport() { return ORBIS_OK; } -int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num) { - LOG_TRACE(Lib_Pad, "called"); - int connected_count = 0; - bool connected = false; - Input::State states[64]; - auto* controller = Common::Singleton::Instance(); - const auto* engine = controller->GetEngine(); - int ret_num = controller->ReadStates(states, num, &connected, &connected_count); - +int ProcessStates(s32 handle, OrbisPadData* pData, Input::State* states, s32 num, bool connected, + u32 connected_count) { if (!connected) { - ret_num = 1; + pData[0] = {}; + pData[0].orientation = {0.0f, 0.0f, 0.0f, 1.0f}; + pData[0].connected = false; + return 1; } - for (int i = 0; i < ret_num; i++) { + for (int i = 0; i < num; i++) { pData[i].buttons = states[i].buttonsState; pData[i].leftStick.x = states[i].axes[static_cast(Input::Axis::LeftX)]; pData[i].leftStick.y = states[i].axes[static_cast(Input::Axis::LeftY)]; @@ -323,20 +311,16 @@ int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num) { pData[i].rightStick.y = states[i].axes[static_cast(Input::Axis::RightY)]; pData[i].analogButtons.l2 = states[i].axes[static_cast(Input::Axis::TriggerLeft)]; pData[i].analogButtons.r2 = states[i].axes[static_cast(Input::Axis::TriggerRight)]; - pData[i].acceleration.x = states[i].acceleration.x; - pData[i].acceleration.y = states[i].acceleration.y; - pData[i].acceleration.z = states[i].acceleration.z; - pData[i].angularVelocity.x = states[i].angularVelocity.x; - pData[i].angularVelocity.y = states[i].angularVelocity.y; - pData[i].angularVelocity.z = states[i].angularVelocity.z; - pData[i].orientation = {0.0f, 0.0f, 0.0f, 1.0f}; pData[i].acceleration.x = states[i].acceleration.x * 0.098; pData[i].acceleration.y = states[i].acceleration.y * 0.098; pData[i].acceleration.z = states[i].acceleration.z * 0.098; pData[i].angularVelocity.x = states[i].angularVelocity.x; pData[i].angularVelocity.y = states[i].angularVelocity.y; pData[i].angularVelocity.z = states[i].angularVelocity.z; + pData[i].orientation = {0.0f, 0.0f, 0.0f, 1.0f}; + auto* controller = Common::Singleton::Instance(); + const auto* engine = controller->GetEngine(); if (engine && handle == 1) { const auto gyro_poll_rate = engine->GetAccelPollRate(); if (gyro_poll_rate != 0.0f) { @@ -354,7 +338,6 @@ int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num) { controller->SetLastOrientation(outputOrientation); } } - pData[i].touchData.touchNum = (states[i].touchpad[0].state ? 1 : 0) + (states[i].touchpad[1].state ? 1 : 0); @@ -409,7 +392,18 @@ int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num) { pData[i].deviceUniqueDataLen = 0; } - return ret_num; + return num; +} + +int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num) { + LOG_TRACE(Lib_Pad, "called"); + int connected_count = 0; + bool connected = false; + std::vector states(64); + auto* controller = Common::Singleton::Instance(); + const auto* engine = controller->GetEngine(); + int ret_num = controller->ReadStates(states.data(), num, &connected, &connected_count); + return ProcessStates(handle, pData, states.data(), ret_num, connected, connected_count); } int PS4_SYSV_ABI scePadReadBlasterForTracker() { @@ -439,95 +433,11 @@ int PS4_SYSV_ABI scePadReadState(s32 handle, OrbisPadData* pData) { } auto* controller = Common::Singleton::Instance(); const auto* engine = controller->GetEngine(); - int connectedCount = 0; - bool isConnected = false; + int connected_count = 0; + bool connected = false; Input::State state; - controller->ReadState(&state, &isConnected, &connectedCount); - pData->buttons = state.buttonsState; - pData->leftStick.x = state.axes[static_cast(Input::Axis::LeftX)]; - pData->leftStick.y = state.axes[static_cast(Input::Axis::LeftY)]; - pData->rightStick.x = state.axes[static_cast(Input::Axis::RightX)]; - pData->rightStick.x = state.axes[static_cast(Input::Axis::RightX)]; - pData->rightStick.y = state.axes[static_cast(Input::Axis::RightY)]; - pData->analogButtons.l2 = state.axes[static_cast(Input::Axis::TriggerLeft)]; - pData->analogButtons.r2 = state.axes[static_cast(Input::Axis::TriggerRight)]; - pData->acceleration.x = state.acceleration.x * 0.098; - pData->acceleration.y = state.acceleration.y * 0.098; - pData->acceleration.z = state.acceleration.z * 0.098; - pData->angularVelocity.x = state.angularVelocity.x; - pData->angularVelocity.y = state.angularVelocity.y; - pData->angularVelocity.z = state.angularVelocity.z; - pData->orientation = {0.0f, 0.0f, 0.0f, 1.0f}; - - // Only do this on handle 1 for now - if (engine && handle == 1) { - auto now = std::chrono::steady_clock::now(); - float deltaTime = - std::chrono::duration_cast(now - controller->GetLastUpdate()) - .count() / - 1000000.0f; - controller->SetLastUpdate(now); - Libraries::Pad::OrbisFQuaternion lastOrientation = controller->GetLastOrientation(); - Libraries::Pad::OrbisFQuaternion outputOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; - GameController::CalculateOrientation(pData->acceleration, pData->angularVelocity, deltaTime, - lastOrientation, outputOrientation); - pData->orientation = outputOrientation; - controller->SetLastOrientation(outputOrientation); - } - pData->touchData.touchNum = - (state.touchpad[0].state ? 1 : 0) + (state.touchpad[1].state ? 1 : 0); - - // Only do this on handle 1 for now - if (handle == 1) { - if (controller->GetTouchCount() >= 127) { - controller->SetTouchCount(0); - } - - if (controller->GetSecondaryTouchCount() >= 127) { - controller->SetSecondaryTouchCount(0); - } - - if (pData->touchData.touchNum == 1 && controller->GetPreviousTouchNum() == 0) { - controller->SetTouchCount(controller->GetTouchCount() + 1); - controller->SetSecondaryTouchCount(controller->GetTouchCount()); - } else if (pData->touchData.touchNum == 2 && controller->GetPreviousTouchNum() == 1) { - controller->SetSecondaryTouchCount(controller->GetSecondaryTouchCount() + 1); - } else if (pData->touchData.touchNum == 0 && controller->GetPreviousTouchNum() > 0) { - if (controller->GetTouchCount() < controller->GetSecondaryTouchCount()) { - controller->SetTouchCount(controller->GetSecondaryTouchCount()); - } else { - if (controller->WasSecondaryTouchReset()) { - controller->SetTouchCount(controller->GetSecondaryTouchCount()); - controller->UnsetSecondaryTouchResetBool(); - } - } - } - - controller->SetPreviousTouchNum(pData->touchData.touchNum); - - if (pData->touchData.touchNum == 1) { - state.touchpad[0].ID = controller->GetTouchCount(); - state.touchpad[1].ID = 0; - } else if (pData->touchData.touchNum == 2) { - state.touchpad[0].ID = controller->GetTouchCount(); - state.touchpad[1].ID = controller->GetSecondaryTouchCount(); - } - } else { - state.touchpad[0].ID = 1; - state.touchpad[1].ID = 2; - } - - pData->touchData.touch[0].x = state.touchpad[0].x; - pData->touchData.touch[0].y = state.touchpad[0].y; - pData->touchData.touch[0].id = state.touchpad[0].ID; - pData->touchData.touch[1].x = state.touchpad[1].x; - pData->touchData.touch[1].y = state.touchpad[1].y; - pData->touchData.touch[1].id = state.touchpad[1].ID; - pData->timestamp = state.time; - pData->connected = true; // isConnected; //TODO fix me proper - pData->connectedCount = 1; // connectedCount; - pData->deviceUniqueDataLen = 0; - + controller->ReadState(&state, &connected, &connected_count); + ProcessStates(handle, pData, &state, 1, connected, connected_count); return ORBIS_OK; } diff --git a/src/input/controller.cpp b/src/input/controller.cpp index 6657f4036..3606ad5d2 100644 --- a/src/input/controller.cpp +++ b/src/input/controller.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 @@ -61,110 +61,51 @@ void State::OnAccel(const float accel[3]) { acceleration.z = accel[2]; } -GameController::GameController() { - m_states_num = 0; - m_last_state = State(); -} +GameController::GameController() : m_states_queue(64) {} void GameController::ReadState(State* state, bool* isConnected, int* connectedCount) { - std::scoped_lock lock{m_mutex}; - *isConnected = m_connected; *connectedCount = m_connected_count; - *state = GetLastState(); + *state = m_state; } int GameController::ReadStates(State* states, int states_num, bool* isConnected, int* connectedCount) { - std::scoped_lock lock{m_mutex}; - *isConnected = m_connected; *connectedCount = m_connected_count; int ret_num = 0; - if (m_connected) { - if (m_states_num == 0) { - ret_num = 1; - states[0] = m_last_state; - } else { - for (uint32_t i = 0; i < m_states_num; i++) { - if (ret_num >= states_num) { - break; - } - auto index = (m_first_state + i) % MAX_STATES; - if (!m_private[index].obtained) { - m_private[index].obtained = true; - - states[ret_num++] = m_states[index]; - } + std::lock_guard lg(m_states_queue_mutex); + for (int i = 0; i < states_num; i++) { + auto o_state = m_states_queue.Pop(); + if (!o_state) { + break; } + states[ret_num++] = *o_state; } } - return ret_num; } -State GameController::GetLastState() const { - if (m_states_num == 0) { - return m_last_state; - } - const u32 last = (m_first_state + m_states_num - 1) % MAX_STATES; - return m_states[last]; -} - -void GameController::AddState(const State& state) { - if (m_states_num >= MAX_STATES) { - m_states_num = MAX_STATES - 1; - m_first_state = (m_first_state + 1) % MAX_STATES; - } - - const u32 index = (m_first_state + m_states_num) % MAX_STATES; - m_states[index] = state; - m_last_state = state; - m_private[index].obtained = false; - m_states_num++; -} - -void GameController::CheckButton(int id, OrbisPadButtonDataOffset button, bool is_pressed) { - std::scoped_lock lock{m_mutex}; - auto state = GetLastState(); - - state.time = Libraries::Kernel::sceKernelGetProcessTime(); - state.OnButton(button, is_pressed); - - AddState(state); +void GameController::Button(int id, OrbisPadButtonDataOffset button, bool is_pressed) { + m_state.OnButton(button, is_pressed); + PushState(); } void GameController::Axis(int id, Input::Axis axis, int value) { - std::scoped_lock lock{m_mutex}; - auto state = GetLastState(); - - state.time = Libraries::Kernel::sceKernelGetProcessTime(); - state.OnAxis(axis, value); - - AddState(state); + m_state.OnAxis(axis, value); + PushState(); } void GameController::Gyro(int id, const float gyro[3]) { - std::scoped_lock lock{m_mutex}; - auto state = GetLastState(); - state.time = Libraries::Kernel::sceKernelGetProcessTime(); - - // Update the angular velocity (gyro data) - state.OnGyro(gyro); - - AddState(state); + m_state.OnGyro(gyro); + PushState(); } + void GameController::Acceleration(int id, const float acceleration[3]) { - std::scoped_lock lock{m_mutex}; - auto state = GetLastState(); - state.time = Libraries::Kernel::sceKernelGetProcessTime(); - - // Update the acceleration values - state.OnAccel(acceleration); - - AddState(state); + m_state.OnAccel(acceleration); + PushState(); } void GameController::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, @@ -206,7 +147,6 @@ void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) { if (!m_engine) { return; } - std::scoped_lock _{m_mutex}; m_engine->SetLightBarRGB(r, g, b); } @@ -214,39 +154,29 @@ void GameController::SetVibration(u8 smallMotor, u8 largeMotor) { if (!m_engine) { return; } - std::scoped_lock _{m_mutex}; m_engine->SetVibration(smallMotor, largeMotor); } void GameController::SetTouchpadState(int touchIndex, bool touchDown, float x, float y) { if (touchIndex < 2) { - std::scoped_lock lock{m_mutex}; - auto state = GetLastState(); - - state.time = Libraries::Kernel::sceKernelGetProcessTime(); - state.OnTouchpad(touchIndex, touchDown, x, y); - - AddState(state); + m_state.OnTouchpad(touchIndex, touchDown, x, y); + PushState(); } } u8 GameController::GetTouchCount() { - std::scoped_lock lock{m_mutex}; return m_touch_count; } void GameController::SetTouchCount(u8 touchCount) { - std::scoped_lock lock{m_mutex}; m_touch_count = touchCount; } u8 GameController::GetSecondaryTouchCount() { - std::scoped_lock lock{m_mutex}; return m_secondary_touch_count; } void GameController::SetSecondaryTouchCount(u8 touchCount) { - std::scoped_lock lock{m_mutex}; m_secondary_touch_count = touchCount; if (touchCount == 0) { m_was_secondary_reset = true; @@ -254,47 +184,38 @@ void GameController::SetSecondaryTouchCount(u8 touchCount) { } u8 GameController::GetPreviousTouchNum() { - std::scoped_lock lock{m_mutex}; return m_previous_touchnum; } void GameController::SetPreviousTouchNum(u8 touchNum) { - std::scoped_lock lock{m_mutex}; m_previous_touchnum = touchNum; } bool GameController::WasSecondaryTouchReset() { - std::scoped_lock lock{m_mutex}; return m_was_secondary_reset; } void GameController::UnsetSecondaryTouchResetBool() { - std::scoped_lock lock{m_mutex}; m_was_secondary_reset = false; } void GameController::SetLastOrientation(Libraries::Pad::OrbisFQuaternion& orientation) { - std::scoped_lock lock{m_mutex}; m_orientation = orientation; } Libraries::Pad::OrbisFQuaternion GameController::GetLastOrientation() { - std::scoped_lock lock{m_mutex}; return m_orientation; } std::chrono::steady_clock::time_point GameController::GetLastUpdate() { - std::scoped_lock lock{m_mutex}; return m_last_update; } void GameController::SetLastUpdate(std::chrono::steady_clock::time_point lastUpdate) { - std::scoped_lock lock{m_mutex}; m_last_update = lastUpdate; } void GameController::SetEngine(std::unique_ptr engine) { - std::scoped_lock _{m_mutex}; m_engine = std::move(engine); if (m_engine) { m_engine->Init(); @@ -305,24 +226,17 @@ Engine* GameController::GetEngine() { return m_engine.get(); } +void GameController::PushState() { + std::lock_guard lg(m_states_queue_mutex); + m_state.time = Libraries::Kernel::sceKernelGetProcessTime(); + m_states_queue.Push(m_state); +} + u32 GameController::Poll() { if (m_connected) { - std::scoped_lock lock{m_mutex}; - auto time = Libraries::Kernel::sceKernelGetProcessTime(); - if (m_states_num == 0) { - auto diff = (time - m_last_state.time) / 1000; - if (diff >= 100) { - AddState(GetLastState()); - } - } else { - auto index = (m_first_state - 1 + m_states_num) % MAX_STATES; - auto diff = (time - m_states[index].time) / 1000; - if (m_private[index].obtained && diff >= 100) { - AddState(GetLastState()); - } - } + PushState(); } - return 100; + return 33; } } // namespace Input diff --git a/src/input/controller.h b/src/input/controller.h index dfde521be..6c13fdf99 100644 --- a/src/input/controller.h +++ b/src/input/controller.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 @@ -6,7 +6,10 @@ #include #include #include +#include + #include + #include "common/types.h" #include "core/libraries/pad/pad.h" @@ -63,7 +66,36 @@ inline int GetAxis(int min, int max, int value) { return std::clamp((255 * (value - min)) / (max - min), 0, 255); } -constexpr u32 MAX_STATES = 32; +template +class RingBufferQueue { +public: + RingBufferQueue(size_t size) : m_storage(size) {} + + void Push(T item) { + const size_t index = (m_begin + m_size) % m_storage.size(); + m_storage[index] = std::move(item); + if (m_size < m_storage.size()) { + m_size += 1; + } else { + m_begin = (m_begin + 1) % m_storage.size(); + } + } + + std::optional Pop() { + if (m_size == 0) { + return {}; + } + const size_t index = m_begin; + m_begin = (m_begin + 1) % m_storage.size(); + m_size -= 1; + return std::move(m_storage[index]); + } + +private: + size_t m_begin = 0; + size_t m_size = 0; + std::vector m_storage; +}; class GameController { public: @@ -72,9 +104,8 @@ public: void ReadState(State* state, bool* isConnected, int* connectedCount); int ReadStates(State* states, int states_num, bool* isConnected, int* connectedCount); - State GetLastState() const; - void CheckButton(int id, Libraries::Pad::OrbisPadButtonDataOffset button, bool isPressed); - void AddState(const State& state); + + void Button(int id, Libraries::Pad::OrbisPadButtonDataOffset button, bool isPressed); void Axis(int id, Input::Axis axis, int value); void Gyro(int id, const float gyro[3]); void Acceleration(int id, const float acceleration[3]); @@ -105,26 +136,22 @@ public: Libraries::Pad::OrbisFQuaternion& orientation); private: - struct StateInternal { - bool obtained = false; - }; + void PushState(); - std::mutex m_mutex; bool m_connected = true; - State m_last_state; - int m_connected_count = 0; - u32 m_states_num = 0; - u32 m_first_state = 0; + int m_connected_count = 1; u8 m_touch_count = 0; u8 m_secondary_touch_count = 0; - u8 m_previous_touch_count = 0; u8 m_previous_touchnum = 0; bool m_was_secondary_reset = false; - std::array m_states; - std::array m_private; std::chrono::steady_clock::time_point m_last_update = {}; Libraries::Pad::OrbisFQuaternion m_orientation = {0.0f, 0.0f, 0.0f, 1.0f}; + State m_state; + + std::mutex m_states_queue_mutex; + RingBufferQueue m_states_queue; + std::unique_ptr m_engine = nullptr; }; diff --git a/src/input/input_handler.cpp b/src/input/input_handler.cpp index 6e5014c1b..e44693fbf 100644 --- a/src/input/input_handler.cpp +++ b/src/input/input_handler.cpp @@ -557,15 +557,15 @@ void ControllerOutput::FinalizeUpdate() { switch (button) { case SDL_GAMEPAD_BUTTON_TOUCHPAD_LEFT: controller->SetTouchpadState(0, new_button_state, 0.25f, 0.5f); - controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); break; case SDL_GAMEPAD_BUTTON_TOUCHPAD_CENTER: controller->SetTouchpadState(0, new_button_state, 0.50f, 0.5f); - controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); break; case SDL_GAMEPAD_BUTTON_TOUCHPAD_RIGHT: controller->SetTouchpadState(0, new_button_state, 0.75f, 0.5f); - controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); break; case LEFTJOYSTICK_HALFMODE: leftjoystick_halfmode = new_button_state; @@ -617,7 +617,7 @@ void ControllerOutput::FinalizeUpdate() { SetMouseGyroRollMode(new_button_state); break; default: // is a normal key (hopefully) - controller->CheckButton(0, SDLGamepadToOrbisButton(button), new_button_state); + controller->Button(0, SDLGamepadToOrbisButton(button), new_button_state); break; } } else if (axis != SDL_GAMEPAD_AXIS_INVALID && positive_axis) { @@ -648,12 +648,12 @@ void ControllerOutput::FinalizeUpdate() { case Axis::TriggerLeft: ApplyDeadzone(new_param, lefttrigger_deadzone); controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param)); - controller->CheckButton(0, OrbisPadButtonDataOffset::L2, *new_param > 0x20); + controller->Button(0, OrbisPadButtonDataOffset::L2, *new_param > 0x20); return; case Axis::TriggerRight: ApplyDeadzone(new_param, righttrigger_deadzone); controller->Axis(0, c_axis, GetAxis(0x0, 0x7f, *new_param)); - controller->CheckButton(0, OrbisPadButtonDataOffset::R2, *new_param > 0x20); + controller->Button(0, OrbisPadButtonDataOffset::R2, *new_param > 0x20); return; default: break; diff --git a/src/input/input_mouse.cpp b/src/input/input_mouse.cpp index 996c35ef9..0dc44608b 100644 --- a/src/input/input_mouse.cpp +++ b/src/input/input_mouse.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 @@ -104,8 +104,8 @@ void EmulateTouchpad(GameController* controller, u32 interval) { controller->SetTouchpadState(0, (mouse_buttons & SDL_BUTTON_LMASK) != 0, std::clamp(x / g_window->GetWidth(), 0.0f, 1.0f), std::clamp(y / g_window->GetHeight(), 0.0f, 1.0f)); - controller->CheckButton(0, Libraries::Pad::OrbisPadButtonDataOffset::TouchPad, - (mouse_buttons & SDL_BUTTON_RMASK) != 0); + controller->Button(0, Libraries::Pad::OrbisPadButtonDataOffset::TouchPad, + (mouse_buttons & SDL_BUTTON_RMASK) != 0); } void ApplyMouseInputBlockers() { diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index 476a56b52..c9183f301 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.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 "SDL3/SDL_events.h" @@ -471,7 +471,7 @@ void WindowSDL::WaitEvent() { } void WindowSDL::InitTimers() { - SDL_AddTimer(100, &PollController, controller); + SDL_AddTimer(33, &PollController, controller); SDL_AddTimer(33, Input::MousePolling, (void*)controller); } @@ -540,7 +540,7 @@ void WindowSDL::OnGamepadEvent(const SDL_Event* event) { // as it would break the entire touchpad handling // You can still bind other things to it though if (event->gbutton.button == SDL_GAMEPAD_BUTTON_TOUCHPAD) { - controller->CheckButton(0, OrbisPadButtonDataOffset::TouchPad, input_down); + controller->Button(0, OrbisPadButtonDataOffset::TouchPad, input_down); return; } From c2a47d2a9986582b24c04ed6cc4f3b3712deeb02 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:00:13 +0100 Subject: [PATCH 49/50] Handle operand fields execlo and exechi for S_MOV (#4023) Co-authored-by: TheTurtle --- src/shader_recompiler/frontend/translate/scalar_alu.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shader_recompiler/frontend/translate/scalar_alu.cpp b/src/shader_recompiler/frontend/translate/scalar_alu.cpp index b2e981d6a..8931e8dde 100644 --- a/src/shader_recompiler/frontend/translate/scalar_alu.cpp +++ b/src/shader_recompiler/frontend/translate/scalar_alu.cpp @@ -583,6 +583,14 @@ void Translator::S_MULK_I32(const GcnInst& inst) { // SOP1 void Translator::S_MOV(const GcnInst& inst) { + if (inst.dst[0].field == OperandField::ScalarGPR) { + if (inst.src[0].field == OperandField::ExecLo) { + ir.SetThreadBitScalarReg(IR::ScalarReg(inst.dst[0].code), ir.GetExec()); + return; + } else if (inst.src[0].field == OperandField::ExecHi) { + return; + } + } SetDst(inst.dst[0], GetSrc(inst.src[0])); } From 7bdb5eb7e104742436ee37a9381b33d045d51660 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:09:34 +0100 Subject: [PATCH 50/50] improved motion controls emulation (#4022) --- src/sdl_window.cpp | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index c9183f301..1ee107efe 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -269,9 +269,14 @@ namespace Frontend { using namespace Libraries::Pad; -static Uint32 SDLCALL PollController(void* userdata, SDL_TimerID timer_id, Uint32 interval) { +std::mutex motion_control_mutex; +float gyro_buf[3] = {0.0f, 0.0f, 0.0f}, accel_buf[3] = {0.0f, -9.81f, 0.0f}; +static Uint32 SDLCALL PollGyroAndAccel(void* userdata, SDL_TimerID timer_id, Uint32 interval) { auto* controller = reinterpret_cast(userdata); - return controller->Poll(); + std::scoped_lock l{motion_control_mutex}; + controller->Gyro(0, gyro_buf); + controller->Acceleration(0, accel_buf); + return 4; } WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_, @@ -407,12 +412,16 @@ void WindowSDL::WaitEvent() { // AND IT DOESN'T EVEN USE PROPER ENUMS case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: switch ((SDL_SensorType)event.gsensor.sensor) { - case SDL_SENSOR_GYRO: - controller->Gyro(0, event.gsensor.data); + case SDL_SENSOR_GYRO: { + std::scoped_lock l{motion_control_mutex}; + memcpy(gyro_buf, event.gsensor.data, sizeof(gyro_buf)); break; - case SDL_SENSOR_ACCEL: - controller->Acceleration(0, event.gsensor.data); + } + case SDL_SENSOR_ACCEL: { + std::scoped_lock l{motion_control_mutex}; + memcpy(accel_buf, event.gsensor.data, sizeof(accel_buf)); break; + } default: break; } @@ -471,7 +480,7 @@ void WindowSDL::WaitEvent() { } void WindowSDL::InitTimers() { - SDL_AddTimer(33, &PollController, controller); + SDL_AddTimer(4, &PollGyroAndAccel, controller); SDL_AddTimer(33, Input::MousePolling, (void*)controller); }