From d3ad728ac0ec033b13310b38269b2622f98b6697 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Sun, 7 Dec 2025 01:11:29 +0200 Subject: [PATCH 01/83] vector_alu: Handle -1 as src1 in v_cmp_u64 (#3855) --- .../frontend/translate/vector_alu.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index 94cefb958..0803647a2 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -1050,7 +1050,14 @@ void Translator::V_CMP_U32(ConditionOp op, bool is_signed, bool set_exec, const } void Translator::V_CMP_U64(ConditionOp op, bool is_signed, bool set_exec, const GcnInst& inst) { - ASSERT(inst.src[1].field == OperandField::ConstZero); + const bool is_zero = inst.src[1].field == OperandField::ConstZero; + const bool is_neg_one = inst.src[1].field == OperandField::SignedConstIntNeg; + ASSERT(is_zero || is_neg_one); + if (is_neg_one) { + ASSERT_MSG(-s32(inst.src[1].code) + SignedConstIntNegMin - 1 == -1, + "SignedConstIntNeg must be -1"); + } + const IR::U1 src0 = [&] { switch (inst.src[0].field) { case OperandField::ScalarGPR: @@ -1064,10 +1071,11 @@ void Translator::V_CMP_U64(ConditionOp op, bool is_signed, bool set_exec, const const IR::U1 result = [&] { switch (op) { case ConditionOp::EQ: - return ir.LogicalNot(src0); + return is_zero ? ir.LogicalNot(src0) : src0; case ConditionOp::LG: // NE - return src0; + return is_zero ? src0 : ir.LogicalNot(src0); case ConditionOp::GT: + ASSERT(is_zero); return ir.GroupAny(ir.GetThreadBitScalarReg(IR::ScalarReg(inst.src[0].code))); default: UNREACHABLE_MSG("Unsupported V_CMP_U64 condition operation: {}", u32(op)); From 391d30cbb1ba6a9b69166a465387a9c8ea177d91 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:47:39 -0600 Subject: [PATCH 02/83] cpu_patches: Patch stack canary accesses (#3857) * Patch stack checks done using fs:[0x28] Additionally adds support for multiple patches per instruction, since this makes two separate patches we need to conditionally perform for mov instructions. * Missing include * Disable patches for Apple Mac can use their native FS segment directly, so these patches aren't needed * Oops --- src/core/cpu_patches.cpp | 125 +++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/src/core/cpu_patches.cpp b/src/core/cpu_patches.cpp index 8c0897a48..2788cfe58 100644 --- a/src/core/cpu_patches.cpp +++ b/src/core/cpu_patches.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -122,6 +123,30 @@ static void GenerateTcbAccess(void* /* address */, const ZydisDecodedOperand* op #endif } +static bool FilterStackCheck(const ZydisDecodedOperand* operands) { + const auto& dst_op = operands[0]; + const auto& src_op = operands[1]; + + // Some compilers emit stack checks by starting a function with + // 'mov (64-bit register), fs:[0x28]', then checking with `xor (64-bit register), fs:[0x28]` + return src_op.type == ZYDIS_OPERAND_TYPE_MEMORY && src_op.mem.segment == ZYDIS_REGISTER_FS && + src_op.mem.base == ZYDIS_REGISTER_NONE && src_op.mem.index == ZYDIS_REGISTER_NONE && + src_op.mem.disp.value == 0x28 && dst_op.reg.value >= ZYDIS_REGISTER_RAX && + dst_op.reg.value <= ZYDIS_REGISTER_R15; +} + +static void GenerateStackCheck(void* /* address */, const ZydisDecodedOperand* operands, + Xbyak::CodeGenerator& c) { + const auto dst = ZydisToXbyakRegisterOperand(operands[0]); + c.xor_(dst, 0); +} + +static void GenerateStackCanary(void* /* address */, const ZydisDecodedOperand* operands, + Xbyak::CodeGenerator& c) { + const auto dst = ZydisToXbyakRegisterOperand(operands[0]); + c.mov(dst, 0); +} + static bool FilterNoSSE4a(const ZydisDecodedOperand*) { Cpu cpu; return !cpu.has(Cpu::tSSE4a); @@ -440,18 +465,26 @@ struct PatchInfo { bool trampoline; }; -static const std::unordered_map Patches = { +static const std::unordered_map> Patches = { // SSE4a - {ZYDIS_MNEMONIC_EXTRQ, {FilterNoSSE4a, GenerateEXTRQ, true}}, - {ZYDIS_MNEMONIC_INSERTQ, {FilterNoSSE4a, GenerateINSERTQ, true}}, - {ZYDIS_MNEMONIC_MOVNTSS, {FilterNoSSE4a, ReplaceMOVNTSS, false}}, - {ZYDIS_MNEMONIC_MOVNTSD, {FilterNoSSE4a, ReplaceMOVNTSD, false}}, + {ZYDIS_MNEMONIC_EXTRQ, {{FilterNoSSE4a, GenerateEXTRQ, true}}}, + {ZYDIS_MNEMONIC_INSERTQ, {{FilterNoSSE4a, GenerateINSERTQ, true}}}, + {ZYDIS_MNEMONIC_MOVNTSS, {{FilterNoSSE4a, ReplaceMOVNTSS, false}}}, + {ZYDIS_MNEMONIC_MOVNTSD, {{FilterNoSSE4a, ReplaceMOVNTSD, false}}}, +#if !defined(__APPLE__) + // FS segment patches + // These first two patches are for accesses to the stack canary, fs:[0x28] + {ZYDIS_MNEMONIC_XOR, {{FilterStackCheck, GenerateStackCheck, false}}}, + {ZYDIS_MNEMONIC_MOV, + {{FilterStackCheck, GenerateStackCanary, false}, #if defined(_WIN32) - // Windows needs a trampoline. - {ZYDIS_MNEMONIC_MOV, {FilterTcbAccess, GenerateTcbAccess, true}}, -#elif !defined(__APPLE__) - {ZYDIS_MNEMONIC_MOV, {FilterTcbAccess, GenerateTcbAccess, false}}, + // Windows needs a trampoline for Tcb accesses. + {FilterTcbAccess, GenerateTcbAccess, true} +#else + {FilterTcbAccess, GenerateTcbAccess, false} +#endif + }}, #endif }; @@ -503,51 +536,53 @@ static std::pair TryPatch(u8* code, PatchModule* module) { } if (Patches.contains(instruction.mnemonic)) { - const auto& patch_info = Patches.at(instruction.mnemonic); - bool needs_trampoline = patch_info.trampoline; - if (patch_info.filter(operands)) { - auto& patch_gen = module->patch_gen; + const auto& patches = Patches.at(instruction.mnemonic); + for (const auto& patch_info : patches) { + bool needs_trampoline = patch_info.trampoline; + if (patch_info.filter(operands)) { + auto& patch_gen = module->patch_gen; - if (needs_trampoline && instruction.length < 5) { - // Trampoline is needed but instruction is too short to patch. - // Return false and length to signal to AOT compilation that this instruction - // should be skipped and handled at runtime. - return std::make_pair(false, instruction.length); - } + if (needs_trampoline && instruction.length < 5) { + // Trampoline is needed but instruction is too short to patch. + // Return false and length to signal to AOT compilation that this instruction + // should be skipped and handled at runtime. + return std::make_pair(false, instruction.length); + } - // Reset state and move to current code position. - patch_gen.reset(); - patch_gen.setSize(code - patch_gen.getCode()); + // Reset state and move to current code position. + patch_gen.reset(); + patch_gen.setSize(code - patch_gen.getCode()); - if (needs_trampoline) { - auto& trampoline_gen = module->trampoline_gen; - const auto trampoline_ptr = trampoline_gen.getCurr(); + if (needs_trampoline) { + auto& trampoline_gen = module->trampoline_gen; + const auto trampoline_ptr = trampoline_gen.getCurr(); - patch_info.generator(code, operands, trampoline_gen); + patch_info.generator(code, operands, trampoline_gen); - // Return to the following instruction at the end of the trampoline. - trampoline_gen.jmp(code + instruction.length); + // Return to the following instruction at the end of the trampoline. + trampoline_gen.jmp(code + instruction.length); - // Replace instruction with near jump to the trampoline. - patch_gen.jmp(trampoline_ptr, Xbyak::CodeGenerator::LabelType::T_NEAR); - } else { - patch_info.generator(code, operands, patch_gen); - } + // Replace instruction with near jump to the trampoline. + patch_gen.jmp(trampoline_ptr, Xbyak::CodeGenerator::LabelType::T_NEAR); + } else { + patch_info.generator(code, operands, patch_gen); + } - const auto patch_size = patch_gen.getCurr() - code; - if (patch_size > 0) { - ASSERT_MSG(instruction.length >= patch_size, - "Instruction {} with length {} is too short to replace at: {}", - ZydisMnemonicGetString(instruction.mnemonic), instruction.length, - fmt::ptr(code)); + const auto patch_size = patch_gen.getCurr() - code; + if (patch_size > 0) { + ASSERT_MSG(instruction.length >= patch_size, + "Instruction {} with length {} is too short to replace at: {}", + ZydisMnemonicGetString(instruction.mnemonic), instruction.length, + fmt::ptr(code)); - // Fill remaining space with nops. - patch_gen.nop(instruction.length - patch_size); + // Fill remaining space with nops. + patch_gen.nop(instruction.length - patch_size); - module->patched.insert(code); - LOG_DEBUG(Core, "Patched instruction '{}' at: {}", - ZydisMnemonicGetString(instruction.mnemonic), fmt::ptr(code)); - return std::make_pair(true, instruction.length); + module->patched.insert(code); + LOG_DEBUG(Core, "Patched instruction '{}' at: {}", + ZydisMnemonicGetString(instruction.mnemonic), fmt::ptr(code)); + return std::make_pair(true, instruction.length); + } } } } From 2a5910ed5190770b4927b15308ff872045b9d6e3 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Sun, 7 Dec 2025 22:56:51 +0100 Subject: [PATCH 03/83] New translations en_us.ts (OpenOrbis) (#3858) --- src/core/libraries/kernel/threads/exception.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/libraries/kernel/threads/exception.cpp b/src/core/libraries/kernel/threads/exception.cpp index 5455d425e..95ced79c0 100644 --- a/src/core/libraries/kernel/threads/exception.cpp +++ b/src/core/libraries/kernel/threads/exception.cpp @@ -173,6 +173,8 @@ void RegisterException(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("OMDRKKAZ8I4", "libkernel", 1, "libkernel", sceKernelDebugRaiseException); LIB_FUNCTION("zE-wXIZjLoM", "libkernel", 1, "libkernel", sceKernelDebugRaiseExceptionOnReleaseMode); + LIB_FUNCTION("WkwEd3N7w0Y", "libkernel", 1, "libkernel", sceKernelInstallExceptionHandler); + LIB_FUNCTION("Qhv5ARAoOEc", "libkernel", 1, "libkernel", sceKernelRemoveExceptionHandler); } } // namespace Libraries::Kernel From 65f0b07c34d0d0fe43bbcb25556741bb629b233e Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Mon, 8 Dec 2025 05:46:32 -0600 Subject: [PATCH 04/83] libkernel: Implement sceKernelEnableDmemAliasing, proper mapping type checks in posix_mmap (#3859) * Basic handling for MAP_VOID, MAP_STACK, and MAP_ANON in mmap. * Update memory.cpp * Update memory.cpp * Dmem aliasing check * Oops --- src/core/libraries/kernel/memory.cpp | 45 ++++++++++++++++++++++++---- src/core/memory.h | 3 ++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 92280308d..62903ff72 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -5,24 +5,35 @@ #include "common/alignment.h" #include "common/assert.h" +#include "common/elf_info.h" #include "common/logging/log.h" #include "common/scope_exit.h" #include "common/singleton.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/kernel/process.h" #include "core/libraries/libs.h" #include "core/linker.h" #include "core/memory.h" namespace Libraries::Kernel { +static s32 g_sdk_version = -1; +static bool g_alias_dmem = false; + u64 PS4_SYSV_ABI sceKernelGetDirectMemorySize() { LOG_TRACE(Kernel_Vmm, "called"); const auto* memory = Core::Memory::Instance(); return memory->GetTotalDirectSize(); } +s32 PS4_SYSV_ABI sceKernelEnableDmemAliasing() { + LOG_DEBUG(Kernel_Vmm, "called"); + g_alias_dmem = true; + return ORBIS_OK; +} + s32 PS4_SYSV_ABI sceKernelAllocateDirectMemory(s64 searchStart, s64 searchEnd, u64 len, u64 alignment, s32 memoryType, s64* physAddrOut) { if (searchStart < 0 || searchEnd < 0) { @@ -197,8 +208,14 @@ s32 PS4_SYSV_ABI sceKernelMapNamedDirectMemory(void** addr, u64 len, s32 prot, s const VAddr in_addr = reinterpret_cast(*addr); auto* memory = Core::Memory::Instance(); - const auto ret = memory->MapMemory(addr, in_addr, len, mem_prot, map_flags, - Core::VMAType::Direct, name, false, phys_addr, alignment); + bool should_check = false; + if (g_sdk_version >= Common::ElfInfo::FW_25 && False(map_flags & Core::MemoryMapFlags::Stack)) { + // Under these conditions, this would normally redirect to sceKernelMapDirectMemory2. + should_check = !g_alias_dmem; + } + const auto ret = + memory->MapMemory(addr, in_addr, len, mem_prot, map_flags, Core::VMAType::Direct, name, + should_check, phys_addr, alignment); LOG_INFO(Kernel_Vmm, "out_addr = {}", fmt::ptr(*addr)); return ret; @@ -244,8 +261,9 @@ s32 PS4_SYSV_ABI sceKernelMapDirectMemory2(void** addr, u64 len, s32 type, s32 p const VAddr in_addr = reinterpret_cast(*addr); auto* memory = Core::Memory::Instance(); - const auto ret = memory->MapMemory(addr, in_addr, len, mem_prot, map_flags, - Core::VMAType::Direct, "anon", true, phys_addr, alignment); + const auto ret = + memory->MapMemory(addr, in_addr, len, mem_prot, map_flags, Core::VMAType::Direct, "anon", + !g_alias_dmem, phys_addr, alignment); if (ret == 0) { // If the map call succeeds, set the direct memory type using the output address. @@ -668,10 +686,21 @@ void* PS4_SYSV_ABI posix_mmap(void* addr, u64 len, s32 prot, s32 flags, s32 fd, } s32 result = ORBIS_OK; - if (fd == -1) { + if (True(mem_flags & Core::MemoryMapFlags::Anon)) { + // Maps flexible memory result = memory->MapMemory(&addr_out, aligned_addr, aligned_size, mem_prot, mem_flags, Core::VMAType::Flexible, "anon", false); + } else if (True(mem_flags & Core::MemoryMapFlags::Stack)) { + // Maps stack memory + result = memory->MapMemory(&addr_out, aligned_addr, aligned_size, mem_prot, mem_flags, + Core::VMAType::Stack, "anon", false); + } else if (True(mem_flags & Core::MemoryMapFlags::Void)) { + // Reserves memory + result = + memory->MapMemory(&addr_out, aligned_addr, aligned_size, Core::MemoryProt::NoAccess, + mem_flags, Core::VMAType::Reserved, "anon", false); } else { + // Default to file mapping result = memory->MapFile(&addr_out, aligned_addr, aligned_size, mem_prot, mem_flags, fd, phys_addr); } @@ -769,6 +798,12 @@ s32 PS4_SYSV_ABI sceKernelGetPrtAperture(s32 id, VAddr* address, u64* size) { } void RegisterMemory(Core::Loader::SymbolsResolver* sym) { + ASSERT_MSG(sceKernelGetCompiledSdkVersion(&g_sdk_version) == ORBIS_OK, + "Failed to get compiled SDK verision."); + + LIB_FUNCTION("usHTMoFoBTM", "libkernel_dmem_aliasing2", 1, "libkernel", + sceKernelEnableDmemAliasing); + LIB_FUNCTION("usHTMoFoBTM", "libkernel", 1, "libkernel", sceKernelEnableDmemAliasing); LIB_FUNCTION("rTXw65xmLIA", "libkernel", 1, "libkernel", sceKernelAllocateDirectMemory); LIB_FUNCTION("B+vc2AO2Zrc", "libkernel", 1, "libkernel", sceKernelAllocateMainDirectMemory); LIB_FUNCTION("C0f7TJcbfac", "libkernel", 1, "libkernel", sceKernelAvailableDirectMemorySize); diff --git a/src/core/memory.h b/src/core/memory.h index db988c305..7ebf9d34c 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -45,7 +45,10 @@ enum class MemoryMapFlags : u32 { Private = 2, Fixed = 0x10, NoOverwrite = 0x80, + Void = 0x100, + Stack = 0x400, NoSync = 0x800, + Anon = 0x1000, NoCore = 0x20000, NoCoalesce = 0x400000, }; From de6c5bbb836f05560a3639810f4ea8e9ef044c3d Mon Sep 17 00:00:00 2001 From: AlpinDale <52078762+AlpinDale@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:32:54 +0430 Subject: [PATCH 05/83] cli: add `--show-fps` to the CLI launcher (#3860) * cli: add `--show-fps` to the CLI launcher * fix: clang-format * nit: PascalCase -> camelCase --- src/common/config.cpp | 12 ++++++++++++ src/common/config.h | 2 ++ src/core/devtools/layer.cpp | 6 ++++++ src/core/devtools/layer.h | 1 + src/main.cpp | 7 +++++-- 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/common/config.cpp b/src/common/config.cpp index 94d8b488c..1af326af7 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -199,6 +199,7 @@ static ConfigEntry isDebugDump(false); static ConfigEntry isShaderDebug(false); static ConfigEntry isSeparateLogFilesEnabled(false); static ConfigEntry isFpsColor(true); +static ConfigEntry showFpsCounter(false); static ConfigEntry logEnabled(true); // GUI @@ -466,6 +467,14 @@ bool fpsColor() { return isFpsColor.get(); } +bool getShowFpsCounter() { + return showFpsCounter.get(); +} + +void setShowFpsCounter(bool enable, bool is_game_specific) { + showFpsCounter.set(enable, is_game_specific); +} + bool isLoggingEnabled() { return logEnabled.get(); } @@ -969,6 +978,7 @@ void load(const std::filesystem::path& path, bool is_game_specific) { isSeparateLogFilesEnabled.setFromToml(debug, "isSeparateLogFilesEnabled", is_game_specific); isShaderDebug.setFromToml(debug, "CollectShader", is_game_specific); isFpsColor.setFromToml(debug, "FPSColor", is_game_specific); + showFpsCounter.setFromToml(debug, "showFpsCounter", is_game_specific); logEnabled.setFromToml(debug, "logEnabled", is_game_specific); current_version = toml::find_or(debug, "ConfigVersion", current_version); } @@ -1188,6 +1198,7 @@ void save(const std::filesystem::path& path, bool is_game_specific) { data["GPU"]["internalScreenHeight"] = internalScreenHeight.base_value; data["GPU"]["patchShaders"] = shouldPatchShaders.base_value; data["Debug"]["FPSColor"] = isFpsColor.base_value; + data["Debug"]["showFpsCounter"] = showFpsCounter.base_value; } // Sorting of TOML sections @@ -1296,6 +1307,7 @@ void setDefaultValues(bool is_game_specific) { // Debug isFpsColor.base_value = true; + showFpsCounter.base_value = false; } } diff --git a/src/common/config.h b/src/common/config.h index 481ef6444..2bd65b783 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -126,6 +126,8 @@ bool getPSNSignedIn(); void setPSNSignedIn(bool sign, bool is_game_specific = false); bool patchShaders(); // no set bool fpsColor(); // no set +bool getShowFpsCounter(); +void setShowFpsCounter(bool enable, bool is_game_specific = false); bool isNeoModeConsole(); void setNeoMode(bool enable, bool is_game_specific = false); bool isDevKitConsole(); diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index 1fb810030..cfa950568 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -311,6 +311,7 @@ static void LoadSettings(const char* line) { void L::SetupSettings() { frame_graph.is_open = true; + show_simple_fps = Config::getShowFpsCounter(); using SettingLoader = void (*)(const char*); @@ -475,6 +476,11 @@ void ToggleSimpleFps() { visibility_toggled = true; } +void SetSimpleFps(bool enabled) { + show_simple_fps = enabled; + visibility_toggled = true; +} + void ToggleQuitWindow() { show_quit_window = !show_quit_window; } diff --git a/src/core/devtools/layer.h b/src/core/devtools/layer.h index 44afc95bc..96b48a7f0 100644 --- a/src/core/devtools/layer.h +++ b/src/core/devtools/layer.h @@ -30,6 +30,7 @@ private: namespace Overlay { void ToggleSimpleFps(); +void SetSimpleFps(bool enabled); void ToggleQuitWindow(); } // namespace Overlay diff --git a/src/main.cpp b/src/main.cpp index f1e5ce932..b3a8586ba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -67,6 +67,7 @@ int main(int argc, char* argv[]) { "values, ignores the config file(s) entirely.\n" " --config-global Run the emulator with the base config file " "only, ignores game specific configs.\n" + " --show-fps Enable FPS counter display at startup\n" " -h, --help Display this help message\n"; exit(0); }}, @@ -174,13 +175,15 @@ int main(int argc, char* argv[]) { game_folder = folder; }}, {"--wait-for-debugger", [&](int& i) { waitForDebugger = true; }}, - {"--wait-for-pid", [&](int& i) { + {"--wait-for-pid", + [&](int& i) { if (++i >= argc) { std::cerr << "Error: Missing argument for --wait-for-pid\n"; exit(1); } waitPid = std::stoi(argv[i]); - }}}; + }}, + {"--show-fps", [&](int& i) { Config::setShowFpsCounter(true); }}}; if (argc == 1) { if (!SDL_ShowSimpleMessageBox( From 9e7df6ae5406f54712c85e8cd28ee7d27284742b Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 11 Dec 2025 04:24:45 -0600 Subject: [PATCH 06/83] Kernel.Vmm: Remove hack from #2726 (#3864) * Remove SceKernelInternalMemory mapping Contrary to my initial beliefs, this is very much a hack. * Unreachable for unpatched code This will always infinitely loop, making logs extremely large. * Update linker.cpp --- src/core/cpu_patches.cpp | 9 +++++---- src/core/linker.cpp | 11 ----------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/core/cpu_patches.cpp b/src/core/cpu_patches.cpp index 2788cfe58..e303417c3 100644 --- a/src/core/cpu_patches.cpp +++ b/src/core/cpu_patches.cpp @@ -790,11 +790,12 @@ static bool PatchesIllegalInstructionHandler(void* context) { Common::Decoder::Instance()->decodeInstruction(instruction, operands, code_address); if (ZYAN_SUCCESS(status) && instruction.mnemonic == ZydisMnemonic::ZYDIS_MNEMONIC_UD2) [[unlikely]] { - UNREACHABLE_MSG("ud2 at code address {:#x}", (u64)code_address); + UNREACHABLE_MSG("ud2 at code address {:#x}", reinterpret_cast(code_address)); } - LOG_ERROR(Core, "Failed to patch address {:x} -- mnemonic: {}", (u64)code_address, - ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic) - : "Failed to decode"); + UNREACHABLE_MSG("Failed to patch address {:x} -- mnemonic: {}", + reinterpret_cast(code_address), + ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic) + : "Failed to decode"); } } diff --git a/src/core/linker.cpp b/src/core/linker.cpp index ac6b37769..8d7f9207e 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -104,17 +104,6 @@ void Linker::Execute(const std::vector& args) { memory->SetupMemoryRegions(fmem_size, use_extended_mem1, use_extended_mem2); - // Simulate sceKernelInternalMemory mapping, a mapping usually performed during libkernel init. - // Due to the large size of this mapping, failing to emulate it causes issues in some titles. - // This mapping belongs in the system reserved area, which starts at address 0x880000000. - static constexpr VAddr KernelAllocBase = 0x880000000ULL; - static constexpr s64 InternalMemorySize = 0x1000000; - void* addr_out{reinterpret_cast(KernelAllocBase)}; - - s32 ret = Libraries::Kernel::sceKernelMapNamedFlexibleMemory(&addr_out, InternalMemorySize, 3, - 0, "SceKernelInternalMemory"); - ASSERT_MSG(ret == 0, "Unable to perform sceKernelInternalMemory mapping"); - main_thread.Run([this, module, &args](std::stop_token) { Common::SetCurrentThreadName("GAME_MainThread"); if (auto& ipc = IPC::Instance()) { From 9e287564ced1c7d84a5a165ce4ad6ba85d561ee1 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 12 Dec 2025 23:40:17 +0200 Subject: [PATCH 07/83] update submodules (ffmpeg,fmt,sdl3) (#3865) * update submodules (ffmpeg,fmt,sdl3) * fixed linux builds? * more linux issues * let's see more linux... * baby one more time --- .github/workflows/build.yml | 4 ++-- externals/ffmpeg-core | 2 +- externals/fmt | 2 +- externals/sdl3 | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1fab6354..8cf5efbf0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,7 +160,7 @@ jobs: sudo add-apt-repository 'deb http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main' - name: Install dependencies - run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 clang-19 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev + run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 clang-19 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev - name: Cache CMake Configuration uses: actions/cache@v4 @@ -216,7 +216,7 @@ jobs: submodules: recursive - name: Install dependencies - run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 gcc-14 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev + run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 gcc-14 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev - name: Cache CMake Configuration uses: actions/cache@v4 diff --git a/externals/ffmpeg-core b/externals/ffmpeg-core index b0de1dcca..94dde08c8 160000 --- a/externals/ffmpeg-core +++ b/externals/ffmpeg-core @@ -1 +1 @@ -Subproject commit b0de1dcca26c0ebfb8011b8e59dd17fc399db0ff +Subproject commit 94dde08c8a9e4271a93a2a7e4159e9fb05d30c0a diff --git a/externals/fmt b/externals/fmt index 64db979e3..ec73fb724 160000 --- a/externals/fmt +++ b/externals/fmt @@ -1 +1 @@ -Subproject commit 64db979e38ec644b1798e41610b28c8d2c8a2739 +Subproject commit ec73fb72477d80926c758894a3ab2cb3994fd051 diff --git a/externals/sdl3 b/externals/sdl3 index e9c2e9bfc..bdb72bb3f 160000 --- a/externals/sdl3 +++ b/externals/sdl3 @@ -1 +1 @@ -Subproject commit e9c2e9bfc3a6e1e70596f743fa9e1fc5fadabef7 +Subproject commit bdb72bb3f051de32c91f5deb439a50bfd51499dc From eae5e0ad55aeaa8f22f4e133c8047b6c626a6b7f Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Tue, 16 Dec 2025 01:50:14 +0100 Subject: [PATCH 08/83] Initialize VK_EXT_shader_atomic_float before VK_EXT_shader_atomic_float2 (#3867) --- src/video_core/renderer_vulkan/vk_instance.cpp | 1 + src/video_core/renderer_vulkan/vk_instance.h | 1 + 2 files changed, 2 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index ca7d09c52..44aa79d98 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -299,6 +299,7 @@ bool Instance::CreateDevice() { amd_shader_trinary_minmax = add_extension(VK_AMD_SHADER_TRINARY_MINMAX_EXTENSION_NAME); nv_framebuffer_mixed_samples = add_extension(VK_NV_FRAMEBUFFER_MIXED_SAMPLES_EXTENSION_NAME); amd_mixed_attachment_samples = add_extension(VK_AMD_MIXED_ATTACHMENT_SAMPLES_EXTENSION_NAME); + shader_atomic_float = add_extension(VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME); shader_atomic_float2 = add_extension(VK_EXT_SHADER_ATOMIC_FLOAT_2_EXTENSION_NAME); if (shader_atomic_float2) { shader_atomic_float2_features = diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index bbefdc1b3..8975669bb 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -493,6 +493,7 @@ private: bool amd_shader_trinary_minmax{}; bool nv_framebuffer_mixed_samples{}; bool amd_mixed_attachment_samples{}; + bool shader_atomic_float{}; bool shader_atomic_float2{}; bool workgroup_memory_explicit_layout{}; bool portability_subset{}; From 2bbb04ff556192f94b03db57312c03a41d3a0192 Mon Sep 17 00:00:00 2001 From: marecl Date: Fri, 19 Dec 2025 13:58:07 +0100 Subject: [PATCH 09/83] ENAMETOOLONG, posix_rename fix (#3869) * ENAMETOOLONG on paths > 255 characters Corrected posix_rename behaviour: * handles errors only if dst exists (shouldn't error out if doesn't) * recursively removes "old name" (would otherwise fail if it's a not-empty dir) * actually creates target directory * Updated detection * destubbed unlink() * proper error returned --- src/core/file_sys/fs.cpp | 3 + src/core/libraries/kernel/file_system.cpp | 112 +++++++++++++++------- 2 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index 2a0fa43dd..f6c34ae94 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -52,6 +52,9 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea pos = corrected_path.find("//", pos + 1); } + if (path.length() > 255) + return ""; + const MntPair* mount = GetMount(corrected_path); if (!mount) { return ""; diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index b4c342f18..5330b90fd 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -37,6 +37,7 @@ #endif namespace D = Core::Devices; +namespace fs = std::filesystem; using FactoryDevice = std::function(u32, const char*, int, u16)>; #define GET_DEVICE_FD(fd) \ @@ -74,6 +75,7 @@ namespace Libraries::Kernel { s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { LOG_INFO(Kernel_Fs, "path = {} flags = {:#x} mode = {:#o}", raw_path, flags, mode); + auto* h = Common::Singleton::Instance(); auto* mnt = Common::Singleton::Instance(); @@ -87,6 +89,11 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { return -1; } + if (strlen(raw_path) > 255) { + *__Error() = POSIX_ENAMETOOLONG; + return -1; + } + bool nonblock = (flags & ORBIS_KERNEL_O_NONBLOCK) != 0; bool append = (flags & ORBIS_KERNEL_O_APPEND) != 0; // Flags fsync and sync behave the same @@ -121,7 +128,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { bool read_only = false; file->m_guest_name = path; file->m_host_name = mnt->GetHostPath(file->m_guest_name, &read_only); - bool exists = std::filesystem::exists(file->m_host_name); + bool exists = fs::exists(file->m_host_name); s32 e = 0; if (create) { @@ -149,14 +156,14 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) { return -1; } - if (std::filesystem::is_directory(file->m_host_name) || directory) { + if (fs::is_directory(file->m_host_name) || directory) { // Directories can be opened even if the directory flag isn't set. // In these cases, error behavior is identical to the directory code path. directory = true; } if (directory) { - if (!std::filesystem::is_directory(file->m_host_name)) { + if (!fs::is_directory(file->m_host_name)) { // If the opened file is not a directory, return ENOTDIR. // This will trigger when create & directory is specified, this is expected. h->DeleteHandle(handle); @@ -554,6 +561,10 @@ s64 PS4_SYSV_ABI sceKernelRead(s32 fd, void* buf, u64 nbytes) { s32 PS4_SYSV_ABI posix_mkdir(const char* path, u16 mode) { LOG_INFO(Kernel_Fs, "path = {} mode = {:#o}", path, mode); + if (strlen(path) > 255) { + *__Error() = POSIX_ENAMETOOLONG; + return -1; + } if (path == nullptr) { *__Error() = POSIX_ENOTDIR; return -1; @@ -563,7 +574,7 @@ s32 PS4_SYSV_ABI posix_mkdir(const char* path, u16 mode) { bool ro = false; const auto dir_name = mnt->GetHostPath(path, &ro); - if (std::filesystem::exists(dir_name)) { + if (fs::exists(dir_name)) { *__Error() = POSIX_EEXIST; return -1; } @@ -575,12 +586,12 @@ s32 PS4_SYSV_ABI posix_mkdir(const char* path, u16 mode) { // CUSA02456: path = /aotl after sceSaveDataMount(mode = 1) std::error_code ec; - if (dir_name.empty() || !std::filesystem::create_directory(dir_name, ec)) { + if (dir_name.empty() || !fs::create_directory(dir_name, ec)) { *__Error() = POSIX_EIO; return -1; } - if (!std::filesystem::exists(dir_name)) { + if (!fs::exists(dir_name)) { *__Error() = POSIX_ENOENT; return -1; } @@ -597,28 +608,32 @@ s32 PS4_SYSV_ABI sceKernelMkdir(const char* path, u16 mode) { } s32 PS4_SYSV_ABI posix_rmdir(const char* path) { + if (strlen(path) > 255) { + *__Error() = POSIX_ENAMETOOLONG; + return -1; + } auto* mnt = Common::Singleton::Instance(); bool ro = false; - const std::filesystem::path dir_name = mnt->GetHostPath(path, &ro); + const fs::path dir_name = mnt->GetHostPath(path, &ro); if (ro) { *__Error() = POSIX_EROFS; return -1; } - if (dir_name.empty() || !std::filesystem::is_directory(dir_name)) { + if (dir_name.empty() || !fs::is_directory(dir_name)) { *__Error() = POSIX_ENOTDIR; return -1; } - if (!std::filesystem::exists(dir_name)) { + if (!fs::exists(dir_name)) { *__Error() = POSIX_ENOENT; return -1; } std::error_code ec; - s32 result = std::filesystem::remove_all(dir_name, ec); + s32 result = fs::remove_all(dir_name, ec); if (ec) { *__Error() = POSIX_EIO; @@ -638,11 +653,15 @@ s32 PS4_SYSV_ABI sceKernelRmdir(const char* path) { s32 PS4_SYSV_ABI posix_stat(const char* path, OrbisKernelStat* sb) { LOG_DEBUG(Kernel_Fs, "(PARTIAL) path = {}", path); + if (strlen(path) > 255) { + *__Error() = POSIX_ENAMETOOLONG; + return -1; + } auto* mnt = Common::Singleton::Instance(); const auto path_name = mnt->GetHostPath(path); std::memset(sb, 0, sizeof(OrbisKernelStat)); - const bool is_dir = std::filesystem::is_directory(path_name); - const bool is_file = std::filesystem::is_regular_file(path_name); + const bool is_dir = fs::is_directory(path_name); + const bool is_file = fs::is_regular_file(path_name); if (!is_dir && !is_file) { *__Error() = POSIX_ENOENT; return -1; @@ -650,12 +669,12 @@ s32 PS4_SYSV_ABI posix_stat(const char* path, OrbisKernelStat* sb) { // get the difference between file clock and system clock const auto now_sys = std::chrono::system_clock::now(); - const auto now_file = std::filesystem::file_time_type::clock::now(); + const auto now_file = fs::file_time_type::clock::now(); // calculate the file modified time - const auto mtime = std::filesystem::last_write_time(path_name); + const auto mtime = fs::last_write_time(path_name); const auto mtimestamp = now_sys + (mtime - now_file); - if (std::filesystem::is_directory(path_name)) { + if (fs::is_directory(path_name)) { sb->st_mode = 0000777u | 0040000u; sb->st_size = 65536; sb->st_blksize = 65536; @@ -665,7 +684,7 @@ s32 PS4_SYSV_ABI posix_stat(const char* path, OrbisKernelStat* sb) { // TODO incomplete } else { sb->st_mode = 0000777u | 0100000u; - sb->st_size = static_cast(std::filesystem::file_size(path_name)); + sb->st_size = static_cast(fs::file_size(path_name)); sb->st_blksize = 512; sb->st_blocks = (sb->st_size + 511) / 512; sb->st_mtim.tv_sec = @@ -686,6 +705,10 @@ s32 PS4_SYSV_ABI sceKernelStat(const char* path, OrbisKernelStat* sb) { } s32 PS4_SYSV_ABI sceKernelCheckReachability(const char* path) { + if (strlen(path) > 255) { + return ORBIS_KERNEL_ERROR_ENAMETOOLONG; + } + auto* mnt = Common::Singleton::Instance(); std::string_view guest_path{path}; for (const auto& prefix : available_device | std::views::keys) { @@ -694,7 +717,7 @@ s32 PS4_SYSV_ABI sceKernelCheckReachability(const char* path) { } } const auto path_name = mnt->GetHostPath(guest_path); - if (!std::filesystem::exists(path_name)) { + if (!fs::exists(path_name)) { return ORBIS_KERNEL_ERROR_ENOENT; } return ORBIS_OK; @@ -807,7 +830,15 @@ s32 PS4_SYSV_ABI posix_rename(const char* from, const char* to) { auto* mnt = Common::Singleton::Instance(); bool ro = false; const auto src_path = mnt->GetHostPath(from, &ro); - if (!std::filesystem::exists(src_path)) { + if (strlen(from) > 255) { + *__Error() = POSIX_ENAMETOOLONG; + return -1; + } + if (strlen(to) > 255) { + *__Error() = POSIX_ENAMETOOLONG; + return -1; + } + if (!fs::exists(src_path)) { *__Error() = POSIX_ENOENT; return -1; } @@ -820,32 +851,36 @@ s32 PS4_SYSV_ABI posix_rename(const char* from, const char* to) { *__Error() = POSIX_EROFS; return -1; } - const bool src_is_dir = std::filesystem::is_directory(src_path); - const bool dst_is_dir = std::filesystem::is_directory(dst_path); - if (src_is_dir && !dst_is_dir) { - *__Error() = POSIX_ENOTDIR; - return -1; - } - if (!src_is_dir && dst_is_dir) { - *__Error() = POSIX_EISDIR; - return -1; - } - if (dst_is_dir && !std::filesystem::is_empty(dst_path)) { - *__Error() = POSIX_ENOTEMPTY; - return -1; + const bool src_is_dir = fs::is_directory(src_path); + const bool dst_is_dir = fs::is_directory(dst_path); + + if (fs::exists(dst_path)) { + if (src_is_dir && !dst_is_dir) { + *__Error() = POSIX_ENOTDIR; + return -1; + } + if (!src_is_dir && dst_is_dir) { + *__Error() = POSIX_EISDIR; + return -1; + } + if (dst_is_dir && !fs::is_empty(dst_path)) { + *__Error() = POSIX_ENOTEMPTY; + return -1; + } } - // On Windows, std::filesystem::rename will error if the file has been opened before. - std::filesystem::copy(src_path, dst_path, std::filesystem::copy_options::overwrite_existing); + // On Windows, fs::rename will error if the file has been opened before. + fs::copy(src_path, dst_path, + fs::copy_options::overwrite_existing | fs::copy_options::recursive); auto* h = Common::Singleton::Instance(); auto file = h->GetFile(src_path); if (file) { auto access_mode = file->f.GetAccessMode(); file->f.Close(); - std::filesystem::remove(src_path); + fs::remove(src_path); file->f.Open(dst_path, access_mode); } else { - std::filesystem::remove(src_path); + fs::remove_all(src_path); } return ORBIS_OK; @@ -1098,6 +1133,10 @@ s64 PS4_SYSV_ABI sceKernelPwritev(s32 fd, const OrbisKernelIovec* iov, s32 iovcn } s32 PS4_SYSV_ABI posix_unlink(const char* path) { + if (strlen(path) > 255) { + *__Error() = POSIX_ENAMETOOLONG; + return -1; + } if (path == nullptr) { *__Error() = POSIX_EINVAL; return -1; @@ -1118,7 +1157,7 @@ s32 PS4_SYSV_ABI posix_unlink(const char* path) { return -1; } - if (std::filesystem::is_directory(host_path)) { + if (fs::is_directory(host_path)) { *__Error() = POSIX_EPERM; return -1; } @@ -1491,6 +1530,7 @@ void RegisterFileSystem(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("FCcmRZhWtOk", "libkernel", 1, "libkernel", posix_pwritev); LIB_FUNCTION("nKWi-N2HBV4", "libkernel", 1, "libkernel", sceKernelPwrite); LIB_FUNCTION("mBd4AfLP+u8", "libkernel", 1, "libkernel", sceKernelPwritev); + LIB_FUNCTION("VAzswvTOCzI", "libkernel", 1, "libkernel", posix_unlink); LIB_FUNCTION("AUXVxWeJU-A", "libkernel", 1, "libkernel", sceKernelUnlink); LIB_FUNCTION("T8fER+tIGgk", "libScePosix", 1, "libkernel", posix_select); LIB_FUNCTION("T8fER+tIGgk", "libkernel", 1, "libkernel", posix_select); From 138425fdf411083b66d68740e58024def13fc7a3 Mon Sep 17 00:00:00 2001 From: Rodrigo Cioletti Date: Sun, 21 Dec 2025 10:50:49 -0300 Subject: [PATCH 10/83] Network: Fixed null string crash on sceNetResolverCreate (#3872) * Network: Fixed null string crash on sceNetResolverCreate * Assigned an empty string for better code styling * Fixed wrong commit --- src/core/libraries/network/net.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/libraries/network/net.cpp b/src/core/libraries/network/net.cpp index 97005813b..a365d407b 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -1357,7 +1357,8 @@ int PS4_SYSV_ABI sceNetResolverConnectDestroy() { } int PS4_SYSV_ABI sceNetResolverCreate(const char* name, int poolid, int flags) { - LOG_INFO(Lib_Net, "name = {}, poolid = {}, flags = {}", name, poolid, flags); + const char* safe_name = name ? name : ""; + LOG_INFO(Lib_Net, "name = {}, poolid = {}, flags = {}", safe_name, poolid, flags); if (flags != 0) { *sceNetErrnoLoc() = ORBIS_NET_EINVAL; @@ -1368,8 +1369,8 @@ int PS4_SYSV_ABI sceNetResolverCreate(const char* name, int poolid, int flags) { auto* resolver = FDTable::Instance()->GetFile(fd); resolver->is_opened = true; resolver->type = Core::FileSys::FileType::Resolver; - resolver->resolver = std::make_shared(name, poolid, flags); - resolver->m_guest_name = name; + resolver->resolver = std::make_shared(safe_name, poolid, flags); + resolver->m_guest_name = safe_name; return fd; } From 05f14e36829d775329a774772e7bcd25dedff3f3 Mon Sep 17 00:00:00 2001 From: Vladislav Mikhalin Date: Tue, 23 Dec 2025 12:11:52 +0300 Subject: [PATCH 11/83] ajm fixes (#3875) --- src/core/libraries/ajm/ajm.h | 2 +- src/core/libraries/ajm/ajm_at9.cpp | 52 ++++++--- src/core/libraries/ajm/ajm_at9.h | 5 +- src/core/libraries/ajm/ajm_batch.h | 1 + src/core/libraries/ajm/ajm_context.cpp | 11 +- src/core/libraries/ajm/ajm_instance.cpp | 107 ++++++++---------- src/core/libraries/ajm/ajm_instance.h | 14 ++- .../libraries/ajm/ajm_instance_statistics.cpp | 6 +- .../libraries/ajm/ajm_instance_statistics.h | 1 + src/core/libraries/ajm/ajm_mp3.cpp | 44 ++++--- src/core/libraries/ajm/ajm_mp3.h | 6 +- src/core/libraries/ajm/ajm_result.h | 17 +++ 12 files changed, 165 insertions(+), 101 deletions(-) create mode 100644 src/core/libraries/ajm/ajm_result.h diff --git a/src/core/libraries/ajm/ajm.h b/src/core/libraries/ajm/ajm.h index 2c529cd4b..d68a4c0f4 100644 --- a/src/core/libraries/ajm/ajm.h +++ b/src/core/libraries/ajm/ajm.h @@ -133,7 +133,7 @@ struct AjmSidebandGaplessDecode { struct AjmSidebandResampleParameters { float ratio; - uint32_t flags; + u32 flags; }; struct AjmDecAt9InitializeParameters { diff --git a/src/core/libraries/ajm/ajm_at9.cpp b/src/core/libraries/ajm/ajm_at9.cpp index 014d1a4e5..ea7add4f3 100644 --- a/src/core/libraries/ajm/ajm_at9.cpp +++ b/src/core/libraries/ajm/ajm_at9.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "ajm_result.h" #include "common/assert.h" #include "core/libraries/ajm/ajm_at9.h" #include "error_codes.h" @@ -85,8 +86,8 @@ void AjmAt9Decoder::GetInfo(void* out_info) const { auto* info = reinterpret_cast(out_info); info->super_frame_size = m_codec_info.superframeSize; info->frames_in_super_frame = m_codec_info.framesInSuperframe; + info->next_frame_size = m_superframe_bytes_remain; info->frame_samples = m_codec_info.frameSamples; - info->next_frame_size = static_cast(m_handle)->Config.FrameBytes; } u8 g_at9_guid[] = {0xD2, 0x42, 0xE1, 0x47, 0xBA, 0x36, 0x8D, 0x4D, @@ -133,18 +134,22 @@ void AjmAt9Decoder::ParseRIFFHeader(std::span& in_buf, AjmInstanceGapless& g } } -std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf, - SparseOutputBuffer& output, - AjmInstanceGapless& gapless) { - bool is_reset = false; +u32 AjmAt9Decoder::GetMinimumInputSize() const { + return m_superframe_bytes_remain; +} + +DecoderResult AjmAt9Decoder::ProcessData(std::span& in_buf, SparseOutputBuffer& output, + AjmInstanceGapless& gapless) { + DecoderResult result{}; if (True(m_flags & AjmAt9CodecFlags::ParseRiffHeader) && *reinterpret_cast(in_buf.data()) == 'FFIR') { ParseRIFFHeader(in_buf, gapless); - is_reset = true; + result.is_reset = true; } if (!m_is_initialized) { - return {0, 0, is_reset}; + result.result = ORBIS_AJM_RESULT_NOT_INITIALIZED; + return result; } int ret = 0; @@ -166,7 +171,14 @@ std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf, default: UNREACHABLE(); } - ASSERT_MSG(ret == At9Status::ERR_SUCCESS, "Atrac9Decode failed ret = {:#x}", ret); + if (ret != At9Status::ERR_SUCCESS) { + LOG_ERROR(Lib_Ajm, "Atrac9Decode failed ret = {:#x}", ret); + result.result = ORBIS_AJM_RESULT_CODEC_ERROR | ORBIS_AJM_RESULT_FATAL; + result.internal_result = ret; + return result; + } + + result.frames_decoded += 1; in_buf = in_buf.subspan(bytes_used); m_superframe_bytes_remain -= bytes_used; @@ -196,10 +208,10 @@ std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf, UNREACHABLE(); } - const auto samples_written = pcm_written / m_codec_info.channels; - gapless.current.skipped_samples += m_codec_info.frameSamples - samples_written; + result.samples_written = pcm_written / m_codec_info.channels; + gapless.current.skipped_samples += m_codec_info.frameSamples - result.samples_written; if (gapless.init.total_samples != 0) { - gapless.current.total_samples -= samples_written; + gapless.current.total_samples -= result.samples_written; } m_num_frames += 1; @@ -209,9 +221,23 @@ std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf, } m_superframe_bytes_remain = m_codec_info.superframeSize; m_num_frames = 0; + } else if (gapless.IsEnd()) { + // Drain the remaining superframe + std::vector buf(m_codec_info.frameSamples * m_codec_info.channels, 0); + while ((m_num_frames % m_codec_info.framesInSuperframe) != 0) { + ret = Atrac9Decode(m_handle, in_buf.data(), buf.data(), &bytes_used, + True(m_flags & AjmAt9CodecFlags::NonInterleavedOutput)); + in_buf = in_buf.subspan(bytes_used); + m_superframe_bytes_remain -= bytes_used; + result.frames_decoded += 1; + m_num_frames += 1; + } + in_buf = in_buf.subspan(m_superframe_bytes_remain); + m_superframe_bytes_remain = m_codec_info.superframeSize; + m_num_frames = 0; } - return {1, m_codec_info.frameSamples, is_reset}; + return result; } AjmSidebandFormat AjmAt9Decoder::GetFormat() const { @@ -232,7 +258,7 @@ u32 AjmAt9Decoder::GetNextFrameSize(const AjmInstanceGapless& gapless) const { const auto samples = gapless.init.total_samples != 0 ? std::min(gapless.current.total_samples, m_codec_info.frameSamples - skip_samples) - : m_codec_info.frameSamples; + : m_codec_info.frameSamples - skip_samples; return samples * m_codec_info.channels * GetPCMSize(m_format); } diff --git a/src/core/libraries/ajm/ajm_at9.h b/src/core/libraries/ajm/ajm_at9.h index 3262f1aa0..94a718824 100644 --- a/src/core/libraries/ajm/ajm_at9.h +++ b/src/core/libraries/ajm/ajm_at9.h @@ -36,9 +36,10 @@ struct AjmAt9Decoder final : AjmCodec { void Initialize(const void* buffer, u32 buffer_size) override; void GetInfo(void* out_info) const override; AjmSidebandFormat GetFormat() const override; + u32 GetMinimumInputSize() const override; u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const override; - std::tuple ProcessData(std::span& input, SparseOutputBuffer& output, - AjmInstanceGapless& gapless) override; + DecoderResult ProcessData(std::span& input, SparseOutputBuffer& output, + AjmInstanceGapless& gapless) override; private: template diff --git a/src/core/libraries/ajm/ajm_batch.h b/src/core/libraries/ajm/ajm_batch.h index 09daa630d..7f8890898 100644 --- a/src/core/libraries/ajm/ajm_batch.h +++ b/src/core/libraries/ajm/ajm_batch.h @@ -52,6 +52,7 @@ struct AjmBatch { u32 id{}; std::atomic_bool waiting{}; std::atomic_bool canceled{}; + std::atomic_bool processed{}; std::binary_semaphore finished{0}; boost::container::small_vector jobs; diff --git a/src/core/libraries/ajm/ajm_context.cpp b/src/core/libraries/ajm/ajm_context.cpp index 0e2915f32..83d38c5b5 100644 --- a/src/core/libraries/ajm/ajm_context.cpp +++ b/src/core/libraries/ajm/ajm_context.cpp @@ -39,7 +39,12 @@ s32 AjmContext::BatchCancel(const u32 batch_id) { batch = *p_batch; } - batch->canceled = true; + if (batch->processed) { + return ORBIS_OK; + } + + bool expected = false; + batch->canceled.compare_exchange_strong(expected, true); return ORBIS_OK; } @@ -58,7 +63,9 @@ void AjmContext::WorkerThread(std::stop_token stop) { Common::SetCurrentThreadName("shadPS4:AjmWorker"); while (!stop.stop_requested()) { auto batch = batch_queue.PopWait(stop); - if (batch != nullptr) { + if (batch != nullptr && !batch->canceled) { + bool expected = false; + batch->processed.compare_exchange_strong(expected, true); ProcessBatch(batch->id, batch->jobs); batch->finished.release(); } diff --git a/src/core/libraries/ajm/ajm_instance.cpp b/src/core/libraries/ajm/ajm_instance.cpp index c4ea395b9..35685e6a4 100644 --- a/src/core/libraries/ajm/ajm_instance.cpp +++ b/src/core/libraries/ajm/ajm_instance.cpp @@ -1,27 +1,15 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "core/libraries/ajm/ajm_at9.h" -#include "core/libraries/ajm/ajm_instance.h" -#include "core/libraries/ajm/ajm_mp3.h" +#include "ajm_at9.h" +#include "ajm_instance.h" +#include "ajm_mp3.h" +#include "ajm_result.h" #include namespace Libraries::Ajm { -constexpr int ORBIS_AJM_RESULT_NOT_INITIALIZED = 0x00000001; -constexpr int ORBIS_AJM_RESULT_INVALID_DATA = 0x00000002; -constexpr int ORBIS_AJM_RESULT_INVALID_PARAMETER = 0x00000004; -constexpr int ORBIS_AJM_RESULT_PARTIAL_INPUT = 0x00000008; -constexpr int ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010; -constexpr int ORBIS_AJM_RESULT_STREAM_CHANGE = 0x00000020; -constexpr int ORBIS_AJM_RESULT_TOO_MANY_CHANNELS = 0x00000040; -constexpr int ORBIS_AJM_RESULT_UNSUPPORTED_FLAG = 0x00000080; -constexpr int ORBIS_AJM_RESULT_SIDEBAND_TRUNCATED = 0x00000100; -constexpr int ORBIS_AJM_RESULT_PRIORITY_PASSED = 0x00000200; -constexpr int ORBIS_AJM_RESULT_CODEC_ERROR = 0x40000000; -constexpr int ORBIS_AJM_RESULT_FATAL = 0x80000000; - u8 GetPCMSize(AjmFormatEncoding format) { switch (format) { case AjmFormatEncoding::S16: @@ -60,6 +48,7 @@ void AjmInstance::Reset() { void AjmInstance::ExecuteJob(AjmJob& job) { const auto control_flags = job.flags.control_flags; + job.output.p_result->result = 0; if (True(control_flags & AjmJobControlFlags::Reset)) { LOG_TRACE(Lib_Ajm, "Resetting instance {}", job.instance_id); Reset(); @@ -91,8 +80,7 @@ void AjmInstance::ExecuteJob(AjmJob& job) { m_gapless.current.total_samples -= sample_difference; } else { LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_INVALID_PARAMETER"); - job.output.p_result->result = ORBIS_AJM_RESULT_INVALID_PARAMETER; - return; + job.output.p_result->result |= ORBIS_AJM_RESULT_INVALID_PARAMETER; } } @@ -106,61 +94,59 @@ void AjmInstance::ExecuteJob(AjmJob& job) { m_gapless.current.skip_samples -= sample_difference; } else { LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_INVALID_PARAMETER"); - job.output.p_result->result = ORBIS_AJM_RESULT_INVALID_PARAMETER; - return; + job.output.p_result->result |= ORBIS_AJM_RESULT_INVALID_PARAMETER; } } } - if (!job.input.buffer.empty() && !job.output.buffers.empty()) { - std::span in_buf(job.input.buffer); - SparseOutputBuffer out_buf(job.output.buffers); + std::span in_buf(job.input.buffer); + SparseOutputBuffer out_buf(job.output.buffers); + auto in_size = in_buf.size(); + auto out_size = out_buf.Size(); + u32 frames_decoded = 0; - u32 frames_decoded = 0; - auto in_size = in_buf.size(); - auto out_size = out_buf.Size(); - while (!in_buf.empty() && !out_buf.IsEmpty() && !m_gapless.IsEnd()) { - if (!HasEnoughSpace(out_buf)) { - if (job.output.p_mframe == nullptr || frames_decoded == 0) { - LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM ({} < {})", - out_buf.Size(), m_codec->GetNextFrameSize(m_gapless)); - job.output.p_result->result = ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM; - break; - } - } - - const auto [nframes, nsamples, reset] = - m_codec->ProcessData(in_buf, out_buf, m_gapless); - if (reset) { + if (!job.input.buffer.empty()) { + for (;;) { + if (m_flags.gapless_loop && m_gapless.IsEnd()) { + m_gapless.Reset(); m_total_samples = 0; } - if (!nframes) { - LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_NOT_INITIALIZED"); - job.output.p_result->result = ORBIS_AJM_RESULT_NOT_INITIALIZED; + if (!HasEnoughSpace(out_buf)) { + LOG_TRACE(Lib_Ajm, "ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM ({} < {})", out_buf.Size(), + m_codec->GetNextFrameSize(m_gapless)); + job.output.p_result->result |= ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM; + } + if (in_buf.size() < m_codec->GetMinimumInputSize()) { + job.output.p_result->result |= ORBIS_AJM_RESULT_PARTIAL_INPUT; + } + if (job.output.p_result->result != 0) { + break; + } + const auto result = m_codec->ProcessData(in_buf, out_buf, m_gapless); + if (result.is_reset) { + m_total_samples = 0; + } else { + m_total_samples += result.samples_written; + } + frames_decoded += result.frames_decoded; + if (result.result != 0) { + job.output.p_result->result |= result.result; + job.output.p_result->internal_result = result.internal_result; break; } - frames_decoded += nframes; - m_total_samples += nsamples; - if (False(job.flags.run_flags & AjmJobRunFlags::MultipleFrames)) { break; } } + } - const auto total_decoded_samples = m_total_samples; - if (m_flags.gapless_loop && m_gapless.IsEnd()) { - in_buf = in_buf.subspan(in_buf.size()); - m_gapless.Reset(); - m_codec->Reset(); - } - if (job.output.p_mframe) { - job.output.p_mframe->num_frames = frames_decoded; - } - if (job.output.p_stream) { - job.output.p_stream->input_consumed = in_size - in_buf.size(); - job.output.p_stream->output_written = out_size - out_buf.Size(); - job.output.p_stream->total_decoded_samples = total_decoded_samples; - } + if (job.output.p_mframe) { + job.output.p_mframe->num_frames = frames_decoded; + } + if (job.output.p_stream) { + job.output.p_stream->input_consumed = in_size - in_buf.size(); + job.output.p_stream->output_written = out_size - out_buf.Size(); + job.output.p_stream->total_decoded_samples = m_total_samples; } if (job.output.p_format != nullptr) { @@ -175,6 +161,9 @@ void AjmInstance::ExecuteJob(AjmJob& job) { } bool AjmInstance::HasEnoughSpace(const SparseOutputBuffer& output) const { + if (m_gapless.IsEnd()) { + return true; + } return output.Size() >= m_codec->GetNextFrameSize(m_gapless); } diff --git a/src/core/libraries/ajm/ajm_instance.h b/src/core/libraries/ajm/ajm_instance.h index ad0a82f29..db53add4d 100644 --- a/src/core/libraries/ajm/ajm_instance.h +++ b/src/core/libraries/ajm/ajm_instance.h @@ -73,6 +73,14 @@ struct AjmInstanceGapless { } }; +struct DecoderResult { + s32 result = 0; + s32 internal_result = 0; + u32 frames_decoded = 0; + u32 samples_written = 0; + bool is_reset = false; +}; + class AjmCodec { public: virtual ~AjmCodec() = default; @@ -81,9 +89,10 @@ public: virtual void Reset() = 0; virtual void GetInfo(void* out_info) const = 0; virtual AjmSidebandFormat GetFormat() const = 0; + virtual u32 GetMinimumInputSize() const = 0; virtual u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const = 0; - virtual std::tuple ProcessData(std::span& input, SparseOutputBuffer& output, - AjmInstanceGapless& gapless) = 0; + virtual DecoderResult ProcessData(std::span& input, SparseOutputBuffer& output, + AjmInstanceGapless& gapless) = 0; }; class AjmInstance { @@ -94,7 +103,6 @@ public: private: bool HasEnoughSpace(const SparseOutputBuffer& output) const; - std::optional GetNumRemainingSamples() const; void Reset(); AjmInstanceFlags m_flags{}; diff --git a/src/core/libraries/ajm/ajm_instance_statistics.cpp b/src/core/libraries/ajm/ajm_instance_statistics.cpp index c0c1af8bb..2e4a65914 100644 --- a/src/core/libraries/ajm/ajm_instance_statistics.cpp +++ b/src/core/libraries/ajm/ajm_instance_statistics.cpp @@ -8,7 +8,7 @@ namespace Libraries::Ajm { void AjmInstanceStatistics::ExecuteJob(AjmJob& job) { if (job.output.p_engine) { - job.output.p_engine->usage_batch = 0.01; + job.output.p_engine->usage_batch = 0.05; const auto ic = job.input.statistics_engine_parameters->interval_count; for (u32 idx = 0; idx < ic; ++idx) { job.output.p_engine->usage_interval[idx] = 0.01; @@ -25,10 +25,12 @@ void AjmInstanceStatistics::ExecuteJob(AjmJob& job) { job.output.p_memory->batch_size = 0x4200; job.output.p_memory->input_size = 0x2000; job.output.p_memory->output_size = 0x2000; - job.output.p_memory->small_size = 0x200; + job.output.p_memory->small_size = 0x400; } } +void AjmInstanceStatistics::Reset() {} + AjmInstanceStatistics& AjmInstanceStatistics::Getinstance() { static AjmInstanceStatistics instance; return instance; diff --git a/src/core/libraries/ajm/ajm_instance_statistics.h b/src/core/libraries/ajm/ajm_instance_statistics.h index ea70c9d56..0ec79aeac 100644 --- a/src/core/libraries/ajm/ajm_instance_statistics.h +++ b/src/core/libraries/ajm/ajm_instance_statistics.h @@ -10,6 +10,7 @@ namespace Libraries::Ajm { class AjmInstanceStatistics { public: void ExecuteJob(AjmJob& job); + void Reset(); static AjmInstanceStatistics& Getinstance(); }; diff --git a/src/core/libraries/ajm/ajm_mp3.cpp b/src/core/libraries/ajm/ajm_mp3.cpp index f17f53d51..f8d77f031 100644 --- a/src/core/libraries/ajm/ajm_mp3.cpp +++ b/src/core/libraries/ajm/ajm_mp3.cpp @@ -122,6 +122,7 @@ 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 { @@ -138,16 +139,28 @@ void AjmMp3Decoder::GetInfo(void* out_info) const { } } -std::tuple AjmMp3Decoder::ProcessData(std::span& in_buf, - SparseOutputBuffer& output, - AjmInstanceGapless& gapless) { +u32 AjmMp3Decoder::GetMinimumInputSize() const { + // 4 bytes is for mp3 header that contains frame_size + return std::max(m_frame_size, 4); +} + +DecoderResult AjmMp3Decoder::ProcessData(std::span& in_buf, SparseOutputBuffer& output, + AjmInstanceGapless& gapless) { + 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(), false, &info); + ParseMp3Header(in_buf.data(), in_buf.size(), true, &info); m_frame_samples = info.samples_per_channel; + m_frame_size = info.frame_size; + gapless.init = { + .total_samples = info.total_samples, + .skip_samples = static_cast(info.encoder_delay), + .skipped_samples = 0, + }; + gapless.current = gapless.init; } int ret = av_parser_parse2(m_parser, m_codec_context, &pkt->data, &pkt->size, in_buf.data(), @@ -155,9 +168,6 @@ std::tuple AjmMp3Decoder::ProcessData(std::span& in_buf, ASSERT_MSG(ret >= 0, "Error while parsing {}", ret); in_buf = in_buf.subspan(ret); - u32 frames_decoded = 0; - u32 samples_decoded = 0; - if (pkt->size) { // Send the packet with the compressed data to the decoder pkt->pts = m_parser->pts; @@ -177,9 +187,8 @@ std::tuple AjmMp3Decoder::ProcessData(std::span& in_buf, UNREACHABLE_MSG("Error during decoding"); } frame = ConvertAudioFrame(frame); - samples_decoded += u32(frame->nb_samples); - frames_decoded += 1; + result.frames_decoded += 1; u32 skip_samples = 0; if (gapless.current.skip_samples > 0) { skip_samples = std::min(u16(frame->nb_samples), gapless.current.skip_samples); @@ -211,6 +220,7 @@ std::tuple AjmMp3Decoder::ProcessData(std::span& in_buf, if (gapless.init.total_samples != 0) { gapless.current.total_samples -= samples; } + result.samples_written += samples; av_frame_free(&frame); } @@ -218,16 +228,16 @@ std::tuple AjmMp3Decoder::ProcessData(std::span& in_buf, av_packet_free(&pkt); - return {frames_decoded, samples_decoded, false}; + return result; } u32 AjmMp3Decoder::GetNextFrameSize(const AjmInstanceGapless& gapless) const { - const auto max_samples = gapless.init.total_samples != 0 - ? std::min(gapless.current.total_samples, m_frame_samples) - : m_frame_samples; - const auto skip_samples = std::min(u32(gapless.current.skip_samples), max_samples); - return (max_samples - skip_samples) * m_codec_context->ch_layout.nb_channels * - GetPCMSize(m_format); + const auto skip_samples = std::min(gapless.current.skip_samples, m_frame_samples); + const auto samples = + gapless.init.total_samples != 0 + ? std::min(gapless.current.total_samples, m_frame_samples - skip_samples) + : m_frame_samples - skip_samples; + return samples * m_codec_context->ch_layout.nb_channels * GetPCMSize(m_format); } class BitReader { @@ -264,7 +274,7 @@ private: int AjmMp3Decoder::ParseMp3Header(const u8* p_begin, u32 stream_size, int parse_ofl, AjmDecMp3ParseFrame* frame) { - LOG_INFO(Lib_Ajm, "called stream_size = {} parse_ofl = {}", stream_size, parse_ofl); + LOG_TRACE(Lib_Ajm, "called stream_size = {} parse_ofl = {}", stream_size, parse_ofl); if (p_begin == nullptr || stream_size < 4 || frame == nullptr) { return ORBIS_AJM_ERROR_INVALID_PARAMETER; diff --git a/src/core/libraries/ajm/ajm_mp3.h b/src/core/libraries/ajm/ajm_mp3.h index 7ac65fdba..c03d5ba15 100644 --- a/src/core/libraries/ajm/ajm_mp3.h +++ b/src/core/libraries/ajm/ajm_mp3.h @@ -70,9 +70,10 @@ public: void Initialize(const void* buffer, u32 buffer_size) override {} void GetInfo(void* out_info) const override; AjmSidebandFormat GetFormat() const override; + u32 GetMinimumInputSize() const override; u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const override; - std::tuple ProcessData(std::span& input, SparseOutputBuffer& output, - AjmInstanceGapless& gapless) override; + DecoderResult ProcessData(std::span& input, SparseOutputBuffer& output, + AjmInstanceGapless& gapless) override; static int ParseMp3Header(const u8* buf, u32 stream_size, int parse_ofl, AjmDecMp3ParseFrame* frame); @@ -97,6 +98,7 @@ private: SwrContext* m_swr_context = nullptr; std::optional m_header; u32 m_frame_samples = 0; + u32 m_frame_size = 0; }; } // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_result.h b/src/core/libraries/ajm/ajm_result.h new file mode 100644 index 000000000..d4e6d1147 --- /dev/null +++ b/src/core/libraries/ajm/ajm_result.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +constexpr int ORBIS_AJM_RESULT_NOT_INITIALIZED = 0x00000001; +constexpr int ORBIS_AJM_RESULT_INVALID_DATA = 0x00000002; +constexpr int ORBIS_AJM_RESULT_INVALID_PARAMETER = 0x00000004; +constexpr int ORBIS_AJM_RESULT_PARTIAL_INPUT = 0x00000008; +constexpr int ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010; +constexpr int ORBIS_AJM_RESULT_STREAM_CHANGE = 0x00000020; +constexpr int ORBIS_AJM_RESULT_TOO_MANY_CHANNELS = 0x00000040; +constexpr int ORBIS_AJM_RESULT_UNSUPPORTED_FLAG = 0x00000080; +constexpr int ORBIS_AJM_RESULT_SIDEBAND_TRUNCATED = 0x00000100; +constexpr int ORBIS_AJM_RESULT_PRIORITY_PASSED = 0x00000200; +constexpr int ORBIS_AJM_RESULT_CODEC_ERROR = 0x40000000; +constexpr int ORBIS_AJM_RESULT_FATAL = 0x80000000; From c414d1f5a174601d8b3d07f9afd75c106269685c Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 24 Dec 2025 10:19:09 +0200 Subject: [PATCH 12/83] tagged 0.13.0 release --- 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 04534ec26..efadf22f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,13 +202,13 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") -set(EMULATOR_VERSION_MINOR "12") -set(EMULATOR_VERSION_PATCH "6") +set(EMULATOR_VERSION_MINOR "13") +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 c85fcf003..f42840e9b 100644 --- a/dist/net.shadps4.shadPS4.metainfo.xml +++ b/dist/net.shadps4.shadPS4.metainfo.xml @@ -37,6 +37,9 @@ Game + + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.13.0 + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.12.5 From aa227cae579cc9ec7e934e665bb201dc3c7b6f5e Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 24 Dec 2025 10:46:40 +0200 Subject: [PATCH 13/83] started 0.13.1 WIP --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index efadf22f0..e838030bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,12 +203,12 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") set(EMULATOR_VERSION_MINOR "13") -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 9a3e4ea56b10e5ca02b644178b65abef4ddb25f8 Mon Sep 17 00:00:00 2001 From: Vladislav Mikhalin Date: Sun, 28 Dec 2025 14:24:42 +0300 Subject: [PATCH 14/83] ajm: support for m4aac (#3880) * ajm m4aac * fix build on unix * small tunes * skip 2 frames if nodelay is not set, change to google repo --- .gitmodules | 3 + CMakeLists.txt | 4 +- externals/CMakeLists.txt | 4 + externals/aacdec/CMakeLists.txt | 154 +++++++++++++++++++ externals/aacdec/fdk-aac | 1 + src/core/libraries/ajm/ajm.cpp | 5 +- src/core/libraries/ajm/ajm.h | 4 +- src/core/libraries/ajm/ajm_aac.cpp | 194 ++++++++++++++++++++++++ src/core/libraries/ajm/ajm_aac.h | 73 +++++++++ src/core/libraries/ajm/ajm_at9.cpp | 2 +- src/core/libraries/ajm/ajm_at9.h | 5 +- src/core/libraries/ajm/ajm_batch.cpp | 2 +- src/core/libraries/ajm/ajm_context.cpp | 11 +- src/core/libraries/ajm/ajm_instance.cpp | 14 +- src/core/libraries/ajm/ajm_mp3.cpp | 5 +- src/core/libraries/ajm/ajm_mp3.h | 3 +- 16 files changed, 460 insertions(+), 24 deletions(-) create mode 100644 externals/aacdec/CMakeLists.txt create mode 160000 externals/aacdec/fdk-aac create mode 100644 src/core/libraries/ajm/ajm_aac.cpp create mode 100644 src/core/libraries/ajm/ajm_aac.h diff --git a/.gitmodules b/.gitmodules index c5d05edd3..c0ba5e79d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -120,3 +120,6 @@ [submodule "externals/miniz"] path = externals/miniz url = https://github.com/richgel999/miniz +[submodule "externals/aacdec/fdk-aac"] + path = externals/aacdec/fdk-aac + url = https://android.googlesource.com/platform/external/aac diff --git a/CMakeLists.txt b/CMakeLists.txt index e838030bf..99aca5268 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -262,6 +262,8 @@ include_directories(src) set(AJM_LIB src/core/libraries/ajm/ajm.cpp src/core/libraries/ajm/ajm.h + src/core/libraries/ajm/ajm_aac.cpp + src/core/libraries/ajm/ajm_aac.h src/core/libraries/ajm/ajm_at9.cpp src/core/libraries/ajm/ajm_at9.h src/core/libraries/ajm/ajm_batch.cpp @@ -1085,7 +1087,7 @@ create_target_directory_groups(shadps4) target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG) target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 SDL3_mixer::SDL3_mixer pugixml::pugixml) -target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz) +target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz fdk-aac) target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index eb3723f2c..8e96f9bec 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -258,6 +258,10 @@ if (WIN32) add_subdirectory(ext-wepoll) endif() +if (NOT TARGET fdk-aac) +add_subdirectory(aacdec) +endif() + #nlohmann json set(JSON_BuildTests OFF CACHE INTERNAL "") add_subdirectory(json) diff --git a/externals/aacdec/CMakeLists.txt b/externals/aacdec/CMakeLists.txt new file mode 100644 index 000000000..2adfa032b --- /dev/null +++ b/externals/aacdec/CMakeLists.txt @@ -0,0 +1,154 @@ +# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +set(AACDEC_SRC + fdk-aac/libAACdec/src/FDK_delay.cpp + fdk-aac/libAACdec/src/aac_ram.cpp + fdk-aac/libAACdec/src/aac_rom.cpp + fdk-aac/libAACdec/src/aacdec_drc.cpp + fdk-aac/libAACdec/src/aacdec_hcr.cpp + fdk-aac/libAACdec/src/aacdec_hcr_bit.cpp + fdk-aac/libAACdec/src/aacdec_hcrs.cpp + fdk-aac/libAACdec/src/aacdec_pns.cpp + fdk-aac/libAACdec/src/aacdec_tns.cpp + fdk-aac/libAACdec/src/aacdecoder.cpp + fdk-aac/libAACdec/src/aacdecoder_lib.cpp + fdk-aac/libAACdec/src/block.cpp + fdk-aac/libAACdec/src/channel.cpp + fdk-aac/libAACdec/src/channelinfo.cpp + fdk-aac/libAACdec/src/conceal.cpp + fdk-aac/libAACdec/src/ldfiltbank.cpp + fdk-aac/libAACdec/src/pulsedata.cpp + fdk-aac/libAACdec/src/rvlc.cpp + fdk-aac/libAACdec/src/rvlcbit.cpp + fdk-aac/libAACdec/src/rvlcconceal.cpp + fdk-aac/libAACdec/src/stereo.cpp + fdk-aac/libAACdec/src/usacdec_ace_d4t64.cpp + fdk-aac/libAACdec/src/usacdec_ace_ltp.cpp + fdk-aac/libAACdec/src/usacdec_acelp.cpp + fdk-aac/libAACdec/src/usacdec_fac.cpp + fdk-aac/libAACdec/src/usacdec_lpc.cpp + fdk-aac/libAACdec/src/usacdec_lpd.cpp + fdk-aac/libAACdec/src/usacdec_rom.cpp +) + +set(FDK_SRC + fdk-aac/libFDK/src/FDK_bitbuffer.cpp + fdk-aac/libFDK/src/FDK_core.cpp + fdk-aac/libFDK/src/FDK_crc.cpp + fdk-aac/libFDK/src/FDK_decorrelate.cpp + fdk-aac/libFDK/src/FDK_hybrid.cpp + fdk-aac/libFDK/src/FDK_lpc.cpp + fdk-aac/libFDK/src/FDK_matrixCalloc.cpp + fdk-aac/libFDK/src/FDK_qmf_domain.cpp + fdk-aac/libFDK/src/FDK_tools_rom.cpp + fdk-aac/libFDK/src/FDK_trigFcts.cpp + fdk-aac/libFDK/src/autocorr2nd.cpp + fdk-aac/libFDK/src/dct.cpp + fdk-aac/libFDK/src/fft.cpp + fdk-aac/libFDK/src/fft_rad2.cpp + fdk-aac/libFDK/src/fixpoint_math.cpp + fdk-aac/libFDK/src/huff_nodes.cpp + fdk-aac/libFDK/src/mdct.cpp + fdk-aac/libFDK/src/nlc_dec.cpp + fdk-aac/libFDK/src/qmf.cpp + fdk-aac/libFDK/src/scale.cpp +) + +set(SYS_SRC + fdk-aac/libSYS/src/genericStds.cpp + fdk-aac/libSYS/src/syslib_channelMapDescr.cpp +) + +set(ARITHCODING_SRC + fdk-aac/libArithCoding/src/ac_arith_coder.cpp +) + +set(MPEGTPDEC_SRC + fdk-aac/libMpegTPDec/src/tpdec_adif.cpp + fdk-aac/libMpegTPDec/src/tpdec_adts.cpp + fdk-aac/libMpegTPDec/src/tpdec_asc.cpp + fdk-aac/libMpegTPDec/src/tpdec_drm.cpp + fdk-aac/libMpegTPDec/src/tpdec_latm.cpp + fdk-aac/libMpegTPDec/src/tpdec_lib.cpp +) + +set(SBRDEC_SRC + fdk-aac/libSBRdec/src/HFgen_preFlat.cpp + fdk-aac/libSBRdec/src/env_calc.cpp + fdk-aac/libSBRdec/src/env_dec.cpp + fdk-aac/libSBRdec/src/env_extr.cpp + fdk-aac/libSBRdec/src/hbe.cpp + fdk-aac/libSBRdec/src/huff_dec.cpp + fdk-aac/libSBRdec/src/lpp_tran.cpp + fdk-aac/libSBRdec/src/psbitdec.cpp + fdk-aac/libSBRdec/src/psdec.cpp + fdk-aac/libSBRdec/src/psdec_drm.cpp + fdk-aac/libSBRdec/src/psdecrom_drm.cpp + fdk-aac/libSBRdec/src/pvc_dec.cpp + fdk-aac/libSBRdec/src/sbr_deb.cpp + fdk-aac/libSBRdec/src/sbr_dec.cpp + fdk-aac/libSBRdec/src/sbr_ram.cpp + fdk-aac/libSBRdec/src/sbr_rom.cpp + fdk-aac/libSBRdec/src/sbrdec_drc.cpp + fdk-aac/libSBRdec/src/sbrdec_freq_sca.cpp + fdk-aac/libSBRdec/src/sbrdecoder.cpp +) + +set(PCMUTILS_SRC + fdk-aac/libPCMutils/src/limiter.cpp + fdk-aac/libPCMutils/src/pcm_utils.cpp + fdk-aac/libPCMutils/src/pcmdmx_lib.cpp +) + +set(DRCDEC_SRC + fdk-aac/libDRCdec/src/FDK_drcDecLib.cpp + fdk-aac/libDRCdec/src/drcDec_gainDecoder.cpp + fdk-aac/libDRCdec/src/drcDec_reader.cpp + fdk-aac/libDRCdec/src/drcDec_rom.cpp + fdk-aac/libDRCdec/src/drcDec_selectionProcess.cpp + fdk-aac/libDRCdec/src/drcDec_tools.cpp + fdk-aac/libDRCdec/src/drcGainDec_init.cpp + fdk-aac/libDRCdec/src/drcGainDec_preprocess.cpp + fdk-aac/libDRCdec/src/drcGainDec_process.cpp +) + +set(SACDEC_SRC + fdk-aac/libSACdec/src/sac_bitdec.cpp + fdk-aac/libSACdec/src/sac_calcM1andM2.cpp + fdk-aac/libSACdec/src/sac_dec.cpp + fdk-aac/libSACdec/src/sac_dec_conceal.cpp + fdk-aac/libSACdec/src/sac_dec_lib.cpp + fdk-aac/libSACdec/src/sac_process.cpp + fdk-aac/libSACdec/src/sac_qmf.cpp + fdk-aac/libSACdec/src/sac_reshapeBBEnv.cpp + fdk-aac/libSACdec/src/sac_rom.cpp + fdk-aac/libSACdec/src/sac_smoothing.cpp + fdk-aac/libSACdec/src/sac_stp.cpp + fdk-aac/libSACdec/src/sac_tsd.cpp +) + +add_library(fdk-aac + ${AACDEC_SRC} + ${FDK_SRC} + ${SYS_SRC} + ${ARITHCODING_SRC} + ${MPEGTPDEC_SRC} + ${SBRDEC_SRC} + ${PCMUTILS_SRC} + ${DRCDEC_SRC} + ${SACDEC_SRC} +) + +target_include_directories(fdk-aac + PUBLIC + fdk-aac/libAACdec/include + fdk-aac/libFDK/include + fdk-aac/libSYS/include + fdk-aac/libArithCoding/include + fdk-aac/libMpegTPDec/include + fdk-aac/libSBRdec/include + fdk-aac/libPCMutils/include + fdk-aac/libDRCdec/include + fdk-aac/libSACdec/include +) diff --git a/externals/aacdec/fdk-aac b/externals/aacdec/fdk-aac new file mode 160000 index 000000000..ee76460ef --- /dev/null +++ b/externals/aacdec/fdk-aac @@ -0,0 +1 @@ +Subproject commit ee76460efbdb147e26d804c798949c23f174460b diff --git a/src/core/libraries/ajm/ajm.cpp b/src/core/libraries/ajm/ajm.cpp index 83620250b..b64bb47fd 100644 --- a/src/core/libraries/ajm/ajm.cpp +++ b/src/core/libraries/ajm/ajm.cpp @@ -144,9 +144,8 @@ int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* p_context_id) { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmInstanceCodecType() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +AjmCodecType PS4_SYSV_ABI sceAjmInstanceCodecType(u32 instance_id) { + return static_cast((instance_id >> 14) & 0x1F); } int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context_id, AjmCodecType codec_type, diff --git a/src/core/libraries/ajm/ajm.h b/src/core/libraries/ajm/ajm.h index d68a4c0f4..1bfd88351 100644 --- a/src/core/libraries/ajm/ajm.h +++ b/src/core/libraries/ajm/ajm.h @@ -82,8 +82,6 @@ enum class AjmStatisticsFlags : u64 { DECLARE_ENUM_FLAG_OPERATORS(AjmStatisticsFlags) union AjmStatisticsJobFlags { - AjmStatisticsJobFlags(AjmJobFlags job_flags) : raw(job_flags.raw) {} - u64 raw; struct { u64 version : 3; @@ -217,7 +215,7 @@ int PS4_SYSV_ABI sceAjmDecMp3ParseFrame(const u8* stream, u32 stream_size, int p AjmDecMp3ParseFrame* frame); int PS4_SYSV_ABI sceAjmFinalize(); int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* out_context); -int PS4_SYSV_ABI sceAjmInstanceCodecType(); +AjmCodecType PS4_SYSV_ABI sceAjmInstanceCodecType(u32 instance_id); int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context, AjmCodecType codec_type, AjmInstanceFlags flags, u32* instance); int PS4_SYSV_ABI sceAjmInstanceDestroy(u32 context, u32 instance); diff --git a/src/core/libraries/ajm/ajm_aac.cpp b/src/core/libraries/ajm/ajm_aac.cpp new file mode 100644 index 000000000..b96394b72 --- /dev/null +++ b/src/core/libraries/ajm/ajm_aac.cpp @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "ajm.h" +#include "ajm_aac.h" +#include "ajm_result.h" + +// using this internal header to manually configure the decoder in RAW mode +#include "externals/aacdec/fdk-aac/libAACdec/src/aacdecoder.h" + +#include +#include + +namespace Libraries::Ajm { + +AjmAacDecoder::AjmAacDecoder(AjmFormatEncoding format, AjmAacCodecFlags flags, u32 channels) + : m_format(format), m_flags(flags), m_channels(channels), m_pcm_buffer(2048 * 8), + m_skip_frames(True(flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2) {} + +AjmAacDecoder::~AjmAacDecoder() { + aacDecoder_Close(m_decoder); +} + +TRANSPORT_TYPE TransportTypeFromConfigType(ConfigType config_type) { + switch (config_type) { + case ConfigType::ADTS: + return TT_MP4_ADTS; + case ConfigType::RAW: + return TT_MP4_RAW; + default: + UNREACHABLE(); + } +} + +static UINT g_freq[] = { + 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, +}; + +void AjmAacDecoder::Reset() { + if (m_decoder) { + aacDecoder_Close(m_decoder); + } + + m_decoder = aacDecoder_Open(TransportTypeFromConfigType(m_init_params.config_type), 1); + if (m_init_params.config_type == ConfigType::RAW) { + // Manually configure the decoder + // Things may be incorrect due to limited documentation + CSAudioSpecificConfig asc{}; + asc.m_aot = AOT_AAC_LC; + asc.m_samplingFrequency = g_freq[m_init_params.sampling_freq_type]; + asc.m_samplingFrequencyIndex = m_init_params.sampling_freq_type; + asc.m_samplesPerFrame = 1024; + asc.m_epConfig = -1; + switch (m_channels) { + case 0: + asc.m_channelConfiguration = 2; + break; + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + asc.m_channelConfiguration = m_channels; + break; + case 7: + asc.m_channelConfiguration = 11; + break; + case 8: + asc.m_channelConfiguration = 12; // 7, 12 or 14 ? + break; + default: + UNREACHABLE(); + } + + UCHAR changed = 1; + CAacDecoder_Init(m_decoder, &asc, AC_CM_ALLOC_MEM, &changed); + } + m_skip_frames = True(m_flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2; +} + +void AjmAacDecoder::Initialize(const void* buffer, u32 buffer_size) { + ASSERT(buffer_size == 8); + m_init_params = *reinterpret_cast(buffer); + Reset(); +} + +void AjmAacDecoder::GetInfo(void* out_info) const { + auto* codec_info = reinterpret_cast(out_info); + *codec_info = { + .heaac = True(m_flags & AjmAacCodecFlags::EnableSbrDecode), + }; +} + +AjmSidebandFormat AjmAacDecoder::GetFormat() const { + const auto* const info = aacDecoder_GetStreamInfo(m_decoder); + return { + .num_channels = static_cast(info->numChannels), + .channel_mask = GetChannelMask(info->numChannels), + .sampl_freq = static_cast(info->sampleRate), + .sample_encoding = m_format, // AjmFormatEncoding + .bitrate = static_cast(info->bitRate), + }; +} + +u32 AjmAacDecoder::GetMinimumInputSize() const { + return 0; +} + +u32 AjmAacDecoder::GetNextFrameSize(const AjmInstanceGapless& gapless) const { + const auto* const info = aacDecoder_GetStreamInfo(m_decoder); + if (info->aacSamplesPerFrame <= 0) { + return 0; + } + const auto skip_samples = std::min(gapless.current.skip_samples, info->frameSize); + const auto samples = + gapless.init.total_samples != 0 + ? std::min(gapless.current.total_samples, info->frameSize - skip_samples) + : info->frameSize - skip_samples; + return samples * info->numChannels * GetPCMSize(m_format); +} + +DecoderResult AjmAacDecoder::ProcessData(std::span& input, SparseOutputBuffer& output, + AjmInstanceGapless& gapless) { + DecoderResult result{}; + + // Discard the previous contents of the internal buffer and replace them with new ones + aacDecoder_SetParam(m_decoder, AAC_TPDEC_CLEAR_BUFFER, 1); + UCHAR* buffers[] = {input.data()}; + const UINT sizes[] = {static_cast(input.size())}; + UINT valid = sizes[0]; + aacDecoder_Fill(m_decoder, buffers, sizes, &valid); + auto ret = aacDecoder_DecodeFrame(m_decoder, reinterpret_cast(m_pcm_buffer.data()), + m_pcm_buffer.size() / 2, 0); + + switch (ret) { + case AAC_DEC_OK: + break; + case AAC_DEC_NOT_ENOUGH_BITS: + result.result = ORBIS_AJM_RESULT_PARTIAL_INPUT; + return result; + default: + LOG_ERROR(Lib_Ajm, "aacDecoder_DecodeFrame failed ret = {:#x}", static_cast(ret)); + result.result = ORBIS_AJM_RESULT_CODEC_ERROR | ORBIS_AJM_RESULT_FATAL; + result.internal_result = ret; + return result; + } + + const auto* const info = aacDecoder_GetStreamInfo(m_decoder); + auto bytes_used = info->numTotalBytes; + + result.frames_decoded += 1; + input = input.subspan(bytes_used); + + if (m_skip_frames > 0) { + --m_skip_frames; + return result; + } + + u32 skip_samples = 0; + if (gapless.current.skip_samples > 0) { + skip_samples = std::min(info->frameSize, gapless.current.skip_samples); + gapless.current.skip_samples -= skip_samples; + } + + const auto max_samples = + gapless.init.total_samples != 0 ? gapless.current.total_samples : info->aacSamplesPerFrame; + + size_t pcm_written = 0; + switch (m_format) { + case AjmFormatEncoding::S16: + pcm_written = WriteOutputSamples(output, skip_samples * info->numChannels, + max_samples * info->numChannels); + break; + case AjmFormatEncoding::S32: + UNREACHABLE_MSG("NOT IMPLEMENTED"); + break; + case AjmFormatEncoding::Float: + UNREACHABLE_MSG("NOT IMPLEMENTED"); + break; + default: + UNREACHABLE(); + } + + result.samples_written = pcm_written / info->numChannels; + gapless.current.skipped_samples += info->frameSize - result.samples_written; + if (gapless.init.total_samples != 0) { + gapless.current.total_samples -= result.samples_written; + } + + return result; +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_aac.h b/src/core/libraries/ajm/ajm_aac.h new file mode 100644 index 000000000..7ca8ecbf8 --- /dev/null +++ b/src/core/libraries/ajm/ajm_aac.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/enum.h" +#include "common/types.h" +#include "core/libraries/ajm/ajm_instance.h" + +#include +#include + +struct AAC_DECODER_INSTANCE; + +namespace Libraries::Ajm { + +enum ConfigType : u32 { + ADTS = 1, + RAW = 2, +}; + +enum AjmAacCodecFlags : u32 { + EnableSbrDecode = 1 << 0, + EnableNondelayOutput = 1 << 1, + SurroundChannelInterleaveOrderExtlExtrLsRs = 1 << 2, + SurroundChannelInterleaveOrderLsRsExtlExtr = 1 << 3, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmAacCodecFlags) + +struct AjmSidebandDecM4aacCodecInfo { + u32 heaac; + u32 reserved; +}; + +struct AjmAacDecoder final : AjmCodec { + explicit AjmAacDecoder(AjmFormatEncoding format, AjmAacCodecFlags flags, u32 channels); + ~AjmAacDecoder() override; + + void Reset() override; + void Initialize(const void* buffer, u32 buffer_size) override; + void GetInfo(void* out_info) const override; + AjmSidebandFormat GetFormat() const override; + u32 GetMinimumInputSize() const override; + u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const override; + DecoderResult ProcessData(std::span& input, SparseOutputBuffer& output, + AjmInstanceGapless& gapless) override; + +private: + struct InitializeParameters { + ConfigType config_type; + u32 sampling_freq_type; + }; + + template + size_t WriteOutputSamples(SparseOutputBuffer& output, u32 skipped_pcm, u32 max_pcm) { + std::span pcm_data{reinterpret_cast(m_pcm_buffer.data()), + m_pcm_buffer.size() / sizeof(T)}; + pcm_data = pcm_data.subspan(skipped_pcm); + const auto pcm_size = std::min(u32(pcm_data.size()), max_pcm); + return output.Write(pcm_data.subspan(0, pcm_size)); + } + + const AjmFormatEncoding m_format; + const AjmAacCodecFlags m_flags; + const u32 m_channels; + std::vector m_pcm_buffer; + + u32 m_skip_frames = 0; + InitializeParameters m_init_params = {}; + AAC_DECODER_INSTANCE* m_decoder = nullptr; +}; + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_at9.cpp b/src/core/libraries/ajm/ajm_at9.cpp index ea7add4f3..4452d032d 100644 --- a/src/core/libraries/ajm/ajm_at9.cpp +++ b/src/core/libraries/ajm/ajm_at9.cpp @@ -54,7 +54,7 @@ struct RIFFHeader { }; static_assert(sizeof(RIFFHeader) == 12); -AjmAt9Decoder::AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags) +AjmAt9Decoder::AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags, u32) : m_format(format), m_flags(flags), m_handle(Atrac9GetHandle()) {} AjmAt9Decoder::~AjmAt9Decoder() { diff --git a/src/core/libraries/ajm/ajm_at9.h b/src/core/libraries/ajm/ajm_at9.h index 94a718824..8eb6166e2 100644 --- a/src/core/libraries/ajm/ajm_at9.h +++ b/src/core/libraries/ajm/ajm_at9.h @@ -3,6 +3,7 @@ #pragma once +#include "common/enum.h" #include "common/types.h" #include "core/libraries/ajm/ajm_instance.h" @@ -13,8 +14,6 @@ namespace Libraries::Ajm { -constexpr s32 ORBIS_AJM_DEC_AT9_MAX_CHANNELS = 8; - enum AjmAt9CodecFlags : u32 { ParseRiffHeader = 1 << 0, NonInterleavedOutput = 1 << 8, @@ -29,7 +28,7 @@ struct AjmSidebandDecAt9CodecInfo { }; struct AjmAt9Decoder final : AjmCodec { - explicit AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags); + explicit AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags, u32 channels); ~AjmAt9Decoder() override; void Reset() override; diff --git a/src/core/libraries/ajm/ajm_batch.cpp b/src/core/libraries/ajm/ajm_batch.cpp index 30e1deb71..61f80e482 100644 --- a/src/core/libraries/ajm/ajm_batch.cpp +++ b/src/core/libraries/ajm/ajm_batch.cpp @@ -165,7 +165,7 @@ AjmJob AjmStatisticsJobFromBatchBuffer(u32 instance_id, AjmBatchBuffer batch_buf ASSERT(job_flags.has_value()); job.flags = job_flags.value(); - AjmStatisticsJobFlags flags(job.flags); + AjmStatisticsJobFlags flags{.raw = job.flags.raw}; if (input_control_buffer.has_value()) { AjmBatchBuffer input_batch(input_control_buffer.value()); if (True(flags.statistics_flags & AjmStatisticsFlags::Engine)) { diff --git a/src/core/libraries/ajm/ajm_context.cpp b/src/core/libraries/ajm/ajm_context.cpp index 83d38c5b5..8ce8f3434 100644 --- a/src/core/libraries/ajm/ajm_context.cpp +++ b/src/core/libraries/ajm/ajm_context.cpp @@ -18,7 +18,8 @@ namespace Libraries::Ajm { -static constexpr u32 ORBIS_AJM_WAIT_INFINITE = -1; +constexpr u32 ORBIS_AJM_WAIT_INFINITE = -1; +constexpr int INSTANCE_ID_MASK = 0x3FFF; AjmContext::AjmContext() { worker_thread = std::jthread([this](std::stop_token stop) { this->WorkerThread(stop); }); @@ -84,7 +85,7 @@ void AjmContext::ProcessBatch(u32 id, std::span jobs) { std::shared_ptr instance; { std::shared_lock lock(instances_mutex); - auto* p_instance = instances.Get(job.instance_id); + auto* p_instance = instances.Get(job.instance_id & INSTANCE_ID_MASK); ASSERT_MSG(p_instance != nullptr, "Attempting to execute job on null instance"); instance = *p_instance; } @@ -176,15 +177,15 @@ s32 AjmContext::InstanceCreate(AjmCodecType codec_type, AjmInstanceFlags flags, if (!opt_index.has_value()) { return ORBIS_AJM_ERROR_OUT_OF_RESOURCES; } - *out_instance = opt_index.value(); + *out_instance = opt_index.value() | (static_cast(codec_type) << 14); LOG_INFO(Lib_Ajm, "instance = {}", *out_instance); return ORBIS_OK; } -s32 AjmContext::InstanceDestroy(u32 instance) { +s32 AjmContext::InstanceDestroy(u32 instance_id) { std::unique_lock lock(instances_mutex); - if (!instances.Destroy(instance)) { + if (!instances.Destroy(instance_id & INSTANCE_ID_MASK)) { return ORBIS_AJM_ERROR_INVALID_INSTANCE; } return ORBIS_OK; diff --git a/src/core/libraries/ajm/ajm_instance.cpp b/src/core/libraries/ajm/ajm_instance.cpp index 35685e6a4..d25517c81 100644 --- a/src/core/libraries/ajm/ajm_instance.cpp +++ b/src/core/libraries/ajm/ajm_instance.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "ajm_aac.h" #include "ajm_at9.h" #include "ajm_instance.h" #include "ajm_mp3.h" @@ -26,13 +27,18 @@ u8 GetPCMSize(AjmFormatEncoding format) { AjmInstance::AjmInstance(AjmCodecType codec_type, AjmInstanceFlags flags) : m_flags(flags) { switch (codec_type) { case AjmCodecType::At9Dec: { - m_codec = std::make_unique(AjmFormatEncoding(flags.format), - AjmAt9CodecFlags(flags.codec)); + m_codec = std::make_unique( + AjmFormatEncoding(flags.format), AjmAt9CodecFlags(flags.codec), u32(flags.channels)); break; } case AjmCodecType::Mp3Dec: { - m_codec = std::make_unique(AjmFormatEncoding(flags.format), - AjmMp3CodecFlags(flags.codec)); + m_codec = std::make_unique( + AjmFormatEncoding(flags.format), AjmMp3CodecFlags(flags.codec), u32(flags.channels)); + break; + } + case AjmCodecType::M4aacDec: { + m_codec = std::make_unique( + AjmFormatEncoding(flags.format), AjmAacCodecFlags(flags.codec), u32(flags.channels)); break; } default: diff --git a/src/core/libraries/ajm/ajm_mp3.cpp b/src/core/libraries/ajm/ajm_mp3.cpp index f8d77f031..f4ce22b8b 100644 --- a/src/core/libraries/ajm/ajm_mp3.cpp +++ b/src/core/libraries/ajm/ajm_mp3.cpp @@ -105,7 +105,7 @@ AVFrame* AjmMp3Decoder::ConvertAudioFrame(AVFrame* frame) { return new_frame; } -AjmMp3Decoder::AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags) +AjmMp3Decoder::AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags, u32) : m_format(format), m_flags(flags), m_codec(avcodec_find_decoder(AV_CODEC_ID_MP3)), m_codec_context(avcodec_alloc_context3(m_codec)), m_parser(av_parser_init(m_codec->id)) { int ret = avcodec_open2(m_codec_context, m_codec, nullptr); @@ -311,7 +311,8 @@ int AjmMp3Decoder::ParseMp3Header(const u8* p_begin, u32 stream_size, int parse_ BitReader reader(p_current); if (header->protection_type == 0) { - reader.Skip(16); // crc = reader.Read(16); + // crc = reader.Read(16); + reader.Skip(16); } if (header->version == Mp3AudioVersion::V1) { diff --git a/src/core/libraries/ajm/ajm_mp3.h b/src/core/libraries/ajm/ajm_mp3.h index c03d5ba15..ecbc77051 100644 --- a/src/core/libraries/ajm/ajm_mp3.h +++ b/src/core/libraries/ajm/ajm_mp3.h @@ -3,6 +3,7 @@ #pragma once +#include "common/enum.h" #include "common/types.h" #include "core/libraries/ajm/ajm_instance.h" @@ -63,7 +64,7 @@ struct AjmSidebandDecMp3CodecInfo { class AjmMp3Decoder : public AjmCodec { public: - explicit AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags); + explicit AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags, u32 channels); ~AjmMp3Decoder() override; void Reset() override; From 954cc77110535ce777fa8be5ce5aea813d75b8f3 Mon Sep 17 00:00:00 2001 From: Connor Garey Date: Tue, 30 Dec 2025 19:23:21 +0000 Subject: [PATCH 15/83] Added a link to CLI messagebox (#3885) * Added a link to the CLI tool message box * Clang fix * Another Clang fix * Clang x3 --- src/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index b3a8586ba..e19e8a938 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -188,7 +188,9 @@ int main(int argc, char* argv[]) { if (argc == 1) { if (!SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_INFORMATION, "shadPS4", - "This is a CLI application. Please use the QTLauncher for a GUI.", nullptr)) + "This is a CLI application. Please use the QTLauncher for a GUI: " + "https://github.com/shadps4-emu/shadps4-qtlauncher/releases", + nullptr)) std::cerr << "Could not display SDL message box! Error: " << SDL_GetError() << "\n"; int dummy = 0; // one does not simply pass 0 directly arg_map.at("-h")(dummy); From df844f8c021a455eb0ef579775de1d2c31aa2b93 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Sun, 4 Jan 2026 20:10:42 +0100 Subject: [PATCH 16/83] Add thread names to log lines (#3893) * the bare minimum (this won't even compile on windows yet) * well I guess this is redundant now * Windows GetThreadName * Move function to common/thread and add full guest name where applicable * the loathsome clang-formatter * do stuff first ask for opinions later * copyright 2026 * remove unused header * copyright 2024-2026 --------- Co-authored-by: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> --- src/common/logging/text_formatter.cpp | 7 +++++-- src/common/thread.cpp | 21 +++++++++++++++++++++ src/common/thread.h | 3 +++ src/core/signals.cpp | 22 ++++++---------------- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/common/logging/text_formatter.cpp b/src/common/logging/text_formatter.cpp index b4fa204bc..e7a786396 100644 --- a/src/common/logging/text_formatter.cpp +++ b/src/common/logging/text_formatter.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -13,6 +14,7 @@ #include "common/logging/log.h" #include "common/logging/log_entry.h" #include "common/logging/text_formatter.h" +#include "common/thread.h" namespace Common::Log { @@ -23,8 +25,9 @@ std::string FormatLogMessage(const Entry& entry) { const char* class_name = GetLogClassName(entry.log_class); const char* level_name = GetLevelName(entry.log_level); - return fmt::format("[{}] <{}> {}:{} {}: {}", class_name, level_name, entry.filename, - entry.line_num, entry.function, entry.message); + return fmt::format("[{}] <{}> ({}) {}:{} {}: {}", class_name, level_name, + Common::GetCurrentThreadName(), entry.filename, entry.line_num, + entry.function, entry.message); } void PrintMessage(const Entry& entry) { diff --git a/src/common/thread.cpp b/src/common/thread.cpp index 982041ebb..54194186c 100644 --- a/src/common/thread.cpp +++ b/src/common/thread.cpp @@ -1,11 +1,14 @@ // SPDX-FileCopyrightText: 2013 Dolphin Emulator Project // SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include +#include "core/libraries/kernel/threads/pthread.h" + #include "common/error.h" #include "common/logging/log.h" #include "common/thread.h" @@ -237,4 +240,22 @@ void AccurateTimer::End() { target_interval - std::chrono::duration_cast(now - start_time); } +std::string GetCurrentThreadName() { + using namespace Libraries::Kernel; + if (g_curthread && !g_curthread->name.empty()) { + return g_curthread->name; + } +#ifdef _WIN32 + PWSTR name; + GetThreadDescription(GetCurrentThread(), &name); + return Common::UTF16ToUTF8(name); +#else + char name[256]; + if (pthread_getname_np(pthread_self(), name, sizeof(name)) != 0) { + return ""; + } + return std::string{name}; +#endif +} + } // namespace Common diff --git a/src/common/thread.h b/src/common/thread.h index 5bd83d35c..a300d10c3 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -1,5 +1,6 @@ // SPDX-FileCopyrightText: 2013 Dolphin Emulator Project // SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -46,4 +47,6 @@ public: } }; +std::string GetCurrentThreadName(); + } // namespace Common diff --git a/src/core/signals.cpp b/src/core/signals.cpp index 4099ac237..db6e4b6cc 100644 --- a/src/core/signals.cpp +++ b/src/core/signals.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/arch.h" @@ -42,14 +42,6 @@ static LONG WINAPI SignalHandler(EXCEPTION_POINTERS* pExp) noexcept { #else -static std::string GetThreadName() { - char name[256]; - if (pthread_getname_np(pthread_self(), name, sizeof(name)) != 0) { - return ""; - } - return std::string{name}; -} - static std::string DisassembleInstruction(void* code_address) { char buffer[256] = ""; @@ -80,18 +72,16 @@ static void SignalHandler(int sig, siginfo_t* info, void* raw_context) { case SIGBUS: { const bool is_write = Common::IsWriteError(raw_context); if (!signals->DispatchAccessViolation(raw_context, info->si_addr)) { - UNREACHABLE_MSG( - "Unhandled access violation in thread '{}' at code address {}: {} address {}", - GetThreadName(), fmt::ptr(code_address), is_write ? "Write to" : "Read from", - fmt::ptr(info->si_addr)); + UNREACHABLE_MSG("Unhandled access violation at code address {}: {} address {}", + fmt::ptr(code_address), is_write ? "Write to" : "Read from", + fmt::ptr(info->si_addr)); } break; } case SIGILL: if (!signals->DispatchIllegalInstruction(raw_context)) { - UNREACHABLE_MSG("Unhandled illegal instruction in thread '{}' at code address {}: {}", - GetThreadName(), fmt::ptr(code_address), - DisassembleInstruction(code_address)); + UNREACHABLE_MSG("Unhandled illegal instruction at code address {}: {}", + fmt::ptr(code_address), DisassembleInstruction(code_address)); } break; case SIGUSR1: { // Sleep thread until signal is received From 975b1d312ad9145a13b10e9bbc99a3f7728a18d8 Mon Sep 17 00:00:00 2001 From: mercury501 Date: Sun, 4 Jan 2026 20:30:50 +0100 Subject: [PATCH 17/83] Fixed dialog text input off by one error (#3892) * Fixed dialog off by one error * Fixed exclusion error --------- Co-authored-by: georgemoralis --- src/core/libraries/ime/ime_dialog_ui.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/libraries/ime/ime_dialog_ui.cpp b/src/core/libraries/ime/ime_dialog_ui.cpp index 1d1638bd0..4a95c60c9 100644 --- a/src/core/libraries/ime/ime_dialog_ui.cpp +++ b/src/core/libraries/ime/ime_dialog_ui.cpp @@ -48,7 +48,7 @@ ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param, title.resize(title_len * 4 + 1); title[title_len * 4] = '\0'; - if (!ConvertOrbisToUTF8(param->title, title_len, &title[0], title_len * 4)) { + if (!ConvertOrbisToUTF8(param->title, title_len, &title[0], title_len * 4 + 1)) { LOG_ERROR(Lib_ImeDialog, "Failed to convert title to utf8 encoding"); } } @@ -59,14 +59,14 @@ ImeDialogState::ImeDialogState(const OrbisImeDialogParam* param, placeholder[placeholder_len * 4] = '\0'; if (!ConvertOrbisToUTF8(param->placeholder, placeholder_len, &placeholder[0], - placeholder_len * 4)) { + placeholder_len * 4 + 1)) { LOG_ERROR(Lib_ImeDialog, "Failed to convert placeholder to utf8 encoding"); } } std::size_t text_len = std::char_traits::length(text_buffer); if (!ConvertOrbisToUTF8(text_buffer, text_len, current_text.begin(), - ORBIS_IME_DIALOG_MAX_TEXT_LENGTH * 4)) { + ORBIS_IME_DIALOG_MAX_TEXT_LENGTH * 4 + 1)) { LOG_ERROR(Lib_ImeDialog, "Failed to convert text to utf8 encoding"); } } @@ -110,7 +110,7 @@ bool ImeDialogState::CopyTextToOrbisBuffer() { } return ConvertUTF8ToOrbis(current_text.begin(), current_text.capacity(), text_buffer, - max_text_length); + static_cast(max_text_length) + 1); } bool ImeDialogState::CallTextFilter() { @@ -380,10 +380,12 @@ int ImeDialogUi::InputTextCallback(ImGuiInputTextCallbackData* data) { .timestamp = {0}, }; - if (!ui->state->ConvertUTF8ToOrbis(event_char, 4, &src_keycode.character, 1)) { + char16_t tmp_char[2] = {0}; + if (!ui->state->ConvertUTF8ToOrbis(event_char, 4, tmp_char, 2)) { LOG_ERROR(Lib_ImeDialog, "InputTextCallback: ConvertUTF8ToOrbis failed"); return 0; } + src_keycode.character = tmp_char[0]; LOG_DEBUG(Lib_ImeDialog, "InputTextCallback: converted to Orbis char={:#X}", static_cast(src_keycode.character)); src_keycode.keycode = src_keycode.character; // TODO set this to the correct value From 35da0506b6c74669ec4378cd0e4d9684d5400d4e Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 4 Jan 2026 12:24:37 -0800 Subject: [PATCH 18/83] set name (#3894) --- src/emulator.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/emulator.cpp b/src/emulator.cpp index f0026068c..6f199649f 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -14,6 +14,7 @@ #include "common/debug.h" #include "common/logging/backend.h" #include "common/logging/log.h" +#include "common/thread.h" #include "core/ipc/ipc.h" #ifdef ENABLE_DISCORD_RPC #include "common/discord_rpc_handler.h" @@ -75,6 +76,7 @@ Emulator::~Emulator() {} void Emulator::Run(std::filesystem::path file, std::vector args, std::optional p_game_folder) { + Common::SetCurrentThreadName("Main Thread"); if (waitForDebuggerBeforeRun) { Debugger::WaitForDebuggerAttach(); } From 256397aa3b2f52f7d53c73bd0f698f5cf8536594 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Mon, 5 Jan 2026 01:57:31 +0200 Subject: [PATCH 19/83] Net fixes (#3895) * support for flag in recv/send * make kalapsofos happy * on more SendMessage try * ReceiveMessage too --- src/core/libraries/network/net.h | 11 +- src/core/libraries/network/posix_sockets.cpp | 228 +++++++++++++++---- src/core/libraries/network/sockets.h | 5 +- 3 files changed, 198 insertions(+), 46 deletions(-) diff --git a/src/core/libraries/network/net.h b/src/core/libraries/network/net.h index 2f1339d0a..4cb7afdda 100644 --- a/src/core/libraries/network/net.h +++ b/src/core/libraries/network/net.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 @@ -110,6 +110,15 @@ enum OrbisNetSocketSoOption : u32 { ORBIS_NET_SO_PRIORITY = 0x1203 }; +enum OrbisNetFlags : u32 { + ORBIS_NET_MSG_PEEK = 0x00000002, + ORBIS_NET_MSG_WAITALL = 0x00000040, + ORBIS_NET_MSG_DONTWAIT = 0x00000080, + ORBIS_NET_MSG_USECRYPTO = 0x00100000, + ORBIS_NET_MSG_USESIGNATURE = 0x00200000, + ORBIS_NET_MSG_PEEKLEN = (0x00400000 | ORBIS_NET_MSG_PEEK) +}; + constexpr std::string_view NameOf(OrbisNetSocketSoOption o) { switch (o) { case ORBIS_NET_SO_REUSEADDR: diff --git a/src/core/libraries/network/posix_sockets.cpp b/src/core/libraries/network/posix_sockets.cpp index 3ffb42528..7992fa217 100644 --- a/src/core/libraries/network/posix_sockets.cpp +++ b/src/core/libraries/network/posix_sockets.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 @@ -184,28 +184,103 @@ int PosixSocket::Listen(int backlog) { return ConvertReturnErrorCode(::listen(sock, backlog)); } +static int convertOrbisFlagsToPosix(int sock_type, int sce_flags) { + int posix_flags = 0; + + if (sce_flags & ORBIS_NET_MSG_PEEK) + posix_flags |= MSG_PEEK; +#ifndef _WIN32 + if (sce_flags & ORBIS_NET_MSG_DONTWAIT) + posix_flags |= MSG_DONTWAIT; +#endif + // MSG_WAITALL is only valid for stream sockets + if ((sce_flags & ORBIS_NET_MSG_WAITALL) && + ((sock_type == ORBIS_NET_SOCK_STREAM) || (sock_type == ORBIS_NET_SOCK_STREAM_P2P))) + posix_flags |= MSG_WAITALL; + + return posix_flags; +} + +// On Windows, MSG_DONTWAIT is not handled natively by recv/send. +// This function uses select() with zero timeout to simulate non-blocking behavior. +static int socket_is_ready(int sock, bool is_read = true) { + fd_set fds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + timeval timeout{0, 0}; + int res = + select(sock + 1, is_read ? &fds : nullptr, is_read ? nullptr : &fds, nullptr, &timeout); + if (res == 0) + return ORBIS_NET_ERROR_EWOULDBLOCK; + else if (res < 0) + return ConvertReturnErrorCode(res); + + return res; +} + int PosixSocket::SendMessage(const OrbisNetMsghdr* msg, int flags) { std::scoped_lock lock{m_mutex}; + #ifdef _WIN32 - DWORD bytesSent = 0; - LPFN_WSASENDMSG wsasendmsg = nullptr; - GUID guid = WSAID_WSASENDMSG; - DWORD bytes = 0; + int totalSent = 0; + bool waitAll = (flags & ORBIS_NET_MSG_WAITALL) != 0; + bool dontWait = (flags & ORBIS_NET_MSG_DONTWAIT) != 0; - if (WSAIoctl(sock, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &wsasendmsg, - sizeof(wsasendmsg), &bytes, nullptr, nullptr) != 0) { - return ConvertReturnErrorCode(-1); + // stream socket with multiple buffers + bool use_wsamsg = + (socket_type == ORBIS_NET_SOCK_STREAM || socket_type == ORBIS_NET_SOCK_STREAM_P2P) && + msg->msg_iovlen > 1; + + for (int i = 0; i < msg->msg_iovlen; ++i) { + char* buf = (char*)msg->msg_iov[i].iov_base; + size_t remaining = msg->msg_iov[i].iov_len; + + while (remaining > 0) { + if (dontWait) { + int ready = socket_is_ready(sock, false); + if (ready <= 0) + return ready; + } + + int sent = 0; + if (use_wsamsg) { + // only call WSASendMsg if we have multiple buffers + LPFN_WSASENDMSG wsasendmsg = nullptr; + GUID guid = WSAID_WSASENDMSG; + DWORD bytes = 0; + if (WSAIoctl(sock, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), + &wsasendmsg, sizeof(wsasendmsg), &bytes, nullptr, nullptr) != 0) { + // fallback to send() + sent = ::send(sock, buf, remaining, 0); + } else { + DWORD bytesSent = 0; + int res = wsasendmsg( + sock, reinterpret_cast(const_cast(msg)), 0, + &bytesSent, nullptr, nullptr); + if (res == SOCKET_ERROR) + return ConvertReturnErrorCode(WSAGetLastError()); + sent = bytesSent; + } + } else { + sent = ::send(sock, buf, remaining, 0); + if (sent == SOCKET_ERROR) + return ConvertReturnErrorCode(WSAGetLastError()); + } + + totalSent += sent; + remaining -= sent; + buf += sent; + + if (!waitAll) + break; + } } - int res = wsasendmsg(sock, reinterpret_cast(const_cast(msg)), flags, - &bytesSent, nullptr, nullptr); + return totalSent; - if (res == SOCKET_ERROR) { - return ConvertReturnErrorCode(-1); - } - return static_cast(bytesSent); #else - int res = sendmsg(sock, reinterpret_cast(msg), flags); + int native_flags = convertOrbisFlagsToPosix(socket_type, flags); + int res = sendmsg(sock, reinterpret_cast(msg), native_flags); return ConvertReturnErrorCode(res); #endif } @@ -213,37 +288,92 @@ int PosixSocket::SendMessage(const OrbisNetMsghdr* msg, int flags) { int PosixSocket::SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to, u32 tolen) { std::scoped_lock lock{m_mutex}; - if (to != nullptr) { - sockaddr addr; - convertOrbisNetSockaddrToPosix(to, &addr); - return ConvertReturnErrorCode( - sendto(sock, (const char*)msg, len, flags, &addr, sizeof(sockaddr_in))); - } else { - return ConvertReturnErrorCode(send(sock, (const char*)msg, len, flags)); + int res = 0; +#ifdef _WIN32 + if (flags & ORBIS_NET_MSG_DONTWAIT) { + res = socket_is_ready(sock, false); + if (res <= 0) + return res; } +#endif + const auto posix_flags = convertOrbisFlagsToPosix(socket_type, flags); + if (to == nullptr) { + res = send(sock, (const char*)msg, len, posix_flags); + } else { + sockaddr addr{}; + convertOrbisNetSockaddrToPosix(to, &addr); + res = sendto(sock, (const char*)msg, len, posix_flags, &addr, tolen); + } + return ConvertReturnErrorCode(res); } int PosixSocket::ReceiveMessage(OrbisNetMsghdr* msg, int flags) { std::scoped_lock lock{receive_mutex}; + #ifdef _WIN32 - LPFN_WSARECVMSG wsarecvmsg = nullptr; - GUID guid = WSAID_WSARECVMSG; - DWORD bytes = 0; + int totalReceived = 0; + bool waitAll = (flags & ORBIS_NET_MSG_WAITALL) != 0; + bool dontWait = (flags & ORBIS_NET_MSG_DONTWAIT) != 0; - if (WSAIoctl(sock, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &wsarecvmsg, - sizeof(wsarecvmsg), &bytes, nullptr, nullptr) != 0) { - return ConvertReturnErrorCode(-1); + // stream socket with multiple buffers + bool use_wsarecvmsg = + (socket_type == ORBIS_NET_SOCK_STREAM || socket_type == ORBIS_NET_SOCK_STREAM_P2P) && + msg->msg_iovlen > 1; + + for (int i = 0; i < msg->msg_iovlen; ++i) { + char* buf = (char*)msg->msg_iov[i].iov_base; + size_t remaining = msg->msg_iov[i].iov_len; + + while (remaining > 0) { + // emulate DONTWAIT + if (dontWait) { + int ready = socket_is_ready(sock, true); + if (ready <= 0) + return ready; // returns ORBIS_NET_ERROR_EWOULDBLOCK or error + } + + int received = 0; + if (use_wsarecvmsg) { + // only call WSARecvMsg if multiple buffers + stream + LPFN_WSARECVMSG wsarecvmsg = nullptr; + GUID guid = WSAID_WSARECVMSG; + DWORD bytes = 0; + if (WSAIoctl(sock, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), + &wsarecvmsg, sizeof(wsarecvmsg), &bytes, nullptr, nullptr) != 0) { + // fallback to recv() + received = ::recv(sock, buf, remaining, 0); + if (received == SOCKET_ERROR) + return ConvertReturnErrorCode(WSAGetLastError()); + } else { + DWORD bytesReceived = 0; + int res = wsarecvmsg(sock, reinterpret_cast(msg), &bytesReceived, + nullptr, nullptr); + if (res == SOCKET_ERROR) + return ConvertReturnErrorCode(WSAGetLastError()); + received = bytesReceived; + } + } else { + // fallback to recv() for UDP or single-buffer + received = ::recv(sock, buf, remaining, 0); + if (received == SOCKET_ERROR) + return ConvertReturnErrorCode(WSAGetLastError()); + } + + totalReceived += received; + remaining -= received; + buf += received; + + // stop after first receive if WAITALL is not set + if (!waitAll) + break; + } } - DWORD bytesReceived = 0; - int res = wsarecvmsg(sock, reinterpret_cast(msg), &bytesReceived, nullptr, nullptr); + return totalReceived; - if (res == SOCKET_ERROR) { - return ConvertReturnErrorCode(-1); - } - return static_cast(bytesReceived); #else - int res = recvmsg(sock, reinterpret_cast(msg), flags); + int native_flags = convertOrbisFlagsToPosix(socket_type, flags); + int res = recvmsg(sock, reinterpret_cast(msg), native_flags); return ConvertReturnErrorCode(res); #endif } @@ -251,15 +381,27 @@ int PosixSocket::ReceiveMessage(OrbisNetMsghdr* msg, int flags) { int PosixSocket::ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from, u32* fromlen) { std::scoped_lock lock{receive_mutex}; - if (from != nullptr) { - sockaddr addr; - int res = recvfrom(sock, (char*)buf, len, flags, &addr, (socklen_t*)fromlen); - convertPosixSockaddrToOrbis(&addr, from); - *fromlen = sizeof(OrbisNetSockaddrIn); - return ConvertReturnErrorCode(res); - } else { - return ConvertReturnErrorCode(recv(sock, (char*)buf, len, flags)); + int res = 0; +#ifdef _WIN32 + if (flags & ORBIS_NET_MSG_DONTWAIT) { + res = socket_is_ready(sock); + if (res <= 0) + return res; } +#endif + const auto posix_flags = convertOrbisFlagsToPosix(socket_type, flags); + if (from == nullptr) { + res = recv(sock, (char*)buf, len, posix_flags); + } else { + sockaddr addr{}; + socklen_t addrlen = sizeof(addr); + res = recvfrom(sock, (char*)buf, len, posix_flags, &addr, + (fromlen && *fromlen <= sizeof(addr) ? (socklen_t*)fromlen : &addrlen)); + if (res > 0) + convertPosixSockaddrToOrbis(&addr, from); + } + + return ConvertReturnErrorCode(res); } SocketPtr PosixSocket::Accept(OrbisNetSockaddr* addr, u32* addrlen) { diff --git a/src/core/libraries/network/sockets.h b/src/core/libraries/network/sockets.h index 281c83ab4..86661c71c 100644 --- a/src/core/libraries/network/sockets.h +++ b/src/core/libraries/network/sockets.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 @@ -62,7 +62,7 @@ struct OrbisNetLinger { s32 l_linger; }; struct Socket { - explicit Socket(int domain, int type, int protocol) {} + explicit Socket(int domain, int type, int protocol) : socket_type(type) {} virtual ~Socket() = default; virtual bool IsValid() const = 0; virtual int Close() = 0; @@ -84,6 +84,7 @@ struct Socket { virtual std::optional Native() = 0; std::mutex m_mutex; std::mutex receive_mutex; + int socket_type; }; struct PosixSocket : public Socket { From 55e2b0f5205f3be3a60a8a4b0093855a27f23af0 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:56:43 +0100 Subject: [PATCH 20/83] fix float parsing (#3896) --- src/common/memory_patcher.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/memory_patcher.cpp b/src/common/memory_patcher.cpp index 045a530cb..ad737dab4 100644 --- a/src/common/memory_patcher.cpp +++ b/src/common/memory_patcher.cpp @@ -51,14 +51,14 @@ std::string convertValueToHex(const std::string type, const std::string valueStr uint32_t i; } floatUnion; floatUnion.f = std::stof(valueStr); - result = toHex(floatUnion.i, sizeof(floatUnion.i)); + result = toHex(std::byteswap(floatUnion.i), sizeof(floatUnion.i)); } else if (type == "float64") { union { double d; uint64_t i; } doubleUnion; doubleUnion.d = std::stod(valueStr); - result = toHex(doubleUnion.i, sizeof(doubleUnion.i)); + result = toHex(std::byteswap(doubleUnion.i), sizeof(doubleUnion.i)); } else if (type == "utf8") { std::vector byteArray = std::vector(valueStr.begin(), valueStr.end()); From 42220dfbba3f77dc4feecf07b8659086bc0348b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quang=20Ng=C3=B4?= Date: Tue, 6 Jan 2026 16:48:56 +0700 Subject: [PATCH 21/83] Add `vcs-browser` to `net.shadps4.shadPS4.metainfo.xml` (#3897) --- dist/net.shadps4.shadPS4.metainfo.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/dist/net.shadps4.shadPS4.metainfo.xml b/dist/net.shadps4.shadPS4.metainfo.xml index f42840e9b..d2a6747d9 100644 --- a/dist/net.shadps4.shadPS4.metainfo.xml +++ b/dist/net.shadps4.shadPS4.metainfo.xml @@ -11,6 +11,7 @@ GPL-2.0 net.shadps4.shadPS4.desktop https://shadps4.net/ + https://github.com/shadps4-emu/shadPS4

shadPS4 is an early PlayStation 4 emulator for Windows, Linux and macOS written in C++.

The emulator is still early in development, so don't expect a flawless experience. Nonetheless, the emulator can already run a number of commercial games.

From 299159b1e04359a3b45e822d2bf88838dc45a125 Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:49:52 +0100 Subject: [PATCH 22/83] logging: fix thread name logging for async logs (#3898) * logging: fix thread name logging for async logs * copyright 2026 --- src/common/logging/backend.cpp | 2 ++ src/common/logging/log_entry.h | 1 + src/common/logging/text_formatter.cpp | 6 ++---- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 4a85c4cde..a65d1d09b 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -219,6 +220,7 @@ public: .line_num = line_num, .function = function, .message = std::move(message), + .thread = Common::GetCurrentThreadName(), }; if (Config::getLogType() == "async") { message_queue.EmplaceWait(entry); diff --git a/src/common/logging/log_entry.h b/src/common/logging/log_entry.h index cd4ae9355..6c529f878 100644 --- a/src/common/logging/log_entry.h +++ b/src/common/logging/log_entry.h @@ -21,6 +21,7 @@ struct Entry { u32 line_num = 0; std::string function; std::string message; + std::string thread; }; } // namespace Common::Log diff --git a/src/common/logging/text_formatter.cpp b/src/common/logging/text_formatter.cpp index e7a786396..e8c5f4979 100644 --- a/src/common/logging/text_formatter.cpp +++ b/src/common/logging/text_formatter.cpp @@ -14,7 +14,6 @@ #include "common/logging/log.h" #include "common/logging/log_entry.h" #include "common/logging/text_formatter.h" -#include "common/thread.h" namespace Common::Log { @@ -25,9 +24,8 @@ std::string FormatLogMessage(const Entry& entry) { const char* class_name = GetLogClassName(entry.log_class); const char* level_name = GetLevelName(entry.log_level); - return fmt::format("[{}] <{}> ({}) {}:{} {}: {}", class_name, level_name, - Common::GetCurrentThreadName(), entry.filename, entry.line_num, - entry.function, entry.message); + return fmt::format("[{}] <{}> ({}) {}:{} {}: {}", class_name, level_name, entry.thread, + entry.filename, entry.line_num, entry.function, entry.message); } void PrintMessage(const Entry& entry) { From 240c1d64419b9c3c28d6b8b68f04b05787f0b3bc Mon Sep 17 00:00:00 2001 From: Ploo <239304139+xinitrcn1@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:36:07 +0000 Subject: [PATCH 23/83] Update backend.cpp (#3900) --- src/common/logging/backend.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index a65d1d09b..d7c816da3 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -98,6 +98,7 @@ private: std::size_t bytes_written = 0; }; +#ifdef _WIN32 /** * Backend that writes to Visual Studio's output window */ @@ -108,15 +109,14 @@ public: ~DebuggerBackend() = default; void Write(const Entry& entry) { -#ifdef _WIN32 ::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str()); -#endif } void Flush() {} void EnableForStacktrace() {} }; +#endif bool initialization_in_progress_suppress_logging = true; @@ -268,7 +268,9 @@ private: } void ForEachBackend(auto lambda) { - // lambda(debugger_backend); +#ifdef _WIN32 + lambda(debugger_backend); +#endif lambda(color_console_backend); lambda(file_backend); } @@ -281,7 +283,9 @@ private: static inline bool should_append{false}; Filter filter; +#ifdef _WIN32 DebuggerBackend debugger_backend{}; +#endif ColorConsoleBackend color_console_backend{}; FileBackend file_backend; From 233192fa95c9f08f984328899d73462bdbb17c8d Mon Sep 17 00:00:00 2001 From: TheThunderTurner <64212185+thethunderturner@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:22:21 +0100 Subject: [PATCH 24/83] LIbrary: libSceRudp (#3903) * libSceRudp * use newer glslang * remove module start/stop * register lib, revert glslang upgrade * 2026 * remove default funcions --- CMakeLists.txt | 2 + src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + src/core/libraries/libs.cpp | 2 + src/core/libraries/rudp/rudp.cpp | 218 +++++++++++++++++++++++++++++++ src/core/libraries/rudp/rudp.h | 50 +++++++ 6 files changed, 274 insertions(+) create mode 100644 src/core/libraries/rudp/rudp.cpp create mode 100644 src/core/libraries/rudp/rudp.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 99aca5268..e3ac31f17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -424,6 +424,8 @@ set(SYSTEM_LIBS src/core/libraries/system/commondialog.cpp src/core/libraries/rtc/rtc.cpp src/core/libraries/rtc/rtc.h src/core/libraries/rtc/rtc_error.h + src/core/libraries/rudp/rudp.cpp + src/core/libraries/rudp/rudp.h src/core/libraries/disc_map/disc_map.cpp src/core/libraries/disc_map/disc_map.h src/core/libraries/disc_map/disc_map_codes.h diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index fd8386aff..da683d9d1 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -115,6 +115,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, LibCInternal) \ SUB(Lib, AppContent) \ SUB(Lib, Rtc) \ + SUB(Lib, Rudp) \ SUB(Lib, DiscMap) \ SUB(Lib, Png) \ SUB(Lib, Jpeg) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 82db477ed..513ca4535 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -82,6 +82,7 @@ enum class Class : u8 { Lib_LibCInternal, ///< The LibCInternal implementation. Lib_AppContent, ///< The LibSceAppContent implementation. Lib_Rtc, ///< The LibSceRtc implementation. + Lib_Rudp, ///< The LibSceRudp implementation. Lib_DiscMap, ///< The LibSceDiscMap implementation. Lib_Png, ///< The LibScePng implementation. Lib_Jpeg, ///< The LibSceJpeg implementation. diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 1f7ecb75e..4ac8532db 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -48,6 +48,7 @@ #include "core/libraries/razor_cpu/razor_cpu.h" #include "core/libraries/remote_play/remoteplay.h" #include "core/libraries/rtc/rtc.h" +#include "core/libraries/rudp/rudp.h" #include "core/libraries/save_data/dialog/savedatadialog.h" #include "core/libraries/save_data/savedata.h" #include "core/libraries/screenshot/screenshot.h" @@ -141,6 +142,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::CompanionUtil::RegisterLib(sym); Libraries::Voice::RegisterLib(sym); Libraries::Rtc::RegisterLib(sym); + Libraries::Rudp::RegisterLib(sym); Libraries::VrTracker::RegisterLib(sym); // Loading libSceSsl is locked behind a title workaround that currently applies to nothing. diff --git a/src/core/libraries/rudp/rudp.cpp b/src/core/libraries/rudp/rudp.cpp new file mode 100644 index 000000000..2dfb66f64 --- /dev/null +++ b/src/core/libraries/rudp/rudp.cpp @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: Copyright 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/rudp/rudp.h" + +namespace Libraries::Rudp { + +s32 PS4_SYSV_ABI sceRudpAccept() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpActivate() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpBind() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpCreateContext() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpEnableInternalIOThread() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpEnableInternalIOThread2() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpEnd() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpFlush() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpGetContextStatus() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpGetLocalInfo() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpGetMaxSegmentSize() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpGetNumberOfPacketsToRead() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpGetOption() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpGetRemoteInfo() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpGetSizeReadable() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpGetSizeWritable() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpGetStatus() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpInit() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpInitiate() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpListen() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpNetFlush() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpNetReceived() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpPollCancel() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpPollControl() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpPollCreate() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpPollDestroy() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpPollWait() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpProcessEvents() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpRead() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpSetEventHandler() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpSetMaxSegmentSize() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpSetOption() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpTerminate() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceRudpWrite() { + LOG_ERROR(Lib_Rudp, "(STUBBED) called"); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("uQiK7fjU6y8", "libSceRudp", 1, "libSceRudp", sceRudpAccept); + LIB_FUNCTION("J-6d0WTjzMc", "libSceRudp", 1, "libSceRudp", sceRudpActivate); + LIB_FUNCTION("l4SLBpKUDK4", "libSceRudp", 1, "libSceRudp", sceRudpBind); + LIB_FUNCTION("CAbbX6BuQZ0", "libSceRudp", 1, "libSceRudp", sceRudpCreateContext); + LIB_FUNCTION("6PBNpsgyaxw", "libSceRudp", 1, "libSceRudp", sceRudpEnableInternalIOThread); + LIB_FUNCTION("fJ51weR1WAI", "libSceRudp", 1, "libSceRudp", sceRudpEnableInternalIOThread2); + LIB_FUNCTION("3hBvwqEwqj8", "libSceRudp", 1, "libSceRudp", sceRudpEnd); + LIB_FUNCTION("Ms0cLK8sTtE", "libSceRudp", 1, "libSceRudp", sceRudpFlush); + LIB_FUNCTION("wIJsiqY+BMk", "libSceRudp", 1, "libSceRudp", sceRudpGetContextStatus); + LIB_FUNCTION("2G7-vVz9SIg", "libSceRudp", 1, "libSceRudp", sceRudpGetLocalInfo); + LIB_FUNCTION("vfrL8gPlm2Y", "libSceRudp", 1, "libSceRudp", sceRudpGetMaxSegmentSize); + LIB_FUNCTION("Px0miD2LuW0", "libSceRudp", 1, "libSceRudp", sceRudpGetNumberOfPacketsToRead); + LIB_FUNCTION("mCQIhSmCP6o", "libSceRudp", 1, "libSceRudp", sceRudpGetOption); + LIB_FUNCTION("Qignjmfgha0", "libSceRudp", 1, "libSceRudp", sceRudpGetRemoteInfo); + LIB_FUNCTION("sAZqO2+5Qqo", "libSceRudp", 1, "libSceRudp", sceRudpGetSizeReadable); + LIB_FUNCTION("fRc1ahQppR4", "libSceRudp", 1, "libSceRudp", sceRudpGetSizeWritable); + LIB_FUNCTION("i3STzxuwPx0", "libSceRudp", 1, "libSceRudp", sceRudpGetStatus); + LIB_FUNCTION("amuBfI-AQc4", "libSceRudp", 1, "libSceRudp", sceRudpInit); + LIB_FUNCTION("szEVu+edXV4", "libSceRudp", 1, "libSceRudp", sceRudpInitiate); + LIB_FUNCTION("tYVWcWDnctE", "libSceRudp", 1, "libSceRudp", sceRudpListen); + LIB_FUNCTION("+BJ9svDmjYs", "libSceRudp", 1, "libSceRudp", sceRudpNetFlush); + LIB_FUNCTION("vPzJldDSxXc", "libSceRudp", 1, "libSceRudp", sceRudpNetReceived); + LIB_FUNCTION("yzeXuww-UWg", "libSceRudp", 1, "libSceRudp", sceRudpPollCancel); + LIB_FUNCTION("haMpc7TFx0A", "libSceRudp", 1, "libSceRudp", sceRudpPollControl); + LIB_FUNCTION("MVbmLASjn5M", "libSceRudp", 1, "libSceRudp", sceRudpPollCreate); + LIB_FUNCTION("LjwbHpEeW0A", "libSceRudp", 1, "libSceRudp", sceRudpPollDestroy); + LIB_FUNCTION("M6ggviwXpLs", "libSceRudp", 1, "libSceRudp", sceRudpPollWait); + LIB_FUNCTION("9U9m1YH0ScQ", "libSceRudp", 1, "libSceRudp", sceRudpProcessEvents); + LIB_FUNCTION("rZqWV3eXgOA", "libSceRudp", 1, "libSceRudp", sceRudpRead); + LIB_FUNCTION("SUEVes8gvmw", "libSceRudp", 1, "libSceRudp", sceRudpSetEventHandler); + LIB_FUNCTION("beAsSTVWVPQ", "libSceRudp", 1, "libSceRudp", sceRudpSetMaxSegmentSize); + LIB_FUNCTION("0yzYdZf0IwE", "libSceRudp", 1, "libSceRudp", sceRudpSetOption); + LIB_FUNCTION("OMYRTU0uc4w", "libSceRudp", 1, "libSceRudp", sceRudpTerminate); + LIB_FUNCTION("KaPL3fbTLCA", "libSceRudp", 1, "libSceRudp", sceRudpWrite); +}; + +} // namespace Libraries::Rudp \ No newline at end of file diff --git a/src/core/libraries/rudp/rudp.h b/src/core/libraries/rudp/rudp.h new file mode 100644 index 000000000..22d2576a2 --- /dev/null +++ b/src/core/libraries/rudp/rudp.h @@ -0,0 +1,50 @@ +// 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::Rudp { + +s32 PS4_SYSV_ABI sceRudpAccept(); +s32 PS4_SYSV_ABI sceRudpActivate(); +s32 PS4_SYSV_ABI sceRudpBind(); +s32 PS4_SYSV_ABI sceRudpCreateContext(); +s32 PS4_SYSV_ABI sceRudpEnableInternalIOThread(); +s32 PS4_SYSV_ABI sceRudpEnableInternalIOThread2(); +s32 PS4_SYSV_ABI sceRudpEnd(); +s32 PS4_SYSV_ABI sceRudpFlush(); +s32 PS4_SYSV_ABI sceRudpGetContextStatus(); +s32 PS4_SYSV_ABI sceRudpGetLocalInfo(); +s32 PS4_SYSV_ABI sceRudpGetMaxSegmentSize(); +s32 PS4_SYSV_ABI sceRudpGetNumberOfPacketsToRead(); +s32 PS4_SYSV_ABI sceRudpGetOption(); +s32 PS4_SYSV_ABI sceRudpGetRemoteInfo(); +s32 PS4_SYSV_ABI sceRudpGetSizeReadable(); +s32 PS4_SYSV_ABI sceRudpGetSizeWritable(); +s32 PS4_SYSV_ABI sceRudpGetStatus(); +s32 PS4_SYSV_ABI sceRudpInit(); +s32 PS4_SYSV_ABI sceRudpInitiate(); +s32 PS4_SYSV_ABI sceRudpListen(); +s32 PS4_SYSV_ABI sceRudpNetFlush(); +s32 PS4_SYSV_ABI sceRudpNetReceived(); +s32 PS4_SYSV_ABI sceRudpPollCancel(); +s32 PS4_SYSV_ABI sceRudpPollControl(); +s32 PS4_SYSV_ABI sceRudpPollCreate(); +s32 PS4_SYSV_ABI sceRudpPollDestroy(); +s32 PS4_SYSV_ABI sceRudpPollWait(); +s32 PS4_SYSV_ABI sceRudpProcessEvents(); +s32 PS4_SYSV_ABI sceRudpRead(); +s32 PS4_SYSV_ABI sceRudpSetEventHandler(); +s32 PS4_SYSV_ABI sceRudpSetMaxSegmentSize(); +s32 PS4_SYSV_ABI sceRudpSetOption(); +s32 PS4_SYSV_ABI sceRudpTerminate(); +s32 PS4_SYSV_ABI sceRudpWrite(); + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Rudp \ No newline at end of file From f7a473b3917dc5eb0fad0a8289d56e1ccde90b87 Mon Sep 17 00:00:00 2001 From: Vladislav Mikhalin Date: Fri, 9 Jan 2026 18:29:09 +0300 Subject: [PATCH 25/83] read compiled SDK version from eboot (#3905) --- src/core/libraries/kernel/process.cpp | 8 +++++--- src/core/linker.cpp | 4 ++-- src/emulator.cpp | 29 +++++++++++++++++++++++---- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index e88446e02..4ea7fa062 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -36,9 +36,11 @@ s32 PS4_SYSV_ABI sceKernelGetMainSocId() { } s32 PS4_SYSV_ABI sceKernelGetCompiledSdkVersion(s32* ver) { - s32 version = Common::ElfInfo::Instance().CompiledSdkVer(); - *ver = version; - return (version >= 0) ? ORBIS_OK : ORBIS_KERNEL_ERROR_EINVAL; + if (!ver) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + *ver = Common::ElfInfo::Instance().CompiledSdkVer(); + return ORBIS_OK; } s32 PS4_SYSV_ABI sceKernelGetCpumode() { diff --git a/src/core/linker.cpp b/src/core/linker.cpp index 8d7f9207e..97d766a38 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -180,8 +180,8 @@ s32 Linker::LoadAndStartModule(const std::filesystem::path& path, u64 args, cons } // Retrieve and verify proc param according to libkernel. - u64* param = module->GetProcParam(); - ASSERT_MSG(!param || param[0] >= 0x18, "Invalid module param size: {}", param[0]); + auto* param = module->GetProcParam(); + ASSERT_MSG(!param || param->size >= 0x18, "Invalid module param size: {}", param->size); s32 ret = module->Start(args, argp, param); if (pRes) { *pRes = ret; diff --git a/src/emulator.cpp b/src/emulator.cpp index 6f199649f..4947beeb8 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -74,6 +74,25 @@ Emulator::Emulator() { Emulator::~Emulator() {} +s32 ReadCompiledSdkVersion(const std::filesystem::path& file) { + Core::Loader::Elf elf; + elf.Open(file); + if (!elf.IsElfFile()) { + return 0; + } + const auto elf_pheader = elf.GetProgramHeader(); + auto i_procparam = std::find_if(elf_pheader.begin(), elf_pheader.end(), [](const auto& entry) { + return entry.p_type == PT_SCE_PROCPARAM; + }); + + if (i_procparam != elf_pheader.end()) { + Core::OrbisProcParam param{}; + elf.LoadSegment(u64(¶m), i_procparam->p_offset, i_procparam->p_filesz); + return param.sdk_version; + } + return 0; +} + void Emulator::Run(std::filesystem::path file, std::vector args, std::optional p_game_folder) { Common::SetCurrentThreadName("Main Thread"); @@ -162,6 +181,9 @@ void Emulator::Run(std::filesystem::path file, std::vector args, } } + auto guest_eboot_path = "/app0/" + eboot_name.generic_string(); + const auto eboot_path = mnt->GetHostPath(guest_eboot_path); + auto& game_info = Common::ElfInfo::Instance(); game_info.initialized = true; game_info.game_serial = id; @@ -169,7 +191,7 @@ void Emulator::Run(std::filesystem::path file, std::vector args, game_info.app_ver = app_version; game_info.firmware_ver = fw_version & 0xFFF00000; game_info.raw_firmware_ver = fw_version; - game_info.sdk_ver = sdk_version; + game_info.sdk_ver = ReadCompiledSdkVersion(eboot_path); game_info.psf_attributes = psf_attributes; const auto pic1_path = mnt->GetHostPath("/app0/sce_sys/pic1.png"); @@ -241,7 +263,8 @@ void Emulator::Run(std::filesystem::path file, std::vector args, if (param_sfo_exists) { LOG_INFO(Loader, "Game id: {} Title: {}", id, title); LOG_INFO(Loader, "Fw: {:#x} App Version: {}", fw_version, app_version); - LOG_INFO(Loader, "Compiled SDK version: {:#x}", sdk_version); + LOG_INFO(Loader, "param.sfo SDK version: {:#x}", sdk_version); + LOG_INFO(Loader, "eboot SDK version: {:#x}", game_info.sdk_ver); LOG_INFO(Loader, "PSVR Supported: {}", (bool)psf_attributes.support_ps_vr.Value()); LOG_INFO(Loader, "PSVR Required: {}", (bool)psf_attributes.require_ps_vr.Value()); } @@ -339,8 +362,6 @@ void Emulator::Run(std::filesystem::path file, std::vector args, Libraries::InitHLELibs(&linker->GetHLESymbols()); // Load the module with the linker - auto guest_eboot_path = "/app0/" + eboot_name.generic_string(); - const auto eboot_path = mnt->GetHostPath(guest_eboot_path); if (linker->LoadModule(eboot_path) == -1) { LOG_CRITICAL(Loader, "Failed to load game's eboot.bin: {}", Common::FS::PathToUTF8String(std::filesystem::absolute(eboot_path))); From be99cb4d5bfd386eeb0df0af033c99f8ef5deb71 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 9 Jan 2026 19:42:05 +0200 Subject: [PATCH 26/83] Emulator state (#3906) * moved getShowFpsCounter to emu state * more state variables * fps counter is back * removed fpscolor --- CMakeLists.txt | 2 ++ src/common/config.cpp | 25 ----------------------- src/common/config.h | 3 --- src/common/memory_patcher.cpp | 5 +++-- src/core/devtools/layer.cpp | 15 ++++++-------- src/core/emulator_state.cpp | 37 +++++++++++++++++++++++++++++++++++ src/core/emulator_state.h | 29 +++++++++++++++++++++++++++ src/core/ipc/ipc.cpp | 5 +++-- src/main.cpp | 7 +++++-- 9 files changed, 85 insertions(+), 43 deletions(-) create mode 100644 src/core/emulator_state.cpp create mode 100644 src/core/emulator_state.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e3ac31f17..ef5e935a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -840,6 +840,8 @@ set(CORE src/core/aerolib/stubs.cpp src/core/thread.h src/core/tls.cpp src/core/tls.h + src/core/emulator_state.cpp + src/core/emulator_state.h ) if (ARCHITECTURE STREQUAL "x86_64") diff --git a/src/common/config.cpp b/src/common/config.cpp index 1af326af7..eac463d0a 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -198,7 +198,6 @@ static ConfigEntry pipelineCacheArchive(false); static ConfigEntry isDebugDump(false); static ConfigEntry isShaderDebug(false); static ConfigEntry isSeparateLogFilesEnabled(false); -static ConfigEntry isFpsColor(true); static ConfigEntry showFpsCounter(false); static ConfigEntry logEnabled(true); @@ -223,16 +222,6 @@ static string config_version = Common::g_scm_rev; // These entries aren't stored in the config static bool overrideControllerColor = false; static int controllerCustomColorRGB[3] = {0, 0, 255}; -static bool isGameRunning = false; -static bool load_auto_patches = true; - -bool getGameRunning() { - return isGameRunning; -} - -void setGameRunning(bool running) { - isGameRunning = running; -} std::filesystem::path getSysModulesPath() { if (sys_modules_path.empty()) { @@ -463,10 +452,6 @@ bool isPipelineCacheArchived() { return pipelineCacheArchive.get(); } -bool fpsColor() { - return isFpsColor.get(); -} - bool getShowFpsCounter() { return showFpsCounter.get(); } @@ -855,13 +840,6 @@ void setUsbDeviceBackend(int value, bool is_game_specific) { usbDeviceBackend.set(value, is_game_specific); } -bool getLoadAutoPatches() { - return load_auto_patches; -} -void setLoadAutoPatches(bool enable) { - load_auto_patches = enable; -} - void load(const std::filesystem::path& path, bool is_game_specific) { // If the configuration file does not exist, create it and return, unless it is game specific std::error_code error; @@ -977,7 +955,6 @@ void load(const std::filesystem::path& path, bool is_game_specific) { isDebugDump.setFromToml(debug, "DebugDump", is_game_specific); isSeparateLogFilesEnabled.setFromToml(debug, "isSeparateLogFilesEnabled", is_game_specific); isShaderDebug.setFromToml(debug, "CollectShader", is_game_specific); - isFpsColor.setFromToml(debug, "FPSColor", is_game_specific); showFpsCounter.setFromToml(debug, "showFpsCounter", is_game_specific); logEnabled.setFromToml(debug, "logEnabled", is_game_specific); current_version = toml::find_or(debug, "ConfigVersion", current_version); @@ -1197,7 +1174,6 @@ void save(const std::filesystem::path& path, bool is_game_specific) { data["GPU"]["internalScreenWidth"] = internalScreenWidth.base_value; data["GPU"]["internalScreenHeight"] = internalScreenHeight.base_value; data["GPU"]["patchShaders"] = shouldPatchShaders.base_value; - data["Debug"]["FPSColor"] = isFpsColor.base_value; data["Debug"]["showFpsCounter"] = showFpsCounter.base_value; } @@ -1306,7 +1282,6 @@ void setDefaultValues(bool is_game_specific) { internalScreenHeight.base_value = 720; // Debug - isFpsColor.base_value = true; showFpsCounter.base_value = false; } } diff --git a/src/common/config.h b/src/common/config.h index 2bd65b783..2a95e6cf0 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -125,7 +125,6 @@ int getSpecialPadClass(); bool getPSNSignedIn(); void setPSNSignedIn(bool sign, bool is_game_specific = false); bool patchShaders(); // no set -bool fpsColor(); // no set bool getShowFpsCounter(); void setShowFpsCounter(bool enable, bool is_game_specific = false); bool isNeoModeConsole(); @@ -154,8 +153,6 @@ void setConnectedToNetwork(bool enable, bool is_game_specific = false); void setUserName(const std::string& name, bool is_game_specific = false); std::filesystem::path getSysModulesPath(); void setSysModulesPath(const std::filesystem::path& path); -bool getLoadAutoPatches(); -void setLoadAutoPatches(bool enable); enum UsbBackendType : int { Real, SkylandersPortal, InfinityBase, DimensionsToypad }; int getUsbDeviceBackend(); diff --git a/src/common/memory_patcher.cpp b/src/common/memory_patcher.cpp index ad737dab4..a7c020246 100644 --- a/src/common/memory_patcher.cpp +++ b/src/common/memory_patcher.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 @@ -12,6 +12,7 @@ #include "common/elf_info.h" #include "common/logging/log.h" #include "common/path_util.h" +#include "core/emulator_state.h" #include "core/file_format/psf.h" #include "memory_patcher.h" @@ -192,7 +193,7 @@ void OnGameLoaded() { } else { ApplyPatchesFromXML(file_path); } - } else if (Config::getLoadAutoPatches()) { + } else if (EmulatorState::GetInstance()->IsAutoPatchesLoadEnabled()) { for (auto const& repo : std::filesystem::directory_iterator(patch_dir)) { if (!repo.is_directory()) { continue; diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index cfa950568..928040fec 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.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 "layer.h" @@ -11,6 +11,7 @@ #include "common/singleton.h" #include "common/types.h" #include "core/debug_state.h" +#include "core/emulator_state.h" #include "imgui/imgui_std.h" #include "imgui_internal.h" #include "options.h" @@ -273,14 +274,10 @@ void L::DrawAdvanced() { void L::DrawSimple() { const float frameRate = DebugState.Framerate; - if (Config::fpsColor()) { - if (frameRate < 10) { - PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); // Red - } else if (frameRate >= 10 && frameRate < 20) { - PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.0f, 1.0f)); // Orange - } else { - PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); // White - } + if (frameRate < 10) { + PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); // Red + } else if (frameRate >= 10 && frameRate < 20) { + PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.0f, 1.0f)); // Orange } else { PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); // White } diff --git a/src/core/emulator_state.cpp b/src/core/emulator_state.cpp new file mode 100644 index 000000000..1f02043a3 --- /dev/null +++ b/src/core/emulator_state.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "emulator_state.h" + +std::shared_ptr EmulatorState::s_instance = nullptr; +std::mutex EmulatorState::s_mutex; + +EmulatorState::EmulatorState() {} + +EmulatorState::~EmulatorState() {} + +std::shared_ptr EmulatorState::GetInstance() { + std::lock_guard lock(s_mutex); + if (!s_instance) + s_instance = std::make_shared(); + return s_instance; +} + +void EmulatorState::SetInstance(std::shared_ptr instance) { + std::lock_guard lock(s_mutex); + s_instance = instance; +} + +bool EmulatorState::IsGameRunning() const { + return m_running; +} +void EmulatorState::SetGameRunning(bool running) { + m_running = running; +} + +bool EmulatorState::IsAutoPatchesLoadEnabled() const { + return m_load_patches_auto; +} +void EmulatorState::SetAutoPatchesLoadEnabled(bool enable) { + m_load_patches_auto = enable; +} diff --git a/src/core/emulator_state.h b/src/core/emulator_state.h new file mode 100644 index 000000000..c12af5401 --- /dev/null +++ b/src/core/emulator_state.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class EmulatorState { +public: + EmulatorState(); + ~EmulatorState(); + + static std::shared_ptr GetInstance(); + static void SetInstance(std::shared_ptr instance); + + bool IsGameRunning() const; + void SetGameRunning(bool running); + bool IsAutoPatchesLoadEnabled() const; + void SetAutoPatchesLoadEnabled(bool enable); + +private: + static std::shared_ptr s_instance; + static std::mutex s_mutex; + + // state variables + bool m_running = false; + bool m_load_patches_auto = true; +}; \ No newline at end of file diff --git a/src/core/ipc/ipc.cpp b/src/core/ipc/ipc.cpp index 08cf3bbb2..ea7cd38b4 100644 --- a/src/core/ipc/ipc.cpp +++ b/src/core/ipc/ipc.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 "ipc.h" @@ -14,6 +14,7 @@ #include "common/types.h" #include "core/debug_state.h" #include "core/debugger.h" +#include "core/emulator_state.h" #include "core/libraries/audio/audioout.h" #include "input/input_handler.h" #include "sdl_window.h" @@ -71,7 +72,7 @@ void IPC::Init() { return; } - Config::setLoadAutoPatches(false); + EmulatorState::GetInstance()->SetAutoPatchesLoadEnabled(false); input_thread = std::jthread([this] { Common::SetCurrentThreadName("IPC Read thread"); diff --git a/src/main.cpp b/src/main.cpp index e19e8a938..aa3f4de45 100644 --- a/src/main.cpp +++ b/src/main.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 @@ -8,6 +8,7 @@ #include "system_error" #include "unordered_map" +#include #include #include "common/config.h" #include "common/logging/backend.h" @@ -27,7 +28,9 @@ int main(int argc, char* argv[]) { SetConsoleOutputCP(CP_UTF8); #endif IPC::Instance().Init(); - + // Init emulator state + std::shared_ptr m_emu_state = std::make_shared(); + EmulatorState::SetInstance(m_emu_state); // Load configurations const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); Config::load(user_dir / "config.toml"); From f42a566cefbb085fcdb5219fcf238c55c01f8bc5 Mon Sep 17 00:00:00 2001 From: marecl Date: Fri, 9 Jan 2026 23:35:43 +0100 Subject: [PATCH 27/83] Dirents backport (#3876) * lseek for directories behaves correctly when final index is smaller than 0 (EINVAL) Backported and improved dirents from QFS Normal directory dirents update on change * PFS moves pointer to end when last dirent is returned * Correct entry type in PFS directory --- .../file_sys/directories/base_directory.cpp | 32 +++ .../file_sys/directories/base_directory.h | 24 +- .../file_sys/directories/normal_directory.cpp | 221 ++++++++-------- .../file_sys/directories/normal_directory.h | 18 +- .../file_sys/directories/pfs_directory.cpp | 236 ++++++------------ src/core/file_sys/directories/pfs_directory.h | 16 +- 6 files changed, 260 insertions(+), 287 deletions(-) diff --git a/src/core/file_sys/directories/base_directory.cpp b/src/core/file_sys/directories/base_directory.cpp index c709da6a2..75f67577c 100644 --- a/src/core/file_sys/directories/base_directory.cpp +++ b/src/core/file_sys/directories/base_directory.cpp @@ -5,6 +5,7 @@ #include "common/singleton.h" #include "core/file_sys/directories/base_directory.h" #include "core/file_sys/fs.h" +#include "core/libraries/kernel/orbis_error.h" namespace Core::Directories { @@ -12,4 +13,35 @@ BaseDirectory::BaseDirectory() = default; BaseDirectory::~BaseDirectory() = default; +s64 BaseDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) { + s64 bytes_read = 0; + for (s32 i = 0; i < iovcnt; i++) { + const s64 result = read(iov[i].iov_base, iov[i].iov_len); + if (result < 0) { + return result; + } + bytes_read += result; + } + return bytes_read; +} + +s64 BaseDirectory::preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) { + const u64 old_file_pointer = file_offset; + file_offset = offset; + const s64 bytes_read = readv(iov, iovcnt); + file_offset = old_file_pointer; + return bytes_read; +} + +s64 BaseDirectory::lseek(s64 offset, s32 whence) { + + s64 file_offset_new = ((0 == whence) * offset) + ((1 == whence) * (file_offset + offset)) + + ((2 == whence) * (directory_size + offset)); + if (file_offset_new < 0) + return ORBIS_KERNEL_ERROR_EINVAL; + + file_offset = file_offset_new; + return file_offset; +} + } // namespace Core::Directories \ No newline at end of file diff --git a/src/core/file_sys/directories/base_directory.h b/src/core/file_sys/directories/base_directory.h index b412865a2..832b8ac40 100644 --- a/src/core/file_sys/directories/base_directory.h +++ b/src/core/file_sys/directories/base_directory.h @@ -19,6 +19,17 @@ struct OrbisKernelDirent; namespace Core::Directories { class BaseDirectory { +protected: + static inline u32 fileno_pool{10}; + + static u32 next_fileno() { + return ++fileno_pool; + } + + s64 file_offset = 0; + u64 directory_size = 0; + std::vector dirent_cache_bin{}; + public: explicit BaseDirectory(); @@ -28,13 +39,8 @@ public: return ORBIS_KERNEL_ERROR_EBADF; } - virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) { - return ORBIS_KERNEL_ERROR_EBADF; - } - - virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) { - return ORBIS_KERNEL_ERROR_EBADF; - } + virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt); + virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset); virtual s64 write(const void* buf, u64 nbytes) { return ORBIS_KERNEL_ERROR_EBADF; @@ -48,9 +54,7 @@ public: return ORBIS_KERNEL_ERROR_EBADF; } - virtual s64 lseek(s64 offset, s32 whence) { - return ORBIS_KERNEL_ERROR_EBADF; - } + virtual s64 lseek(s64 offset, s32 whence); virtual s32 fstat(Libraries::Kernel::OrbisKernelStat* stat) { return ORBIS_KERNEL_ERROR_EBADF; diff --git a/src/core/file_sys/directories/normal_directory.cpp b/src/core/file_sys/directories/normal_directory.cpp index a7d76074a..3ed7c9492 100644 --- a/src/core/file_sys/directories/normal_directory.cpp +++ b/src/core/file_sys/directories/normal_directory.cpp @@ -15,111 +15,30 @@ std::shared_ptr NormalDirectory::Create(std::string_view guest_di std::make_shared(guest_directory)); } -NormalDirectory::NormalDirectory(std::string_view guest_directory) { - auto* mnt = Common::Singleton::Instance(); - - static s32 fileno = 0; - mnt->IterateDirectory(guest_directory, [this](const auto& ent_path, const auto ent_is_file) { - auto& dirent = dirents.emplace_back(); - dirent.d_fileno = ++fileno; - dirent.d_type = (ent_is_file ? 8 : 4); - strncpy(dirent.d_name, ent_path.filename().string().data(), MAX_LENGTH + 1); - dirent.d_namlen = ent_path.filename().string().size(); - - // Calculate the appropriate length for this dirent. - // Account for the null terminator in d_name too. - dirent.d_reclen = Common::AlignUp(sizeof(dirent.d_fileno) + sizeof(dirent.d_type) + - sizeof(dirent.d_namlen) + sizeof(dirent.d_reclen) + - (dirent.d_namlen + 1), - 4); - - directory_size += dirent.d_reclen; - }); - - // The last entry of a normal directory should have d_reclen covering the remaining data. - // Since the dirents of a folder are constant by this point, we can modify the last dirent - // before creating the emulated file buffer. - const u64 filler_count = Common::AlignUp(directory_size, DIRECTORY_ALIGNMENT) - directory_size; - dirents[dirents.size() - 1].d_reclen += filler_count; - - // Reading from standard directories seems to be based around file pointer logic. - // Keep an internal buffer representing the raw contents of this file descriptor, - // then emulate the various read functions with that. - directory_size = Common::AlignUp(directory_size, DIRECTORY_ALIGNMENT); - data_buffer.reserve(directory_size); - memset(data_buffer.data(), 0, directory_size); - - u8* current_dirent = data_buffer.data(); - for (const NormalDirectoryDirent& dirent : dirents) { - NormalDirectoryDirent* dirent_to_write = - reinterpret_cast(current_dirent); - dirent_to_write->d_fileno = dirent.d_fileno; - - // Using size d_namlen + 1 to account for null terminator. - strncpy(dirent_to_write->d_name, dirent.d_name, dirent.d_namlen + 1); - dirent_to_write->d_namlen = dirent.d_namlen; - dirent_to_write->d_reclen = dirent.d_reclen; - dirent_to_write->d_type = dirent.d_type; - - current_dirent += dirent.d_reclen; - } +NormalDirectory::NormalDirectory(std::string_view guest_directory) + : guest_directory(guest_directory) { + RebuildDirents(); } s64 NormalDirectory::read(void* buf, u64 nbytes) { - // Nothing left to read. - if (file_offset >= directory_size) { - return ORBIS_OK; - } + RebuildDirents(); - const s64 remaining_data = directory_size - file_offset; - const s64 bytes = nbytes > remaining_data ? remaining_data : nbytes; + // data is contiguous. read goes like any regular file would: start at offset, read n bytes + // output is always aligned up to 512 bytes with 0s + // offset - classic. however at the end of read any unused (exceeding dirent buffer size) buffer + // space will be left untouched + // reclen always sums up to end of current alignment - std::memcpy(buf, data_buffer.data() + file_offset, bytes); + s64 bytes_available = this->dirent_cache_bin.size() - file_offset; + if (bytes_available <= 0) + return 0; + bytes_available = std::min(bytes_available, static_cast(nbytes)); - file_offset += bytes; - return bytes; -} + // data + memcpy(buf, this->dirent_cache_bin.data() + file_offset, bytes_available); -s64 NormalDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) { - s64 bytes_read = 0; - for (s32 i = 0; i < iovcnt; i++) { - const s64 result = read(iov[i].iov_base, iov[i].iov_len); - if (result < 0) { - return result; - } - bytes_read += result; - } - return bytes_read; -} - -s64 NormalDirectory::preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, - s64 offset) { - const u64 old_file_pointer = file_offset; - file_offset = offset; - const s64 bytes_read = readv(iov, iovcnt); - file_offset = old_file_pointer; - return bytes_read; -} - -s64 NormalDirectory::lseek(s64 offset, s32 whence) { - switch (whence) { - case 0: { - file_offset = offset; - break; - } - case 1: { - file_offset += offset; - break; - } - case 2: { - file_offset = directory_size + offset; - break; - } - default: { - UNREACHABLE_MSG("lseek with unknown whence {}", whence); - } - } - return file_offset; + file_offset += bytes_available; + return bytes_available; } s32 NormalDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) { @@ -131,10 +50,110 @@ s32 NormalDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) { } s64 NormalDirectory::getdents(void* buf, u64 nbytes, s64* basep) { - if (basep != nullptr) { + RebuildDirents(); + + if (basep) *basep = file_offset; + + // same as others, we just don't need a variable + if (file_offset >= directory_size) + return 0; + + s64 bytes_written = 0; + s64 working_offset = file_offset; + s64 dirent_buffer_offset = 0; + s64 aligned_count = Common::AlignDown(nbytes, 512); + + const u8* dirent_buffer = this->dirent_cache_bin.data(); + while (dirent_buffer_offset < this->dirent_cache_bin.size()) { + const u8* normal_dirent_ptr = dirent_buffer + dirent_buffer_offset; + const NormalDirectoryDirent* normal_dirent = + reinterpret_cast(normal_dirent_ptr); + auto d_reclen = normal_dirent->d_reclen; + + // bad, incomplete or OOB entry + if (normal_dirent->d_namlen == 0) + break; + + if (working_offset >= d_reclen) { + dirent_buffer_offset += d_reclen; + working_offset -= d_reclen; + continue; + } + + if ((bytes_written + d_reclen) > aligned_count) + // dirents are aligned to the last full one + break; + + memcpy(static_cast(buf) + bytes_written, normal_dirent_ptr + working_offset, + d_reclen - working_offset); + bytes_written += d_reclen - working_offset; + dirent_buffer_offset += d_reclen; + working_offset = 0; } - // read behaves identically to getdents for normal directories. - return read(buf, nbytes); + + file_offset += bytes_written; + return bytes_written; } + +void NormalDirectory::RebuildDirents() { + // regenerate only when target wants to read contents again + // no reason for testing - read is always raw and dirents get processed on the go + if (previous_file_offset == file_offset) + return; + previous_file_offset = file_offset; + + constexpr u32 dirent_meta_size = + sizeof(NormalDirectoryDirent::d_fileno) + sizeof(NormalDirectoryDirent::d_type) + + sizeof(NormalDirectoryDirent::d_namlen) + sizeof(NormalDirectoryDirent::d_reclen); + + u64 next_ceiling = 0; + u64 dirent_offset = 0; + u64 last_reclen_offset = 4; + dirent_cache_bin.clear(); + dirent_cache_bin.reserve(512); + + auto* mnt = Common::Singleton::Instance(); + + mnt->IterateDirectory( + guest_directory, [this, &next_ceiling, &dirent_offset, &last_reclen_offset]( + const std::filesystem::path& ent_path, const bool ent_is_file) { + NormalDirectoryDirent tmp{}; + std::string leaf(ent_path.filename().string()); + + // prepare dirent + tmp.d_fileno = BaseDirectory::next_fileno(); + tmp.d_namlen = leaf.size(); + strncpy(tmp.d_name, leaf.data(), tmp.d_namlen + 1); + tmp.d_type = (ent_is_file ? 0100000 : 0040000) >> 12; + tmp.d_reclen = Common::AlignUp(dirent_meta_size + tmp.d_namlen + 1, 4); + + // next element may break 512 byte alignment + if (tmp.d_reclen + dirent_offset > next_ceiling) { + // align previous dirent's size to the current ceiling + *reinterpret_cast(static_cast(dirent_cache_bin.data()) + + last_reclen_offset) += next_ceiling - dirent_offset; + // set writing pointer to the aligned start position (current ceiling) + dirent_offset = next_ceiling; + // move the ceiling up and zero-out the buffer + next_ceiling += 512; + dirent_cache_bin.resize(next_ceiling); + std::fill(dirent_cache_bin.begin() + dirent_offset, + dirent_cache_bin.begin() + next_ceiling, 0); + } + + // current dirent's reclen position + last_reclen_offset = dirent_offset + 4; + memcpy(dirent_cache_bin.data() + dirent_offset, &tmp, tmp.d_reclen); + dirent_offset += tmp.d_reclen; + }); + + // last reclen, as before + *reinterpret_cast(static_cast(dirent_cache_bin.data()) + last_reclen_offset) += + next_ceiling - dirent_offset; + + // i have no idea if this is the case, but lseek returns size aligned to 512 + directory_size = next_ceiling; +} + } // namespace Core::Directories \ No newline at end of file diff --git a/src/core/file_sys/directories/normal_directory.h b/src/core/file_sys/directories/normal_directory.h index 70e52f581..4fc84cd2a 100644 --- a/src/core/file_sys/directories/normal_directory.h +++ b/src/core/file_sys/directories/normal_directory.h @@ -19,27 +19,23 @@ public: ~NormalDirectory() override = default; virtual s64 read(void* buf, u64 nbytes) override; - virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) override; - virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, - s64 offset) override; - virtual s64 lseek(s64 offset, s32 whence) override; virtual s32 fstat(Libraries::Kernel::OrbisKernelStat* stat) override; virtual s64 getdents(void* buf, u64 nbytes, s64* basep) override; private: - static constexpr s32 MAX_LENGTH = 255; - static constexpr s64 DIRECTORY_ALIGNMENT = 0x200; +#pragma pack(push, 1) struct NormalDirectoryDirent { u32 d_fileno; u16 d_reclen; u8 d_type; u8 d_namlen; - char d_name[MAX_LENGTH + 1]; + char d_name[256]; }; +#pragma pack(pop) - u64 directory_size = 0; - s64 file_offset = 0; - std::vector data_buffer; - std::vector dirents; + std::string_view guest_directory{}; + s64 previous_file_offset = -1; + + void RebuildDirents(void); }; } // namespace Core::Directories diff --git a/src/core/file_sys/directories/pfs_directory.cpp b/src/core/file_sys/directories/pfs_directory.cpp index fbd97c019..38ceaf345 100644 --- a/src/core/file_sys/directories/pfs_directory.cpp +++ b/src/core/file_sys/directories/pfs_directory.cpp @@ -15,77 +15,49 @@ std::shared_ptr PfsDirectory::Create(std::string_view guest_direc } PfsDirectory::PfsDirectory(std::string_view guest_directory) { + constexpr u32 dirent_meta_size = + sizeof(PfsDirectoryDirent::d_fileno) + sizeof(PfsDirectoryDirent::d_type) + + sizeof(PfsDirectoryDirent::d_namlen) + sizeof(PfsDirectoryDirent::d_reclen); + + dirent_cache_bin.reserve(512); + auto* mnt = Common::Singleton::Instance(); - static s32 fileno = 0; - mnt->IterateDirectory(guest_directory, [this](const auto& ent_path, const auto ent_is_file) { - auto& dirent = dirents.emplace_back(); - dirent.d_fileno = ++fileno; - dirent.d_type = (ent_is_file ? 8 : 4); - strncpy(dirent.d_name, ent_path.filename().string().data(), MAX_LENGTH + 1); - dirent.d_namlen = ent_path.filename().string().size(); + mnt->IterateDirectory( + guest_directory, [this](const std::filesystem::path& ent_path, const bool ent_is_file) { + PfsDirectoryDirent tmp{}; + std::string leaf(ent_path.filename().string()); - // Calculate the appropriate length for this dirent. - // Account for the null terminator in d_name too. - dirent.d_reclen = Common::AlignUp(sizeof(dirent.d_fileno) + sizeof(dirent.d_type) + - sizeof(dirent.d_namlen) + sizeof(dirent.d_reclen) + - (dirent.d_namlen + 1), - 8); + tmp.d_fileno = BaseDirectory::next_fileno(); + tmp.d_namlen = leaf.size(); + strncpy(tmp.d_name, leaf.data(), tmp.d_namlen + 1); + tmp.d_type = ent_is_file ? 2 : 4; + tmp.d_reclen = Common::AlignUp(dirent_meta_size + tmp.d_namlen + 1, 8); + auto dirent_ptr = reinterpret_cast(&tmp); - // To handle some obscure dirents_index behavior, - // keep track of the "actual" length of this directory. - directory_content_size += dirent.d_reclen; - }); + dirent_cache_bin.insert(dirent_cache_bin.end(), dirent_ptr, dirent_ptr + tmp.d_reclen); + }); - directory_size = Common::AlignUp(directory_content_size, DIRECTORY_ALIGNMENT); + directory_size = Common::AlignUp(dirent_cache_bin.size(), 0x10000); } s64 PfsDirectory::read(void* buf, u64 nbytes) { - if (dirents_index >= dirents.size()) { - if (dirents_index < directory_content_size) { - // We need to find the appropriate dirents_index to start from. - s64 data_to_skip = dirents_index; - u64 corrected_index = 0; - while (data_to_skip > 0) { - const auto dirent = dirents[corrected_index++]; - data_to_skip -= dirent.d_reclen; - } - dirents_index = corrected_index; - } else { - // Nothing left to read. - return ORBIS_OK; - } + s64 bytes_available = this->dirent_cache_bin.size() - file_offset; + if (bytes_available <= 0) + return 0; + + bytes_available = std::min(bytes_available, static_cast(nbytes)); + memcpy(buf, this->dirent_cache_bin.data() + file_offset, bytes_available); + + s64 to_fill = + (std::min(directory_size, static_cast(nbytes))) - bytes_available - file_offset; + if (to_fill < 0) { + LOG_ERROR(Kernel_Fs, "Dirent may have leaked {} bytes", -to_fill); + return -to_fill + bytes_available; } - - s64 bytes_remaining = nbytes > directory_size ? directory_size : nbytes; - // read on PfsDirectories will always return the maximum possible value. - const u64 bytes_written = bytes_remaining; - memset(buf, 0, bytes_remaining); - - char* current_dirent = static_cast(buf); - PfsDirectoryDirent dirent = dirents[dirents_index]; - while (bytes_remaining > dirent.d_reclen) { - PfsDirectoryDirent* dirent_to_write = reinterpret_cast(current_dirent); - dirent_to_write->d_fileno = dirent.d_fileno; - - // Using size d_namlen + 1 to account for null terminator. - strncpy(dirent_to_write->d_name, dirent.d_name, dirent.d_namlen + 1); - dirent_to_write->d_namlen = dirent.d_namlen; - dirent_to_write->d_reclen = dirent.d_reclen; - dirent_to_write->d_type = dirent.d_type; - - current_dirent += dirent.d_reclen; - bytes_remaining -= dirent.d_reclen; - - if (dirents_index == dirents.size() - 1) { - // Currently at the last dirent, so break out of the loop. - dirents_index++; - break; - } - dirent = dirents[++dirents_index]; - } - - return bytes_written; + memset(static_cast(buf) + bytes_available, 0, to_fill); + file_offset += to_fill + bytes_available; + return to_fill + bytes_available; } s64 PfsDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) { @@ -101,62 +73,13 @@ s64 PfsDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovc } s64 PfsDirectory::preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) { - const u64 old_dirent_index = dirents_index; - dirents_index = 0; - s64 data_to_skip = offset; - // If offset is part-way through one dirent, that dirent is skipped. - while (data_to_skip > 0) { - const auto dirent = dirents[dirents_index++]; - data_to_skip -= dirent.d_reclen; - if (dirents_index == dirents.size()) { - // We've reached the end of the dirents, nothing more can be skipped. - break; - } - } - + const u64 old_file_pointer = file_offset; + file_offset = offset; const s64 bytes_read = readv(iov, iovcnt); - dirents_index = old_dirent_index; + file_offset = old_file_pointer; return bytes_read; } -s64 PfsDirectory::lseek(s64 offset, s32 whence) { - switch (whence) { - // Seek start - case 0: { - dirents_index = 0; - } - case 1: { - // There aren't any dirents left to pass through. - if (dirents_index >= dirents.size()) { - dirents_index = dirents_index + offset; - break; - } - s64 data_to_skip = offset; - while (data_to_skip > 0) { - const auto dirent = dirents[dirents_index++]; - data_to_skip -= dirent.d_reclen; - if (dirents_index == dirents.size()) { - // We've passed through all file dirents. - // Set dirents_index to directory_size + remaining_offset instead. - dirents_index = directory_content_size + data_to_skip; - break; - } - } - break; - } - case 2: { - // Seems like real hardware gives up on tracking dirents_index if you go this route. - dirents_index = directory_size + offset; - break; - } - default: { - UNREACHABLE_MSG("lseek with unknown whence {}", whence); - } - } - - return dirents_index; -} - s32 PfsDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) { stat->st_mode = 0000777u | 0040000u; stat->st_size = directory_size; @@ -166,55 +89,58 @@ s32 PfsDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) { } s64 PfsDirectory::getdents(void* buf, u64 nbytes, s64* basep) { - // basep is set at the start of the function. - if (basep != nullptr) { - *basep = dirents_index; - } + if (basep) + *basep = file_offset; - if (dirents_index >= dirents.size()) { - if (dirents_index < directory_content_size) { - // We need to find the appropriate dirents_index to start from. - s64 data_to_skip = dirents_index; - u64 corrected_index = 0; - while (data_to_skip > 0) { - const auto dirent = dirents[corrected_index++]; - data_to_skip -= dirent.d_reclen; - } - dirents_index = corrected_index; - } else { - // Nothing left to read. - return ORBIS_OK; - } - } - - s64 bytes_remaining = nbytes > directory_size ? directory_size : nbytes; - memset(buf, 0, bytes_remaining); + // same as others, we just don't need a variable + if (file_offset >= directory_size) + return 0; u64 bytes_written = 0; - char* current_dirent = static_cast(buf); - // getdents has to convert pfs dirents to normal dirents - PfsDirectoryDirent dirent = dirents[dirents_index]; - while (bytes_remaining > dirent.d_reclen) { - NormalDirectoryDirent* dirent_to_write = - reinterpret_cast(current_dirent); - dirent_to_write->d_fileno = dirent.d_fileno; - strncpy(dirent_to_write->d_name, dirent.d_name, dirent.d_namlen + 1); - dirent_to_write->d_namlen = dirent.d_namlen; - dirent_to_write->d_reclen = dirent.d_reclen; - dirent_to_write->d_type = dirent.d_type; + u64 starting_offset = 0; + u64 buffer_position = 0; + while (buffer_position < this->dirent_cache_bin.size()) { + const PfsDirectoryDirent* pfs_dirent = + reinterpret_cast(this->dirent_cache_bin.data() + buffer_position); - current_dirent += dirent.d_reclen; - bytes_remaining -= dirent.d_reclen; - bytes_written += dirent.d_reclen; - - if (dirents_index == dirents.size() - 1) { - // Currently at the last dirent, so set dirents_index appropriately and break. - dirents_index = directory_size; + // bad, incomplete or OOB entry + if (pfs_dirent->d_namlen == 0) break; + + if (starting_offset < file_offset) { + // reading starts from the nearest full dirent + starting_offset += pfs_dirent->d_reclen; + buffer_position = bytes_written + starting_offset; + continue; } - dirent = dirents[++dirents_index]; + + if ((bytes_written + pfs_dirent->d_reclen) > nbytes) + // dirents are aligned to the last full one + break; + + // if this dirent breaks alignment, skip + // dirents are count-aligned here, excess data is simply not written + // if (Common::AlignUp(buffer_position, count) != + // Common::AlignUp(buffer_position + pfs_dirent->d_reclen, count)) + // break; + + // reclen for both is the same despite difference in var sizes, extra 0s are padded after + // the name + NormalDirectoryDirent normal_dirent{}; + normal_dirent.d_fileno = pfs_dirent->d_fileno; + normal_dirent.d_reclen = pfs_dirent->d_reclen; + normal_dirent.d_type = (pfs_dirent->d_type == 2) ? 8 : 4; + normal_dirent.d_namlen = pfs_dirent->d_namlen; + memcpy(normal_dirent.d_name, pfs_dirent->d_name, pfs_dirent->d_namlen); + + memcpy(static_cast(buf) + bytes_written, &normal_dirent, normal_dirent.d_reclen); + bytes_written += normal_dirent.d_reclen; + buffer_position = bytes_written + starting_offset; } + file_offset = (buffer_position >= this->dirent_cache_bin.size()) + ? directory_size + : (file_offset + bytes_written); return bytes_written; } } // namespace Core::Directories \ No newline at end of file diff --git a/src/core/file_sys/directories/pfs_directory.h b/src/core/file_sys/directories/pfs_directory.h index 8f3e8d1f5..23b7e1eb0 100644 --- a/src/core/file_sys/directories/pfs_directory.h +++ b/src/core/file_sys/directories/pfs_directory.h @@ -22,32 +22,28 @@ public: virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) override; virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) override; - virtual s64 lseek(s64 offset, s32 whence) override; virtual s32 fstat(Libraries::Kernel::OrbisKernelStat* stat) override; virtual s64 getdents(void* buf, u64 nbytes, s64* basep) override; private: - static constexpr s32 MAX_LENGTH = 255; - static constexpr s32 DIRECTORY_ALIGNMENT = 0x10000; +#pragma pack(push, 1) struct PfsDirectoryDirent { u32 d_fileno; u32 d_type; u32 d_namlen; u32 d_reclen; - char d_name[MAX_LENGTH + 1]; + char d_name[256]; }; +#pragma pack(pop) +#pragma pack(push, 1) struct NormalDirectoryDirent { u32 d_fileno; u16 d_reclen; u8 d_type; u8 d_namlen; - char d_name[MAX_LENGTH + 1]; + char d_name[256]; }; - - u64 directory_size = 0; - u64 directory_content_size = 0; - s64 dirents_index = 0; - std::vector dirents; +#pragma pack(pop) }; } // namespace Core::Directories From b84c5ed1108b5935d81beca8318b7c10cc0d97d9 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 10 Jan 2026 09:23:42 -0600 Subject: [PATCH 28/83] Corrected physical base handling for memory pools (#3909) On real hardware, each pool commit can have multiple physical memory allocations, always allocating the lowest free physical addresses first. Currently, since we currently only support one physical mapping per VMA, I create a separate VMA representing each physical allocation we perform. --- src/core/memory.cpp | 161 +++++++++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 71 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index b9fd7fd7d..3a4a16933 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -314,35 +314,47 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 prot |= MemoryProt::CpuRead; } - // Carve out the new VMA representing this mapping - const auto new_vma_handle = CarveVMA(mapped_addr, size); - auto& new_vma = new_vma_handle->second; - new_vma.disallow_merge = false; - new_vma.prot = prot; - new_vma.name = "anon"; - new_vma.type = Core::VMAType::Pooled; - new_vma.is_exec = false; - - // Find a suitable physical address + // Find suitable physical addresses auto handle = dmem_map.begin(); - while (handle != dmem_map.end() && - (handle->second.dma_type != Core::DMAType::Pooled || handle->second.size < size)) { + u64 remaining_size = size; + VAddr current_addr = mapped_addr; + while (handle != dmem_map.end() && remaining_size != 0) { + if (handle->second.dma_type != DMAType::Pooled) { + // Non-pooled means it's either not for pool use, or already committed. + handle++; + continue; + } + + // On PS4, commits can make sparse physical mappings. + // For now, it's easier to create separate memory mappings for each physical mapping. + u64 size_to_map = std::min(remaining_size, handle->second.size); + + // Carve out the new VMA representing this mapping + const auto new_vma_handle = CarveVMA(current_addr, size_to_map); + auto& new_vma = new_vma_handle->second; + new_vma.disallow_merge = false; + new_vma.prot = prot; + new_vma.name = "anon"; + new_vma.type = Core::VMAType::Pooled; + new_vma.is_exec = false; + + // Use the start of this area as the physical backing for this mapping. + const auto new_dmem_handle = CarveDmemArea(handle->second.base, size_to_map); + auto& new_dmem_area = new_dmem_handle->second; + new_dmem_area.dma_type = DMAType::Committed; + new_dmem_area.memory_type = mtype; + new_vma.phys_base = new_dmem_area.base; + handle = MergeAdjacent(dmem_map, new_dmem_handle); + + // Perform the mapping + void* out_addr = impl.Map(current_addr, size_to_map, alignment, new_vma.phys_base, false); + TRACK_ALLOC(out_addr, size_to_map, "VMEM"); + + current_addr += size_to_map; + remaining_size -= size_to_map; handle++; } - ASSERT_MSG(handle != dmem_map.end() && handle->second.dma_type == Core::DMAType::Pooled, - "No suitable physical memory areas to map"); - - // Use the start of this area as the physical backing for this mapping. - const auto new_dmem_handle = CarveDmemArea(handle->second.base, size); - auto& new_dmem_area = new_dmem_handle->second; - new_dmem_area.dma_type = DMAType::Committed; - new_dmem_area.memory_type = mtype; - new_vma.phys_base = new_dmem_area.base; - MergeAdjacent(dmem_map, new_dmem_handle); - - // Perform the mapping - void* out_addr = impl.Map(mapped_addr, size, alignment, new_vma.phys_base, false); - TRACK_ALLOC(out_addr, size, "VMEM"); + ASSERT_MSG(remaining_size == 0, "Unable to map physical memory"); if (IsValidGpuMapping(mapped_addr, size)) { rasterizer->MapMemory(mapped_addr, size); @@ -609,57 +621,64 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { virtual_addr); std::scoped_lock lk{mutex}; - const auto it = FindVMA(virtual_addr); - const auto& vma_base = it->second; - ASSERT_MSG(vma_base.Contains(virtual_addr, size), - "Existing mapping does not contain requested unmap range"); - - const auto vma_base_addr = vma_base.base; - const auto vma_base_size = vma_base.size; - const auto phys_base = vma_base.phys_base; - const bool is_exec = vma_base.is_exec; - const auto start_in_vma = virtual_addr - vma_base_addr; - const auto type = vma_base.type; - - if (type != VMAType::PoolReserved && type != VMAType::Pooled) { - LOG_ERROR(Kernel_Vmm, "Attempting to decommit non-pooled memory!"); - return ORBIS_KERNEL_ERROR_EINVAL; + // Do an initial search to ensure this decommit is valid. + auto it = FindVMA(virtual_addr); + while (it != vma_map.end() && it->second.base + it->second.size <= virtual_addr + size) { + if (it->second.type != VMAType::PoolReserved && it->second.type != VMAType::Pooled) { + LOG_ERROR(Kernel_Vmm, "Attempting to decommit non-pooled memory!"); + return ORBIS_KERNEL_ERROR_EINVAL; + } + it++; } - if (type == VMAType::Pooled) { - // We always map PoolCommitted memory to GPU, so unmap when decomitting. - if (IsValidGpuMapping(virtual_addr, size)) { - rasterizer->UnmapMemory(virtual_addr, size); + // Loop through all vmas in the area, unmap them. + u64 remaining_size = size; + VAddr current_addr = virtual_addr; + while (remaining_size != 0) { + const auto it = FindVMA(current_addr); + const auto& vma_base = it->second; + const bool is_exec = vma_base.is_exec; + 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; + + // Re-pool the direct memory used by this mapping + const auto unmap_phys_base = vma_base.phys_base + start_in_vma; + const auto new_dmem_handle = CarveDmemArea(unmap_phys_base, size_in_vma); + auto& new_dmem_area = new_dmem_handle->second; + new_dmem_area.dma_type = DMAType::Pooled; + + // Coalesce with nearby direct memory areas. + MergeAdjacent(dmem_map, new_dmem_handle); } - // Track how much pooled memory is decommitted - pool_budget += size; + if (vma_base.type != VMAType::PoolReserved) { + // Unmap the memory region. + impl.Unmap(vma_base.base, vma_base.size, start_in_vma, start_in_vma + size_in_vma, + vma_base.phys_base, vma_base.is_exec, true, false); + TRACK_FREE(virtual_addr, "VMEM"); + } - // Re-pool the direct memory used by this mapping - const auto unmap_phys_base = phys_base + start_in_vma; - const auto new_dmem_handle = CarveDmemArea(unmap_phys_base, size); - auto& new_dmem_area = new_dmem_handle->second; - new_dmem_area.dma_type = DMAType::Pooled; + // Mark region as pool reserved and attempt to coalesce it with neighbours. + const auto new_it = CarveVMA(current_addr, size_in_vma); + auto& vma = new_it->second; + vma.type = VMAType::PoolReserved; + vma.prot = MemoryProt::NoAccess; + vma.phys_base = 0; + vma.disallow_merge = false; + vma.name = "anon"; + MergeAdjacent(vma_map, new_it); - // Coalesce with nearby direct memory areas. - MergeAdjacent(dmem_map, new_dmem_handle); - } - - // Mark region as pool reserved and attempt to coalesce it with neighbours. - const auto new_it = CarveVMA(virtual_addr, size); - auto& vma = new_it->second; - vma.type = VMAType::PoolReserved; - vma.prot = MemoryProt::NoAccess; - vma.phys_base = 0; - vma.disallow_merge = false; - vma.name = "anon"; - MergeAdjacent(vma_map, new_it); - - if (type != VMAType::PoolReserved) { - // Unmap the memory region. - impl.Unmap(vma_base_addr, vma_base_size, start_in_vma, start_in_vma + size, phys_base, - is_exec, true, false); - TRACK_FREE(virtual_addr, "VMEM"); + current_addr += size_in_vma; + remaining_size -= size_in_vma; } return ORBIS_OK; From ed73d2b02cfe9bb5adf741c7ea595bfa3ff0ccbc Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 10 Jan 2026 09:33:28 -0600 Subject: [PATCH 29/83] Libraries: libSceNpWebApi2 stubs (#3908) * libSceNpWebApi2 stubs Based on some brief decompilation, brings some extra Unreal Engine titles futher. * Clang * Suggestions and fixes --- 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_web_api2.cpp | 331 ++++++++++++++++++++++ src/core/libraries/np/np_web_api2.h | 40 +++ src/core/libraries/np/np_web_api2_error.h | 35 +++ 7 files changed, 412 insertions(+) create mode 100644 src/core/libraries/np/np_web_api2.cpp create mode 100644 src/core/libraries/np/np_web_api2.h create mode 100644 src/core/libraries/np/np_web_api2_error.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ef5e935a8..ab144aa37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -589,6 +589,8 @@ 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_api2.cpp + src/core/libraries/np/np_web_api2.h src/core/libraries/np/np_party.cpp src/core/libraries/np/np_party.h src/core/libraries/np/np_auth.cpp diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index da683d9d1..d954f8601 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, NpScore) \ SUB(Lib, NpTrophy) \ SUB(Lib, NpWebApi) \ + SUB(Lib, NpWebApi2) \ SUB(Lib, NpProfileDialog) \ SUB(Lib, NpSnsFacebookDialog) \ SUB(Lib, Screenshot) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 513ca4535..ee18cd161 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -76,6 +76,7 @@ enum class Class : u8 { Lib_NpScore, ///< The LibSceNpScore implementation Lib_NpTrophy, ///< The LibSceNpTrophy implementation Lib_NpWebApi, ///< The LibSceWebApi implementation + Lib_NpWebApi2, ///< The LibSceWebApi2 implementation Lib_NpProfileDialog, ///< The LibSceNpProfileDialog implementation Lib_NpSnsFacebookDialog, ///< The LibSceNpSnsFacebookDialog implementation Lib_Screenshot, ///< The LibSceScreenshot implementation diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 4ac8532db..85923134c 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -41,6 +41,7 @@ #include "core/libraries/np/np_sns_facebook_dialog.h" #include "core/libraries/np/np_trophy.h" #include "core/libraries/np/np_web_api.h" +#include "core/libraries/np/np_web_api2.h" #include "core/libraries/pad/pad.h" #include "core/libraries/playgo/playgo.h" #include "core/libraries/playgo/playgo_dialog.h" @@ -101,6 +102,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Np::NpScore::RegisterLib(sym); Libraries::Np::NpTrophy::RegisterLib(sym); Libraries::Np::NpWebApi::RegisterLib(sym); + Libraries::Np::NpWebApi2::RegisterLib(sym); Libraries::Np::NpProfileDialog::RegisterLib(sym); Libraries::Np::NpSnsFacebookDialog::RegisterLib(sym); Libraries::Np::NpAuth::RegisterLib(sym); diff --git a/src/core/libraries/np/np_web_api2.cpp b/src/core/libraries/np/np_web_api2.cpp new file mode 100644 index 000000000..c03636e73 --- /dev/null +++ b/src/core/libraries/np/np_web_api2.cpp @@ -0,0 +1,331 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/config.h" +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" +#include "core/libraries/np/np_web_api2.h" +#include "core/libraries/np/np_web_api2_error.h" +#include "core/libraries/system/userservice.h" + +namespace Libraries::Np::NpWebApi2 { + +s32 PS4_SYSV_ABI sceNpWebApi2AbortRequest(s64 request_id) { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, request_id = {:#x}", request_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2AddHttpRequestHeader() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2AddMultipartPart() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2AddWebTraceTag() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +void PS4_SYSV_ABI sceNpWebApi2CheckTimeout() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); +} + +s32 PS4_SYSV_ABI sceNpWebApi2CreateMultipartRequest() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2CreateRequest() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2CreateUserContext(s32 lib_ctx_id, + UserService::OrbisUserServiceUserId user_id) { + if (lib_ctx_id >= 0x8000) { + return ORBIS_NP_WEBAPI2_ERROR_INVALID_LIB_CONTEXT_ID; + } + if (user_id == UserService::ORBIS_USER_SERVICE_USER_ID_INVALID) { + return ORBIS_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + } + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, lib_ctx_id = {:#x}, user_id = {:#x}", lib_ctx_id, + user_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2DeleteRequest(s64 request_id) { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, request_id = {:#x}", request_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2DeleteUserContext(s32 user_ctx_id) { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, user_ctx_id = {:#x}", user_ctx_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2GetHttpResponseHeaderValue() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2GetHttpResponseHeaderValueLength() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2GetMemoryPoolStats(s32 lib_ctx_id, + OrbisNpWebApi2MemoryPoolStats* stats) { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, lib_ctx_id = {:#x}", lib_ctx_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2Initialize(s32 lib_http_ctx_id, u64 pool_size) { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, lib_http_ctx_id = {:#x}, pool_size = {:#x}", + lib_http_ctx_id, pool_size); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2InitializeForPresence(s32 lib_http_ctx_id, u64 pool_size) { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, lib_http_ctx_id = {:#x}, pool_size = {:#x}", + lib_http_ctx_id, pool_size); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2IntCreateRequest() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2IntInitialize(const OrbisNpWebApi2IntInitializeArgs* args) { + if (args == nullptr || args->struct_size != sizeof(OrbisNpWebApi2IntInitializeArgs)) { + return ORBIS_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + } + LOG_ERROR(Lib_NpWebApi2, + "(STUBBED) called, lib_http_ctx_id = {:#x}, pool_size = {:#x}, name = '{}'", + args->lib_http_ctx_id, args->pool_size, args->name); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2IntInitialize2(const OrbisNpWebApi2IntInitialize2Args* args) { + if (args == nullptr || args->struct_size != sizeof(OrbisNpWebApi2IntInitialize2Args)) { + return ORBIS_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + } + LOG_ERROR( + Lib_NpWebApi2, + "(STUBBED) called, lib_http_ctx_id = {:#x}, pool_size = {:#x}, name = '{}', group = {:#x}", + args->lib_http_ctx_id, args->pool_size, args->name, args->push_config_group); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2IntPushEventCreateCtxIndFilter() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventAbortHandle() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventCreateFilter() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventCreateHandle() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventCreatePushContext() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventDeleteFilter() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventDeleteHandle() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventDeletePushContext() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventRegisterCallback() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventRegisterPushContextCallback() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventSetHandleTimeout() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventStartPushContextCallback() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventUnregisterCallback() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2PushEventUnregisterPushContextCallback() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2ReadData(s64 request_id, void* data, u64 size) { + if (data == nullptr || size == 0) { + return ORBIS_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + } + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, request_id = {:#x}, size = {:#x}", request_id, + size); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2SendMultipartRequest() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2SendRequest() { + if (!Config::getPSNSignedIn()) { + LOG_INFO(Lib_NpWebApi2, "called, returning PSN signed out."); + return ORBIS_NP_WEBAPI2_ERROR_NOT_SIGNED_IN; + } + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2SetMultipartContentType() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2SetRequestTimeout(s64 request_id, u32 timeout) { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, request_id = {:#x}, timeout = {}", request_id, + timeout); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpWebApi2Terminate(s32 lib_ctx_id) { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called, lib_ctx_id = {:#x}", lib_ctx_id); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI Func_A9A31C5F6FBA6620() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI Func_03D22863300D2B73() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI Func_97296F7578AAD541() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI Func_E0DF39A36F087DB9() { + LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("zpiPsH7dbFQ", "libSceNpWebApi2", 1, "libSceNpWebApi2", sceNpWebApi2AbortRequest); + LIB_FUNCTION("egOOvrnF6mI", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2AddHttpRequestHeader); + LIB_FUNCTION("Io7kh1LHDoM", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2AddMultipartPart); + LIB_FUNCTION("MgsTa76wlEk", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2AddWebTraceTag); + LIB_FUNCTION("3Tt9zL3tkoc", "libSceNpWebApi2", 1, "libSceNpWebApi2", sceNpWebApi2CheckTimeout); + LIB_FUNCTION("+nz1Vq-NrDA", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2CreateMultipartRequest); + LIB_FUNCTION("3EI-OSJ65Xc", "libSceNpWebApi2", 1, "libSceNpWebApi2", sceNpWebApi2CreateRequest); + LIB_FUNCTION("sk54bi6FtYM", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2CreateUserContext); + LIB_FUNCTION("vvzWO-DvG1s", "libSceNpWebApi2", 1, "libSceNpWebApi2", sceNpWebApi2DeleteRequest); + LIB_FUNCTION("9X9+cneTGUU", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2DeleteUserContext); + LIB_FUNCTION("hksbskNToEA", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2GetHttpResponseHeaderValue); + LIB_FUNCTION("HwP3aM+c85c", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2GetHttpResponseHeaderValueLength); + LIB_FUNCTION("Xweb+naPZ8Y", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2GetMemoryPoolStats); + LIB_FUNCTION("+o9816YQhqQ", "libSceNpWebApi2", 1, "libSceNpWebApi2", sceNpWebApi2Initialize); + LIB_FUNCTION("dowMWFgowXY", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2InitializeForPresence); + LIB_FUNCTION("qmINYLuqzaA", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2IntCreateRequest); + LIB_FUNCTION("zXaFo7euxsQ", "libSceNpWebApi2", 1, "libSceNpWebApi2", sceNpWebApi2IntInitialize); + LIB_FUNCTION("9KSGFMRnp3k", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2IntInitialize2); + LIB_FUNCTION("2hlBNB96saE", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2IntPushEventCreateCtxIndFilter); + LIB_FUNCTION("1OLgvahaSco", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventAbortHandle); + LIB_FUNCTION("MsaFhR+lPE4", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventCreateFilter); + LIB_FUNCTION("WV1GwM32NgY", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventCreateHandle); + LIB_FUNCTION("NNVf18SlbT8", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventCreatePushContext); + LIB_FUNCTION("KJdPcOGmK58", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventDeleteFilter); + LIB_FUNCTION("fIATVMo4Y1w", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventDeleteHandle); + LIB_FUNCTION("QafxeZM3WK4", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventDeletePushContext); + LIB_FUNCTION("fY3QqeNkF8k", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventRegisterCallback); + LIB_FUNCTION("lxtHJMwBsaU", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventRegisterPushContextCallback); + LIB_FUNCTION("KWkc6Q3tjXc", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventSetHandleTimeout); + LIB_FUNCTION("AAj9X+4aGYA", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventStartPushContextCallback); + LIB_FUNCTION("hOnIlcGrO6g", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventUnregisterCallback); + LIB_FUNCTION("PmyrbbJSFz0", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2PushEventUnregisterPushContextCallback); + LIB_FUNCTION("OOY9+ObfKec", "libSceNpWebApi2", 1, "libSceNpWebApi2", sceNpWebApi2ReadData); + LIB_FUNCTION("NKCwS8+5Fx8", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2SendMultipartRequest); + LIB_FUNCTION("lQOCF84lvzw", "libSceNpWebApi2", 1, "libSceNpWebApi2", sceNpWebApi2SendRequest); + LIB_FUNCTION("bltDCAskmfE", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2SetMultipartContentType); + LIB_FUNCTION("TjAutbrkr60", "libSceNpWebApi2", 1, "libSceNpWebApi2", + sceNpWebApi2SetRequestTimeout); + LIB_FUNCTION("bEvXpcEk200", "libSceNpWebApi2", 1, "libSceNpWebApi2", sceNpWebApi2Terminate); + LIB_FUNCTION("qaMcX2+6ZiA", "libSceNpWebApi2", 1, "libSceNpWebApi2", Func_A9A31C5F6FBA6620); + LIB_FUNCTION("A9IoYzANK3M", "libSceNpWebApi2AsyncRestricted", 1, "libSceNpWebApi2", + Func_03D22863300D2B73); + LIB_FUNCTION("lylvdXiq1UE", "libSceNpWebApi2AsyncRestricted", 1, "libSceNpWebApi2", + Func_97296F7578AAD541); + LIB_FUNCTION("4N85o28Ifbk", "libSceNpWebApi2AsyncRestricted", 1, "libSceNpWebApi2", + Func_E0DF39A36F087DB9); +}; + +} // namespace Libraries::Np::NpWebApi2 \ No newline at end of file diff --git a/src/core/libraries/np/np_web_api2.h b/src/core/libraries/np/np_web_api2.h new file mode 100644 index 000000000..1e970d08e --- /dev/null +++ b/src/core/libraries/np/np_web_api2.h @@ -0,0 +1,40 @@ +// 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::NpWebApi2 { + +struct OrbisNpWebApi2IntInitializeArgs { + s32 lib_http_ctx_id; + s32 reserved; + u64 pool_size; + char* name; + u64 struct_size; +}; + +struct OrbisNpWebApi2IntInitialize2Args { + s32 lib_http_ctx_id; + s32 reserved; + u64 pool_size; + char* name; + u32 push_config_group; + s32 reserved2; + u64 struct_size; +}; + +struct OrbisNpWebApi2MemoryPoolStats { + u64 pool_size; + u64 max_inuse_size; + u64 current_inuse_size; + s32 reserved; +}; + +void RegisterLib(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Np::NpWebApi2 \ No newline at end of file diff --git a/src/core/libraries/np/np_web_api2_error.h b/src/core/libraries/np/np_web_api2_error.h new file mode 100644 index 000000000..6ab4fdb31 --- /dev/null +++ b/src/core/libraries/np/np_web_api2_error.h @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/libraries/error_codes.h" + +constexpr int ORBIS_NP_WEBAPI2_ERROR_OUT_OF_MEMORY = 0x80553401; +constexpr int ORBIS_NP_WEBAPI2_ERROR_INVALID_ARGUMENT = 0x80553402; +constexpr int ORBIS_NP_WEBAPI2_ERROR_INVALID_LIB_CONTEXT_ID = 0x80553403; +constexpr int ORBIS_NP_WEBAPI2_ERROR_LIB_CONTEXT_NOT_FOUND = 0x80553404; +constexpr int ORBIS_NP_WEBAPI2_ERROR_USER_CONTEXT_NOT_FOUND = 0x80553405; +constexpr int ORBIS_NP_WEBAPI2_ERROR_REQUEST_NOT_FOUND = 0x80553406; +constexpr int ORBIS_NP_WEBAPI2_ERROR_NOT_SIGNED_IN = 0x80553407; +constexpr int ORBIS_NP_WEBAPI2_ERROR_INVALID_CONTENT_PARAMETER = 0x80553408; +constexpr int ORBIS_NP_WEBAPI2_ERROR_ABORTED = 0x80553409; +constexpr int ORBIS_NP_WEBAPI2_USER_CONTEXT_ALREADY_EXIST = 0x8055340a; +constexpr int ORBIS_NP_WEBAPI2_PUSH_EVENT_FILTER_NOT_FOUND = 0x8055340b; +constexpr int ORBIS_NP_WEBAPI2_PUSH_EVENT_CALLBACK_NOT_FOUND = 0x8055340c; +constexpr int ORBIS_NP_WEBAPI2_HANDLE_NOT_FOUND = 0x8055340d; +constexpr int ORBIS_NP_WEBAPI2_SIGNED_IN_USER_NOT_FOUND = 0x8055340e; +constexpr int ORBIS_NP_WEBAPI2_LIB_CONTEXT_BUSY = 0x8055340f; +constexpr int ORBIS_NP_WEBAPI2_USER_CONTEXT_BUSY = 0x80553410; +constexpr int ORBIS_NP_WEBAPI2_REQUEST_BUSY = 0x80553411; +constexpr int ORBIS_NP_WEBAPI2_INVALID_HTTP_STATUS_CODE = 0x80553412; +constexpr int ORBIS_NP_WEBAPI2_PROHIBITED_HTTP_HEADER = 0x80553413; +constexpr int ORBIS_NP_WEBAPI2_PROHIBITED_FUNCTION_CALL = 0x80553414; +constexpr int ORBIS_NP_WEBAPI2_MULTIPART_PART_NOT_FOUND = 0x80553415; +constexpr int ORBIS_NP_WEBAPI2_PARAMETER_TOO_LONG = 0x80553416; +constexpr int ORBIS_NP_WEBAPI2_HANDLE_BUSY = 0x80553417; +constexpr int ORBIS_NP_WEBAPI2_LIB_CONTEXT_MAX = 0x80553418; +constexpr int ORBIS_NP_WEBAPI2_USER_CONTEXT_MAX = 0x80553419; +constexpr int ORBIS_NP_WEBAPI2_AFTER_SEND = 0x8055341a; +constexpr int ORBIS_NP_WEBAPI2_TIMEOUT = 0x8055341b; +constexpr int ORBIS_NP_WEBAPI2_PUSH_CONTEXT_NOT_FOUND = 0x8055341c; \ No newline at end of file From 12cd27d0b723dccf39482dabf762a0b23312a2da Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:15:06 -0600 Subject: [PATCH 30/83] Fix socket_is_ready (#3912) --- src/core/libraries/network/posix_sockets.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/libraries/network/posix_sockets.cpp b/src/core/libraries/network/posix_sockets.cpp index 7992fa217..f2adccf50 100644 --- a/src/core/libraries/network/posix_sockets.cpp +++ b/src/core/libraries/network/posix_sockets.cpp @@ -204,17 +204,18 @@ static int convertOrbisFlagsToPosix(int sock_type, int sce_flags) { // On Windows, MSG_DONTWAIT is not handled natively by recv/send. // This function uses select() with zero timeout to simulate non-blocking behavior. static int socket_is_ready(int sock, bool is_read = true) { - fd_set fds; + fd_set fds{}; FD_ZERO(&fds); FD_SET(sock, &fds); timeval timeout{0, 0}; int res = select(sock + 1, is_read ? &fds : nullptr, is_read ? nullptr : &fds, nullptr, &timeout); - if (res == 0) - return ORBIS_NET_ERROR_EWOULDBLOCK; - else if (res < 0) + if (res == 0) { + *Libraries::Kernel::__Error() = ORBIS_NET_EWOULDBLOCK; + return -1; + } else if (res < 0) { return ConvertReturnErrorCode(res); - + } return res; } From ae47dd5d54df31535ee5344686236f250b5dc3fd Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:36:36 -0600 Subject: [PATCH 31/83] Handle exceptions from OutputDebugString API. (#3914) Needed after #3900 --- src/core/signals.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/signals.cpp b/src/core/signals.cpp index db6e4b6cc..8df4edea8 100644 --- a/src/core/signals.cpp +++ b/src/core/signals.cpp @@ -33,6 +33,10 @@ static LONG WINAPI SignalHandler(EXCEPTION_POINTERS* pExp) noexcept { case EXCEPTION_ILLEGAL_INSTRUCTION: handled = signals->DispatchIllegalInstruction(pExp); break; + case DBG_PRINTEXCEPTION_C: + case DBG_PRINTEXCEPTION_WIDE_C: + // Used by OutputDebugString functions. + return EXCEPTION_CONTINUE_EXECUTION; default: break; } From 3844a2fb54a2bdafac1a431dd2ee5501c4af95ae Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 11 Jan 2026 02:47:34 -0600 Subject: [PATCH 32/83] Lib.Videodec2: Stub sceVideodec2AllocateComputeQueue to return a valid computeQueue pointer. (#3915) * Stub compute queue output to cpuGpuMemory * Rename namespace This has bugged me for far too long * Oops --- src/core/libraries/libs.cpp | 4 +-- src/core/libraries/videodec/videodec2.cpp | 34 +++++++++++++++++-- src/core/libraries/videodec/videodec2.h | 4 +-- src/core/libraries/videodec/videodec2_avc.h | 4 +-- .../libraries/videodec/videodec2_impl.cpp | 4 +-- src/core/libraries/videodec/videodec2_impl.h | 4 +-- 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 85923134c..9867d132d 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -120,13 +120,13 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::ErrorDialog::RegisterLib(sym); Libraries::ImeDialog::RegisterLib(sym); Libraries::AvPlayer::RegisterLib(sym); - Libraries::Vdec2::RegisterLib(sym); + Libraries::Videodec::RegisterLib(sym); + Libraries::Videodec2::RegisterLib(sym); Libraries::Audio3d::RegisterLib(sym); Libraries::Ime::RegisterLib(sym); Libraries::GameLiveStreaming::RegisterLib(sym); Libraries::SharePlay::RegisterLib(sym); Libraries::Remoteplay::RegisterLib(sym); - Libraries::Videodec::RegisterLib(sym); Libraries::RazorCpu::RegisterLib(sym); Libraries::Move::RegisterLib(sym); Libraries::Fiber::RegisterLib(sym); diff --git a/src/core/libraries/videodec/videodec2.cpp b/src/core/libraries/videodec/videodec2.cpp index a1edb3b68..121818f39 100644 --- a/src/core/libraries/videodec/videodec2.cpp +++ b/src/core/libraries/videodec/videodec2.cpp @@ -8,7 +8,7 @@ #include "core/libraries/videodec/videodec2_impl.h" #include "core/libraries/videodec/videodec_error.h" -namespace Libraries::Vdec2 { +namespace Libraries::Videodec2 { static constexpr u64 kMinimumMemorySize = 16_MB; ///> Fake minimum memory size for querying @@ -34,7 +34,35 @@ s32 PS4_SYSV_ABI sceVideodec2AllocateComputeQueue(const OrbisVideodec2ComputeConfigInfo* computeCfgInfo, const OrbisVideodec2ComputeMemoryInfo* computeMemInfo, OrbisVideodec2ComputeQueue* computeQueue) { - LOG_INFO(Lib_Vdec2, "called"); + LOG_WARNING(Lib_Vdec2, "called"); + if (!computeCfgInfo || !computeMemInfo || !computeQueue) { + LOG_ERROR(Lib_Vdec2, "Invalid arguments"); + return ORBIS_VIDEODEC2_ERROR_ARGUMENT_POINTER; + } + if (computeCfgInfo->thisSize != sizeof(OrbisVideodec2ComputeConfigInfo) || + computeMemInfo->thisSize != sizeof(OrbisVideodec2ComputeMemoryInfo)) { + LOG_ERROR(Lib_Vdec2, "Invalid struct size"); + return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; + } + if (computeCfgInfo->reserved0 != 0 || computeCfgInfo->reserved1 != 0) { + LOG_ERROR(Lib_Vdec2, "Invalid compute config"); + return ORBIS_VIDEODEC2_ERROR_CONFIG_INFO; + } + if (computeCfgInfo->computePipeId > 4) { + LOG_ERROR(Lib_Vdec2, "Invalid compute pipe id"); + return ORBIS_VIDEODEC2_ERROR_COMPUTE_PIPE_ID; + } + if (computeCfgInfo->computeQueueId > 7) { + LOG_ERROR(Lib_Vdec2, "Invalid compute queue id"); + return ORBIS_VIDEODEC2_ERROR_COMPUTE_QUEUE_ID; + } + if (!computeMemInfo->cpuGpuMemory) { + LOG_ERROR(Lib_Vdec2, "Invalid memory pointer"); + return ORBIS_VIDEODEC2_ERROR_MEMORY_POINTER; + } + + // The real library returns a pointer to memory inside cpuGpuMemory + *computeQueue = computeMemInfo->cpuGpuMemory; return ORBIS_OK; } @@ -233,4 +261,4 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) { sceVideodec2GetPictureInfo); } -} // namespace Libraries::Vdec2 +} // namespace Libraries::Videodec2 diff --git a/src/core/libraries/videodec/videodec2.h b/src/core/libraries/videodec/videodec2.h index 0f080129f..0311e4d27 100644 --- a/src/core/libraries/videodec/videodec2.h +++ b/src/core/libraries/videodec/videodec2.h @@ -10,7 +10,7 @@ namespace Core::Loader { class SymbolsResolver; } -namespace Libraries::Vdec2 { +namespace Libraries::Videodec2 { class VdecDecoder; @@ -138,4 +138,4 @@ s32 PS4_SYSV_ABI sceVideodec2GetPictureInfo(const OrbisVideodec2OutputInfo* outp void* p1stPictureInfo, void* p2ndPictureInfo); void RegisterLib(Core::Loader::SymbolsResolver* sym); -} // namespace Libraries::Vdec2 \ No newline at end of file +} // namespace Libraries::Videodec2 \ No newline at end of file diff --git a/src/core/libraries/videodec/videodec2_avc.h b/src/core/libraries/videodec/videodec2_avc.h index 725d2335f..dcf6ae007 100644 --- a/src/core/libraries/videodec/videodec2_avc.h +++ b/src/core/libraries/videodec/videodec2_avc.h @@ -5,7 +5,7 @@ #include "common/types.h" -namespace Libraries::Vdec2 { +namespace Libraries::Videodec2 { struct OrbisVideodec2AvcPictureInfo { u64 thisSize; @@ -127,4 +127,4 @@ struct OrbisVideodec2LegacyAvcPictureInfo { }; static_assert(sizeof(OrbisVideodec2LegacyAvcPictureInfo) == 0x68); -} // namespace Libraries::Vdec2 \ No newline at end of file +} // namespace Libraries::Videodec2 \ No newline at end of file diff --git a/src/core/libraries/videodec/videodec2_impl.cpp b/src/core/libraries/videodec/videodec2_impl.cpp index 2ad3d7e19..08a7f8f00 100644 --- a/src/core/libraries/videodec/videodec2_impl.cpp +++ b/src/core/libraries/videodec/videodec2_impl.cpp @@ -9,7 +9,7 @@ #include "common/support/avdec.h" -namespace Libraries::Vdec2 { +namespace Libraries::Videodec2 { std::vector gPictureInfos; std::vector gLegacyPictureInfos; @@ -278,4 +278,4 @@ AVFrame* VdecDecoder::ConvertNV12Frame(AVFrame& frame) { return nv12_frame; } -} // namespace Libraries::Vdec2 +} // namespace Libraries::Videodec2 diff --git a/src/core/libraries/videodec/videodec2_impl.h b/src/core/libraries/videodec/videodec2_impl.h index 7ee3339db..da16b3baa 100644 --- a/src/core/libraries/videodec/videodec2_impl.h +++ b/src/core/libraries/videodec/videodec2_impl.h @@ -13,7 +13,7 @@ extern "C" { #include } -namespace Libraries::Vdec2 { +namespace Libraries::Videodec2 { extern std::vector gPictureInfos; extern std::vector gLegacyPictureInfos; @@ -37,4 +37,4 @@ private: SwsContext* mSwsContext = nullptr; }; -} // namespace Libraries::Vdec2 \ No newline at end of file +} // namespace Libraries::Videodec2 \ No newline at end of file From 910f7370791c0b55c6db0b1de852ab415e12ca41 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 11 Jan 2026 12:48:41 -0600 Subject: [PATCH 33/83] Core: Add libSceRtc, libSceJpegDec, libSceJpegEnc, and libScePngEnc LLEs (#3918) * Run libSceRtc LLE The more we've used our HLE, the more issues we've had with it. While we debug these bugs, re-enabling LLE will address any regressions the swap caused. * libSceJpegDec LLE Needed for Trackmania until we implement HLE for this library * libScePngEnc LLE Needed for Minecraft until we implement HLE for this library * Update documentation appropriately * libSceJpegEnc LLE By @georgemoralis's request --- README.md | 4 ++-- src/core/libraries/libs.cpp | 4 ---- src/emulator.cpp | 5 +++++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dbb890f87..69ee64b13 100644 --- a/README.md +++ b/README.md @@ -148,9 +148,9 @@ The following firmware modules are supported and must be placed in shadPS4's `sy | Modules | Modules | Modules | Modules | |-------------------------|-------------------------|-------------------------|-------------------------| | libSceCesCs.sprx | libSceFont.sprx | libSceFontFt.sprx | libSceFreeTypeOt.sprx | -| libSceJson.sprx | libSceJson2.sprx | libSceLibcInternal.sprx | libSceNgs2.sprx | +| libSceJpegDec.sprx | libSceJpegEnc.sprx | libSceJson.sprx | libSceJson2.sprx | +| libSceLibcInternal.sprx | libSceNgs2.sprx | libScePngEnc.sprx | libSceRtc.sprx | | libSceUlt.sprx | | | | - > [!Caution] diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 9867d132d..7f679e7c2 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -48,7 +48,6 @@ #include "core/libraries/random/random.h" #include "core/libraries/razor_cpu/razor_cpu.h" #include "core/libraries/remote_play/remoteplay.h" -#include "core/libraries/rtc/rtc.h" #include "core/libraries/rudp/rudp.h" #include "core/libraries/save_data/dialog/savedatadialog.h" #include "core/libraries/save_data/savedata.h" @@ -72,7 +71,6 @@ #include "core/libraries/web_browser_dialog/webbrowserdialog.h" #include "core/libraries/zlib/zlib_sce.h" #include "fiber/fiber.h" -#include "jpeg/jpegenc.h" namespace Libraries { @@ -130,7 +128,6 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::RazorCpu::RegisterLib(sym); Libraries::Move::RegisterLib(sym); Libraries::Fiber::RegisterLib(sym); - Libraries::JpegEnc::RegisterLib(sym); Libraries::Mouse::RegisterLib(sym); Libraries::WebBrowserDialog::RegisterLib(sym); Libraries::Zlib::RegisterLib(sym); @@ -143,7 +140,6 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::CompanionHttpd::RegisterLib(sym); Libraries::CompanionUtil::RegisterLib(sym); Libraries::Voice::RegisterLib(sym); - Libraries::Rtc::RegisterLib(sym); Libraries::Rudp::RegisterLib(sym); Libraries::VrTracker::RegisterLib(sym); diff --git a/src/emulator.cpp b/src/emulator.cpp index 4947beeb8..44f8b0e72 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -34,6 +34,7 @@ #include "core/libraries/disc_map/disc_map.h" #include "core/libraries/font/font.h" #include "core/libraries/font/fontft.h" +#include "core/libraries/jpeg/jpegenc.h" #include "core/libraries/libc_internal/libc_internal.h" #include "core/libraries/libs.h" #include "core/libraries/ngs2/ngs2.h" @@ -523,6 +524,10 @@ void Emulator::LoadSystemModules(const std::string& game_serial) { constexpr auto ModulesToLoad = std::to_array( {{"libSceNgs2.sprx", &Libraries::Ngs2::RegisterLib}, {"libSceUlt.sprx", nullptr}, + {"libSceRtc.sprx", &Libraries::Rtc::RegisterLib}, + {"libSceJpegDec.sprx", nullptr}, + {"libSceJpegEnc.sprx", &Libraries::JpegEnc::RegisterLib}, + {"libScePngEnc.sprx", nullptr}, {"libSceJson.sprx", nullptr}, {"libSceJson2.sprx", nullptr}, {"libSceLibcInternal.sprx", &Libraries::LibcInternal::RegisterLib}, From 540fdcc8124f3822b800edbbdf2ce7fe0521d1de Mon Sep 17 00:00:00 2001 From: b7k Date: Mon, 12 Jan 2026 10:21:52 +0100 Subject: [PATCH 34/83] changing the mouse speed does not affect the mouse speed offset (#3917) --- src/input/input_mouse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/input_mouse.cpp b/src/input/input_mouse.cpp index cbb07721b..996c35ef9 100644 --- a/src/input/input_mouse.cpp +++ b/src/input/input_mouse.cpp @@ -70,7 +70,7 @@ void EmulateJoystick(GameController* controller, u32 interval) { SDL_GetRelativeMouseState(&d_x, &d_y); float output_speed = - SDL_clamp((sqrt(d_x * d_x + d_y * d_y) + mouse_speed_offset * 128) * mouse_speed, + SDL_clamp(sqrt(d_x * d_x + d_y * d_y) * mouse_speed + mouse_speed_offset * 128, mouse_deadzone_offset * 128, 128.0); float angle = atan2(d_y, d_x); From 108cefaf537728b3d654457eb8c70b4b3afb4d00 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:30:56 -0600 Subject: [PATCH 35/83] Better message for missing game trophies directory (#3922) Also decreased the log level from critical to warning, as all this affects is the ability to earn trophies. --- src/core/file_format/trp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/file_format/trp.cpp b/src/core/file_format/trp.cpp index 9d37b957e..ab873d451 100644 --- a/src/core/file_format/trp.cpp +++ b/src/core/file_format/trp.cpp @@ -59,7 +59,7 @@ static void hexToBytes(const char* hex, unsigned char* dst) { bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string titleId) { std::filesystem::path gameSysDir = trophyPath / "sce_sys/trophy/"; if (!std::filesystem::exists(gameSysDir)) { - LOG_CRITICAL(Common_Filesystem, "Game sce_sys directory doesn't exist"); + LOG_WARNING(Common_Filesystem, "Game trophy directory doesn't exist"); return false; } From acb8d066366ce44fc9b92d612f40bf56d64f4c63 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:32:17 -0600 Subject: [PATCH 36/83] Lib.Audio3d: sceAudio3dGetDefaultOpenParameters fix (#3923) * OrbisAudio3dOpenParameters struct fix Not sure why we have the extra filler, but decomp suggests it shouldn't exist. This fixes stack_chk_fail issues in audio3d using titles. * Bring back filler, only copy 0x20 bytes. The library accepts variations on struct size, with the maximum size being the 0x28 size our current struct has. This fixes the issue without potentially breaking the struct. * Fix memcpy Prevent OOB read --- src/core/libraries/audio3d/audio3d.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/audio3d/audio3d.cpp b/src/core/libraries/audio3d/audio3d.cpp index bac497840..2ddbcd890 100644 --- a/src/core/libraries/audio3d/audio3d.cpp +++ b/src/core/libraries/audio3d/audio3d.cpp @@ -189,7 +189,7 @@ s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray() { s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* params) { LOG_DEBUG(Lib_Audio3d, "called"); if (params) { - *params = OrbisAudio3dOpenParameters{ + auto default_params = OrbisAudio3dOpenParameters{ .size_this = 0x20, .granularity = 0x100, .rate = OrbisAudio3dRate::ORBIS_AUDIO3D_RATE_48000, @@ -197,6 +197,7 @@ s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* .queue_depth = 2, .buffer_mode = OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH, }; + memcpy(params, &default_params, 0x20); } return ORBIS_OK; } @@ -445,7 +446,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortOpen(const OrbisUserServiceUserId user_id, } *port_id = id; - std::memcpy(&state->ports[id].parameters, parameters, sizeof(OrbisAudio3dOpenParameters)); + std::memcpy(&state->ports[id].parameters, parameters, parameters->size_this); return ORBIS_OK; } From 1a99ab7b098680bf9b4d6539d02f548cc24b89e3 Mon Sep 17 00:00:00 2001 From: Vladislav Mikhalin Date: Tue, 13 Jan 2026 12:08:46 +0300 Subject: [PATCH 37/83] ajm: fix init params initialization (#3924) --- src/core/libraries/ajm/ajm_batch.cpp | 4 +--- src/core/libraries/ajm/ajm_batch.h | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/core/libraries/ajm/ajm_batch.cpp b/src/core/libraries/ajm/ajm_batch.cpp index 61f80e482..3ab2ed4ab 100644 --- a/src/core/libraries/ajm/ajm_batch.cpp +++ b/src/core/libraries/ajm/ajm_batch.cpp @@ -280,9 +280,7 @@ AjmJob AjmJobFromBatchBuffer(u32 instance_id, AjmBatchBuffer batch_buffer) { job.input.resample_parameters = input_batch.Consume(); } if (True(control_flags & AjmJobControlFlags::Initialize)) { - job.input.init_params = AjmDecAt9InitializeParameters{}; - std::memcpy(&job.input.init_params.value(), input_batch.GetCurrent(), - input_batch.BytesRemaining()); + job.input.init_params = input_batch.Consume(); } } diff --git a/src/core/libraries/ajm/ajm_batch.h b/src/core/libraries/ajm/ajm_batch.h index 7f8890898..c18e9efbf 100644 --- a/src/core/libraries/ajm/ajm_batch.h +++ b/src/core/libraries/ajm/ajm_batch.h @@ -21,7 +21,7 @@ namespace Libraries::Ajm { struct AjmJob { struct Input { - std::optional init_params; + std::optional init_params; std::optional resample_parameters; std::optional statistics_engine_parameters; std::optional format; From cdf3c468b6a8119db41507fd2a84a7c7824cbe75 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 14 Jan 2026 18:07:44 +0200 Subject: [PATCH 38/83] Added libSceAudiodec to lle modules list (#3916) * added libSceAudiodec to lle modules list * crappy float resample , use it at your own risk * clang * adjustments to aac --------- Co-authored-by: Vladislav Mikhalin --- README.md | 2 +- src/core/libraries/ajm/ajm.cpp | 4 +-- src/core/libraries/ajm/ajm_aac.cpp | 48 ++++++++++++++++++++++-------- src/core/libraries/ajm/ajm_aac.h | 14 ++++----- src/emulator.cpp | 1 + 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 69ee64b13..e43a2408d 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ The following firmware modules are supported and must be placed in shadPS4's `sy | libSceCesCs.sprx | libSceFont.sprx | libSceFontFt.sprx | libSceFreeTypeOt.sprx | | libSceJpegDec.sprx | libSceJpegEnc.sprx | libSceJson.sprx | libSceJson2.sprx | | libSceLibcInternal.sprx | libSceNgs2.sprx | libScePngEnc.sprx | libSceRtc.sprx | -| libSceUlt.sprx | | | | +| libSceUlt.sprx | libSceAudiodec.sprx | | | > [!Caution] diff --git a/src/core/libraries/ajm/ajm.cpp b/src/core/libraries/ajm/ajm.cpp index b64bb47fd..2bec1bf0f 100644 --- a/src/core/libraries/ajm/ajm.cpp +++ b/src/core/libraries/ajm/ajm.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/logging/log.h" @@ -34,7 +34,7 @@ u32 GetChannelMask(u32 num_channels) { case 8: return ORBIS_AJM_CHANNELMASK_7POINT1; default: - UNREACHABLE(); + UNREACHABLE_MSG("Unexpected number of channels: {}", num_channels); } } diff --git a/src/core/libraries/ajm/ajm_aac.cpp b/src/core/libraries/ajm/ajm_aac.cpp index b96394b72..061b77890 100644 --- a/src/core/libraries/ajm/ajm_aac.cpp +++ b/src/core/libraries/ajm/ajm_aac.cpp @@ -5,20 +5,45 @@ #include "ajm_aac.h" #include "ajm_result.h" +#include // using this internal header to manually configure the decoder in RAW mode #include "externals/aacdec/fdk-aac/libAACdec/src/aacdecoder.h" -#include -#include +#include // std::transform +#include // std::back_inserter +#include namespace Libraries::Ajm { +std::span AjmAacDecoder::GetOuputPcm(u32 skipped_pcm, u32 max_pcm) const { + const auto pcm_data = std::span(m_pcm_buffer).subspan(skipped_pcm); + return pcm_data.subspan(0, std::min(pcm_data.size(), max_pcm)); +} + +template <> +size_t AjmAacDecoder::WriteOutputSamples(SparseOutputBuffer& out, std::span pcm) { + if (pcm.empty()) { + return 0; + } + + m_resample_buffer.clear(); + constexpr float inv_scale = 1.0f / std::numeric_limits::max(); + std::transform(pcm.begin(), pcm.end(), std::back_inserter(m_resample_buffer), + [](auto sample) { return float(sample) * inv_scale; }); + + return out.Write(std::span(m_resample_buffer)); +} + AjmAacDecoder::AjmAacDecoder(AjmFormatEncoding format, AjmAacCodecFlags flags, u32 channels) - : m_format(format), m_flags(flags), m_channels(channels), m_pcm_buffer(2048 * 8), - m_skip_frames(True(flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2) {} + : m_format(format), m_flags(flags), m_channels(channels), m_pcm_buffer(1024 * 8), + m_skip_frames(True(flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2) { + m_resample_buffer.reserve(m_pcm_buffer.size()); +} AjmAacDecoder::~AjmAacDecoder() { - aacDecoder_Close(m_decoder); + if (m_decoder) { + aacDecoder_Close(m_decoder); + } } TRANSPORT_TYPE TransportTypeFromConfigType(ConfigType config_type) { @@ -98,7 +123,7 @@ AjmSidebandFormat AjmAacDecoder::GetFormat() const { .num_channels = static_cast(info->numChannels), .channel_mask = GetChannelMask(info->numChannels), .sampl_freq = static_cast(info->sampleRate), - .sample_encoding = m_format, // AjmFormatEncoding + .sample_encoding = m_format, .bitrate = static_cast(info->bitRate), }; } @@ -130,8 +155,7 @@ DecoderResult AjmAacDecoder::ProcessData(std::span& input, SparseOutputBuffe const UINT sizes[] = {static_cast(input.size())}; UINT valid = sizes[0]; aacDecoder_Fill(m_decoder, buffers, sizes, &valid); - auto ret = aacDecoder_DecodeFrame(m_decoder, reinterpret_cast(m_pcm_buffer.data()), - m_pcm_buffer.size() / 2, 0); + auto ret = aacDecoder_DecodeFrame(m_decoder, m_pcm_buffer.data(), m_pcm_buffer.size(), 0); switch (ret) { case AAC_DEC_OK: @@ -167,16 +191,16 @@ DecoderResult AjmAacDecoder::ProcessData(std::span& input, SparseOutputBuffe gapless.init.total_samples != 0 ? gapless.current.total_samples : info->aacSamplesPerFrame; size_t pcm_written = 0; + auto pcm = GetOuputPcm(skip_samples * info->numChannels, max_samples * info->numChannels); switch (m_format) { case AjmFormatEncoding::S16: - pcm_written = WriteOutputSamples(output, skip_samples * info->numChannels, - max_samples * info->numChannels); + pcm_written = output.Write(pcm); break; case AjmFormatEncoding::S32: UNREACHABLE_MSG("NOT IMPLEMENTED"); break; case AjmFormatEncoding::Float: - UNREACHABLE_MSG("NOT IMPLEMENTED"); + pcm_written = WriteOutputSamples(output, pcm); break; default: UNREACHABLE(); @@ -191,4 +215,4 @@ DecoderResult AjmAacDecoder::ProcessData(std::span& input, SparseOutputBuffe return result; } -} // namespace Libraries::Ajm +} // namespace Libraries::Ajm \ No newline at end of file diff --git a/src/core/libraries/ajm/ajm_aac.h b/src/core/libraries/ajm/ajm_aac.h index 7ca8ecbf8..4ff55d843 100644 --- a/src/core/libraries/ajm/ajm_aac.h +++ b/src/core/libraries/ajm/ajm_aac.h @@ -52,22 +52,18 @@ private: }; template - size_t WriteOutputSamples(SparseOutputBuffer& output, u32 skipped_pcm, u32 max_pcm) { - std::span pcm_data{reinterpret_cast(m_pcm_buffer.data()), - m_pcm_buffer.size() / sizeof(T)}; - pcm_data = pcm_data.subspan(skipped_pcm); - const auto pcm_size = std::min(u32(pcm_data.size()), max_pcm); - return output.Write(pcm_data.subspan(0, pcm_size)); - } + size_t WriteOutputSamples(SparseOutputBuffer& output, std::span pcm); + std::span GetOuputPcm(u32 skipped_pcm, u32 max_pcm) const; const AjmFormatEncoding m_format; const AjmAacCodecFlags m_flags; const u32 m_channels; - std::vector m_pcm_buffer; + std::vector m_pcm_buffer; + std::vector m_resample_buffer; u32 m_skip_frames = 0; InitializeParameters m_init_params = {}; AAC_DECODER_INSTANCE* m_decoder = nullptr; }; -} // namespace Libraries::Ajm +} // namespace Libraries::Ajm \ No newline at end of file diff --git a/src/emulator.cpp b/src/emulator.cpp index 44f8b0e72..263bd9c2b 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -532,6 +532,7 @@ void Emulator::LoadSystemModules(const std::string& game_serial) { {"libSceJson2.sprx", nullptr}, {"libSceLibcInternal.sprx", &Libraries::LibcInternal::RegisterLib}, {"libSceCesCs.sprx", nullptr}, + {"libSceAudiodec.sprx", nullptr}, {"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont}, {"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt}, {"libSceFreeTypeOt.sprx", nullptr}}); From 11ee79a33348de7a55ab8d7b688144d5b8a0ce55 Mon Sep 17 00:00:00 2001 From: baggins183 Date: Wed, 14 Jan 2026 23:25:09 -0800 Subject: [PATCH 39/83] shader_recompiler: some fixes for tess shaders (#3926) When walking the users of special constants which form LDS addresses: -ignore when a user contributes to the wrong operand of an LDS inst, for example the data operand of WriteShared* instead of the address operand. This can mistakenly happen due to phi nodes. -don't use flags to stash temp info about phis, since flags may already be in use. Use a separate map. --- src/shader_recompiler/ir/attribute.cpp | 4 +- .../ir/passes/hull_shader_transform.cpp | 62 +++++++++++-------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/shader_recompiler/ir/attribute.cpp b/src/shader_recompiler/ir/attribute.cpp index 382f9b1d9..84a9fafeb 100644 --- a/src/shader_recompiler/ir/attribute.cpp +++ b/src/shader_recompiler/ir/attribute.cpp @@ -153,9 +153,9 @@ std::string NameOf(Attribute attribute) { case Attribute::TessellationEvaluationPointV: return "TessellationEvaluationPointV"; case Attribute::PackedHullInvocationInfo: - return "OffChipLdsBase"; - case Attribute::OffChipLdsBase: return "PackedHullInvocationInfo"; + case Attribute::OffChipLdsBase: + return "OffChipLdsBase"; case Attribute::TessFactorsBufferBase: return "TessFactorsBufferBase"; case Attribute::PointSize: diff --git a/src/shader_recompiler/ir/passes/hull_shader_transform.cpp b/src/shader_recompiler/ir/passes/hull_shader_transform.cpp index 48b496727..d975c47ea 100644 --- a/src/shader_recompiler/ir/passes/hull_shader_transform.cpp +++ b/src/shader_recompiler/ir/passes/hull_shader_transform.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include "common/assert.h" #include "shader_recompiler/info.h" #include "shader_recompiler/ir/attribute.h" @@ -189,7 +190,7 @@ std::optional FindTessConstantSharp(IR::Inst* read_const_buff // Walker that helps deduce what type of attribute a DS instruction is reading // or writing, which could be an input control point, output control point, // or per-patch constant (PatchConst). -// For certain ReadConstBuffer instructions using the tess constants V#,, we visit the users +// For certain ReadConstBuffer instructions using the tess constants V#, we visit the users // recursively and increment a counter on the Load/WriteShared users. // Namely NumPatch (from m_hsNumPatch), HsOutputBase (m_hsOutputBase), // and PatchConstBase (m_patchConstBase). @@ -200,9 +201,11 @@ std::optional FindTessConstantSharp(IR::Inst* read_const_buff // // TODO: this will break if AMD compiler used distributive property like // TcsNumPatches * (ls_stride * #input_cp_in_patch + hs_cp_stride * #output_cp_in_patch) +// +// Assert if the region is ambiguous due to phi nodes in the addr calculation for a DS instruction, class TessConstantUseWalker { public: - void MarkTessAttributeUsers(IR::Inst* read_const_buffer, TessConstantAttribute attr) { + void WalkUsersOfTessConstant(IR::Inst* read_const_buffer, TessConstantAttribute attr) { u32 inc; switch (attr) { case TessConstantAttribute::HsNumPatch: @@ -217,14 +220,19 @@ public: } for (IR::Use use : read_const_buffer->Uses()) { - MarkTessAttributeUsersHelper(use, inc); + WalkUsersOfTessConstantHelper(use, inc, false); } ++seq_num; } private: - void MarkTessAttributeUsersHelper(IR::Use use, u32 inc) { + struct PhiInfo { + u32 seq_num; + u32 unique_edge; + }; + + void WalkUsersOfTessConstantHelper(IR::Use use, u32 inc, bool propagateError) { IR::Inst* inst = use.user; switch (use.user->GetOpcode()) { @@ -232,38 +240,37 @@ private: case IR::Opcode::LoadSharedU64: case IR::Opcode::WriteSharedU32: case IR::Opcode::WriteSharedU64: { - u32 counter = inst->Flags(); - inst->SetFlags(counter + inc); - // Stop here - return; + bool is_addr_operand = use.operand == 0; + if (is_addr_operand) { + u32 counter = inst->Flags(); + inst->SetFlags(counter + inc); + ASSERT_MSG(!propagateError, "LDS instruction {} accesses ambiguous attribute type", + fmt::ptr(use.user)); + // Stop here + return; + } } case IR::Opcode::Phi: { - struct PhiCounter { - u16 seq_num; - u16 unique_edge; - }; - - PhiCounter count = inst->Flags(); - ASSERT_MSG(count.seq_num == 0 || count.unique_edge == use.operand); + auto it = phi_infos.find(use.user); // the point of seq_num is to tell us if we've already traversed this - // phi on the current walk. Alternatively we could keep a set of phi's - // seen on the current walk. This is to handle phi cycles - if (count.seq_num == 0) { + // phi on the current walk to handle phi cycles + if (it == phi_infos.end()) { // First time we've encountered this phi - count.seq_num = seq_num; // Mark the phi as having been traversed originally through this edge - count.unique_edge = use.operand; - } else if (count.seq_num < seq_num) { - count.seq_num = seq_num; + phi_infos[inst] = {.seq_num = seq_num, + .unique_edge = static_cast(use.operand)}; + } else if (it->second.seq_num < seq_num) { + it->second.seq_num = seq_num; // For now, assume we are visiting this phi via the same edge // as on other walks. If not, some dataflow analysis might be necessary - ASSERT(count.unique_edge == use.operand); + if (it->second.unique_edge != use.operand) { + propagateError = true; + } } else { - // count.seq_num == seq_num + ASSERT(it->second.seq_num == seq_num); // there's a cycle, and we've already been here on this walk return; } - inst->SetFlags(count); break; } default: @@ -271,10 +278,11 @@ private: } for (IR::Use use : inst->Uses()) { - MarkTessAttributeUsersHelper(use, inc); + WalkUsersOfTessConstantHelper(use, inc, propagateError); } } + std::unordered_map phi_infos; u32 seq_num{1u}; }; @@ -690,7 +698,7 @@ void TessellationPreprocess(IR::Program& program, RuntimeInfo& runtime_info) { case TessConstantAttribute::HsNumPatch: case TessConstantAttribute::HsOutputBase: case TessConstantAttribute::PatchConstBase: - walker.MarkTessAttributeUsers(&inst, tess_const_attr); + walker.WalkUsersOfTessConstant(&inst, tess_const_attr); // We should be able to safely set these to 0 so that indexing happens only // within the local patch in the recompiled Vulkan shader. This assumes // these values only contribute to address calculations for in/out From dce9c04383bca01325d57b687101986766183ef3 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 16 Jan 2026 18:33:27 +0200 Subject: [PATCH 40/83] AvPlayer fix (#3929) * improved path detection * clang is pretty * improved * improved? * finished? --- src/core/libraries/avplayer/avplayer_common.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/core/libraries/avplayer/avplayer_common.cpp b/src/core/libraries/avplayer/avplayer_common.cpp index f42f690ed..4b42fc8c2 100644 --- a/src/core/libraries/avplayer/avplayer_common.cpp +++ b/src/core/libraries/avplayer/avplayer_common.cpp @@ -30,7 +30,15 @@ AvPlayerSourceType GetSourceType(std::string_view path) { } // schema://server.domain/path/to/file.ext/and/beyond -> .ext/and/beyond - auto ext = name.substr(name.rfind('.')); + + // Find extension dot + auto dot_pos = name.rfind('.'); + if (dot_pos == std::string_view::npos) { + return AvPlayerSourceType::Unknown; + } + + // Extract extension (".ext/anything" or ".ext") + auto ext = name.substr(dot_pos); if (ext.empty()) { return AvPlayerSourceType::Unknown; } From 220e5f67e79db7baf666d924366d9f5301541cf0 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 17 Jan 2026 21:33:14 +0200 Subject: [PATCH 41/83] NetFixes : workaround for Epolls on P2P sockets (#3933) * return error on P2P sockets * error message improved --- src/core/libraries/network/net.cpp | 33 ++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/core/libraries/network/net.cpp b/src/core/libraries/network/net.cpp index a365d407b..9a4f05a5e 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -655,10 +655,17 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or switch (file->type) { case Core::FileSys::FileType::Socket: { + auto native_handle = file->socket->Native(); + if (!native_handle) { + // P2P socket, cannot be added to epoll + LOG_ERROR(Lib_Net, "P2P socket cannot be added to epoll (unimplemented)"); + *sceNetErrnoLoc() = ORBIS_NET_EBADF; + return ORBIS_NET_ERROR_EBADF; + } + epoll_event native_event = {.events = ConvertEpollEventsIn(event->events), .data = {.fd = id}}; - ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_ADD, *file->socket->Native(), - &native_event) == 0); + ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_ADD, *native_handle, &native_event) == 0); epoll->events.emplace_back(id, *event); break; } @@ -696,10 +703,17 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or switch (file->type) { case Core::FileSys::FileType::Socket: { + auto native_handle = file->socket->Native(); + if (!native_handle) { + // P2P socket, cannot be modified in epoll + LOG_ERROR(Lib_Net, "P2P socket cannot be modified in epoll (unimplemented)"); + *sceNetErrnoLoc() = ORBIS_NET_EBADF; + return ORBIS_NET_ERROR_EBADF; + } + epoll_event native_event = {.events = ConvertEpollEventsIn(event->events), .data = {.fd = id}}; - ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_MOD, *file->socket->Native(), - &native_event) == 0); + ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_MOD, *native_handle, &native_event) == 0); *it = {id, *event}; break; } @@ -731,8 +745,15 @@ int PS4_SYSV_ABI sceNetEpollControl(OrbisNetId epollid, OrbisNetEpollFlag op, Or switch (file->type) { case Core::FileSys::FileType::Socket: { - ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_DEL, *file->socket->Native(), nullptr) == - 0); + auto native_handle = file->socket->Native(); + if (!native_handle) { + // P2P socket, cannot be removed from epoll + LOG_ERROR(Lib_Net, "P2P socket cannot be removed from epoll (unimplemented)"); + *sceNetErrnoLoc() = ORBIS_NET_EBADF; + return ORBIS_NET_ERROR_EBADF; + } + + ASSERT(epoll_ctl(epoll->epoll_fd, EPOLL_CTL_DEL, *native_handle, nullptr) == 0); epoll->events.erase(it); break; } From 950d390daf27ed6ddf84fe8afe3a95d78bf9a1d4 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 17 Jan 2026 23:54:17 +0200 Subject: [PATCH 42/83] implemented sceImeDialogGetPanelSizeExtended (#3934) --- src/core/libraries/ime/ime_dialog.cpp | 196 +++++++++++++++++++++++++- src/core/libraries/ime/ime_dialog.h | 6 +- 2 files changed, 197 insertions(+), 5 deletions(-) diff --git a/src/core/libraries/ime/ime_dialog.cpp b/src/core/libraries/ime/ime_dialog.cpp index 8ee4a0407..226570bd6 100644 --- a/src/core/libraries/ime/ime_dialog.cpp +++ b/src/core/libraries/ime/ime_dialog.cpp @@ -115,9 +115,199 @@ Error PS4_SYSV_ABI sceImeDialogGetPanelSize(const OrbisImeDialogParam* param, u3 return Error::OK; } -int PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended() { - LOG_ERROR(Lib_ImeDialog, "(STUBBED) called"); - return ORBIS_OK; +Error PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(const OrbisImeDialogParam* param, + const OrbisImeParamExtended* extended, + u32* width, u32* height) { + if (!param || !width || !height) { + return Error::INVALID_ADDRESS; + } + + // Check parameter bounds + if (static_cast(param->type) > 4) { + return Error::INVALID_ARG; + } + + if (extended) { + // Check panel priority for full panel mode (Accent = 3) + if (extended->priority == OrbisImePanelPriority::Accent) { + // Full panel mode - return maximum size + if ((param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != + OrbisImeOption::DEFAULT) { + *width = 2560; // For 4K/5K displays + *height = 1440; + } else { + *width = 1920; + *height = 1080; + } + LOG_DEBUG(Lib_ImeDialog, "Full panel mode: width={}, height={}", *width, *height); + return Error::OK; + } + } + + // First get the base panel size from the basic function + Error result = sceImeDialogGetPanelSize(param, width, height); + if (result != Error::OK) { + return result; + } + + // Adjust based on IME type + switch (param->type) { + case OrbisImeType::Default: + case OrbisImeType::BasicLatin: + case OrbisImeType::Url: + case OrbisImeType::Mail: + // Standard IME types + if ((param->option & OrbisImeOption::PASSWORD) != OrbisImeOption::DEFAULT) { + *height = *height + 20; + } + if ((param->option & OrbisImeOption::MULTILINE) != OrbisImeOption::DEFAULT) { + *height = *height * 3 / 2; + } + break; + + case OrbisImeType::Number: + *width = *width * 3 / 4; + *height = *height * 2 / 3; + break; + + default: + // Unknown type, use default size + break; + } + + // Apply extended options if provided + if (extended) { + // Handle extended option flags + if ((extended->option & OrbisImeExtOption::PRIORITY_FULL_WIDTH) != + OrbisImeExtOption::DEFAULT) { + // Full width priority + bool use_2k = (param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != + OrbisImeOption::DEFAULT; + *width = use_2k ? 1200 : 800; + LOG_DEBUG(Lib_ImeDialog, "Full width priority: width={}", *width); + } + + if ((extended->option & OrbisImeExtOption::PRIORITY_FIXED_PANEL) != + OrbisImeExtOption::DEFAULT) { + // Fixed panel size + *width = 600; + *height = 400; + LOG_DEBUG(Lib_ImeDialog, "Fixed panel: width={}, height={}", *width, *height); + } + + switch (extended->priority) { + case OrbisImePanelPriority::Alphabet: + *width = 600; + *height = 400; + break; + + case OrbisImePanelPriority::Symbol: + *width = 500; + *height = 300; + break; + + case OrbisImePanelPriority::Accent: + // Already handled + break; + + case OrbisImePanelPriority::Default: + default: + // Use calculated sizes + break; + } + + if ((extended->option & OrbisImeExtOption::INIT_EXT_KEYBOARD_MODE) != + OrbisImeExtOption::DEFAULT) { + if (extended->ext_keyboard_mode != 0) { + // Check for high-res mode flags + if ((extended->ext_keyboard_mode & + static_cast( + OrbisImeInitExtKeyboardMode::INPUT_METHOD_STATE_FULL_WIDTH)) != 0) { + *width = *width * 5 / 4; + } + + // Check for format characters enabled + if ((extended->ext_keyboard_mode & + static_cast( + OrbisImeInitExtKeyboardMode::ENABLE_FORMAT_CHARACTERS)) != 0) { + *height = *height + 30; + } + } + } + + // Check for accessibility mode + if ((extended->option & OrbisImeExtOption::ENABLE_ACCESSIBILITY) != + OrbisImeExtOption::DEFAULT) { + *width = *width * 5 / 4; // 25% larger for accessibility + *height = *height * 5 / 4; + LOG_DEBUG(Lib_ImeDialog, "Accessibility mode: width={}, height={}", *width, *height); + } + + // Check for forced accessibility panel + if ((extended->option & OrbisImeExtOption::ACCESSIBILITY_PANEL_FORCED) != + OrbisImeExtOption::DEFAULT) { + *width = 800; + *height = 600; + LOG_DEBUG(Lib_ImeDialog, "Forced accessibility panel: width={}, height={}", *width, + *height); + } + } + + if ((param->option & static_cast(0x8)) != OrbisImeOption::DEFAULT) { //? + *width *= 2; + *height *= 2; + LOG_DEBUG(Lib_ImeDialog, "Size mode: width={}, height={}", *width, *height); + } + + // Adjust for supported languages if specified + if (param->supported_languages != static_cast(0)) { + // Check if CJK languages are supported (need larger panel) + OrbisImeLanguage cjk_mask = OrbisImeLanguage::JAPANESE | OrbisImeLanguage::KOREAN | + OrbisImeLanguage::SIMPLIFIED_CHINESE | + OrbisImeLanguage::TRADITIONAL_CHINESE; + + if ((param->supported_languages & cjk_mask) != static_cast(0)) { + *width = *width * 5 / 4; // 25% wider for CJK input + *height = *height * 6 / 5; // 20% taller + LOG_DEBUG(Lib_ImeDialog, "CJK language support: width={}, height={}", *width, *height); + } + + // Check if Arabic is supported (right-to-left layout) + if ((param->supported_languages & OrbisImeLanguage::ARABIC) != + static_cast(0)) { + *width = *width * 11 / 10; // 10% wider for Arabic + LOG_DEBUG(Lib_ImeDialog, "Arabic language support: width={}", *width); + } + } + + // Ensure minimum sizes + const uint32_t min_width = 200; + const uint32_t min_height = 100; + if (*width < min_width) + *width = min_width; + if (*height < min_height) + *height = min_height; + + // Ensure maximum sizes (don't exceed screen bounds) + bool use_2k_coords = + (param->option & OrbisImeOption::USE_OVER_2K_COORDINATES) != OrbisImeOption::DEFAULT; + const uint32_t max_width = use_2k_coords ? 2560 : 1920; + const uint32_t max_height = use_2k_coords ? 1440 : 1080; + if (*width > max_width) + *width = max_width; + if (*height > max_height) + *height = max_height; + + // Check for fixed position option + if ((param->option & OrbisImeOption::FIXED_POSITION) != OrbisImeOption::DEFAULT) { + if (*width > 800) + *width = 800; + if (*height > 600) + *height = 600; + } + + LOG_DEBUG(Lib_ImeDialog, "Final panel size: width={}, height={}", *width, *height); + return Error::OK; } Error PS4_SYSV_ABI sceImeDialogGetResult(OrbisImeDialogResult* result) { diff --git a/src/core/libraries/ime/ime_dialog.h b/src/core/libraries/ime/ime_dialog.h index 569bdf3c0..532762ccc 100644 --- a/src/core/libraries/ime/ime_dialog.h +++ b/src/core/libraries/ime/ime_dialog.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 @@ -37,7 +37,9 @@ int PS4_SYSV_ABI sceImeDialogGetCurrentStarState(); int PS4_SYSV_ABI sceImeDialogGetPanelPositionAndForm(); Error PS4_SYSV_ABI sceImeDialogGetPanelSize(const OrbisImeDialogParam* param, u32* width, u32* height); -int PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(); +Error PS4_SYSV_ABI sceImeDialogGetPanelSizeExtended(const OrbisImeDialogParam* param, + const OrbisImeParamExtended* extended, + u32* width, u32* height); Error PS4_SYSV_ABI sceImeDialogGetResult(OrbisImeDialogResult* result); OrbisImeDialogStatus PS4_SYSV_ABI sceImeDialogGetStatus(); Error PS4_SYSV_ABI sceImeDialogInit(OrbisImeDialogParam* param, OrbisImeParamExtended* extended); From c898071b723d6e8b539d034d11d6f16b011cbee9 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Mon, 19 Jan 2026 18:49:57 +0200 Subject: [PATCH 43/83] Introducing key_manager for storing encryption keys . (#3935) * Introducing key_manager for storing encryption keys . Currently only trophy key is neccesary * keep gcc happy? * addded logging to keymanager * revert file * added npbind file format and rewrote part of trp file format --- CMakeLists.txt | 6 +- src/common/key_manager.cpp | 161 ++++++++++++++++++ src/common/key_manager.h | 79 +++++++++ src/common/logging/filter.cpp | 2 + src/common/logging/types.h | 2 + src/core/file_format/npbind.cpp | 114 +++++++++++++ src/core/file_format/npbind.h | 87 ++++++++++ src/core/file_format/trp.cpp | 280 +++++++++++++++++++++++--------- src/core/file_format/trp.h | 11 +- src/main.cpp | 11 ++ 10 files changed, 675 insertions(+), 78 deletions(-) create mode 100644 src/common/key_manager.cpp create mode 100644 src/common/key_manager.h create mode 100644 src/core/file_format/npbind.cpp create mode 100644 src/core/file_format/npbind.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ab144aa37..929e0ebc7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 # Version 3.24 needed for FetchContent OVERRIDE_FIND_PACKAGE @@ -744,6 +744,8 @@ set(COMMON src/common/logging/backend.cpp src/common/memory_patcher.cpp ${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp src/common/scm_rev.h + src/common/key_manager.cpp + src/common/key_manager.h ) if (ENABLE_DISCORD_RPC) @@ -787,6 +789,8 @@ set(CORE src/core/aerolib/stubs.cpp src/core/file_format/playgo_chunk.h src/core/file_format/trp.cpp src/core/file_format/trp.h + src/core/file_format/npbind.cpp + src/core/file_format/npbind.h src/core/file_sys/fs.cpp src/core/file_sys/fs.h src/core/ipc/ipc.cpp diff --git a/src/common/key_manager.cpp b/src/common/key_manager.cpp new file mode 100644 index 000000000..cd0f668bf --- /dev/null +++ b/src/common/key_manager.cpp @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include "common/logging/log.h" +#include "key_manager.h" +#include "path_util.h" + +std::shared_ptr KeyManager::s_instance = nullptr; +std::mutex KeyManager::s_mutex; + +// ------------------- Constructor & Singleton ------------------- +KeyManager::KeyManager() { + SetDefaultKeys(); +} +KeyManager::~KeyManager() { + SaveToFile(); +} + +std::shared_ptr KeyManager::GetInstance() { + std::lock_guard lock(s_mutex); + if (!s_instance) + s_instance = std::make_shared(); + return s_instance; +} + +void KeyManager::SetInstance(std::shared_ptr instance) { + std::lock_guard lock(s_mutex); + s_instance = instance; +} + +// ------------------- Load / Save ------------------- +bool KeyManager::LoadFromFile() { + try { + const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + const auto keysPath = userDir / "keys.json"; + + if (!std::filesystem::exists(keysPath)) { + SetDefaultKeys(); + SaveToFile(); + LOG_DEBUG(KeyManager, "Created default key file: {}", keysPath.string()); + return true; + } + + std::ifstream file(keysPath); + if (!file.is_open()) { + LOG_ERROR(KeyManager, "Could not open key file: {}", keysPath.string()); + return false; + } + + json j; + file >> j; + + SetDefaultKeys(); // start from defaults + + if (j.contains("TrophyKeySet")) + j.at("TrophyKeySet").get_to(m_keys.TrophyKeySet); + + LOG_DEBUG(KeyManager, "Successfully loaded keys from: {}", keysPath.string()); + return true; + + } catch (const std::exception& e) { + LOG_ERROR(KeyManager, "Error loading keys, using defaults: {}", e.what()); + SetDefaultKeys(); + return false; + } +} + +bool KeyManager::SaveToFile() { + try { + const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + const auto keysPath = userDir / "keys.json"; + + json j; + KeysToJson(j); + + std::ofstream file(keysPath); + if (!file.is_open()) { + LOG_ERROR(KeyManager, "Could not open key file for writing: {}", keysPath.string()); + return false; + } + + file << std::setw(4) << j; + file.flush(); + + if (file.fail()) { + LOG_ERROR(KeyManager, "Failed to write keys to: {}", keysPath.string()); + return false; + } + + LOG_DEBUG(KeyManager, "Successfully saved keys to: {}", keysPath.string()); + return true; + + } catch (const std::exception& e) { + LOG_ERROR(KeyManager, "Error saving keys: {}", e.what()); + return false; + } +} + +// ------------------- JSON conversion ------------------- +void KeyManager::KeysToJson(json& j) const { + j = m_keys; +} +void KeyManager::JsonToKeys(const json& j) { + json current = m_keys; // serialize current defaults + current.update(j); // merge only fields present in file + m_keys = current.get(); // deserialize back +} + +// ------------------- Defaults / Checks ------------------- +void KeyManager::SetDefaultKeys() { + m_keys = AllKeys{}; +} + +bool KeyManager::HasKeys() const { + return !m_keys.TrophyKeySet.ReleaseTrophyKey.empty(); +} + +// ------------------- Hex conversion ------------------- +std::vector KeyManager::HexStringToBytes(const std::string& hexStr) { + std::vector bytes; + if (hexStr.empty()) + return bytes; + + if (hexStr.size() % 2 != 0) + throw std::runtime_error("Invalid hex string length"); + + bytes.reserve(hexStr.size() / 2); + + auto hexCharToInt = [](char c) -> u8 { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + throw std::runtime_error("Invalid hex character"); + }; + + for (size_t i = 0; i < hexStr.size(); i += 2) { + u8 high = hexCharToInt(hexStr[i]); + u8 low = hexCharToInt(hexStr[i + 1]); + bytes.push_back((high << 4) | low); + } + + return bytes; +} + +std::string KeyManager::BytesToHexString(const std::vector& bytes) { + static const char hexDigits[] = "0123456789ABCDEF"; + std::string hexStr; + hexStr.reserve(bytes.size() * 2); + for (u8 b : bytes) { + hexStr.push_back(hexDigits[(b >> 4) & 0xF]); + hexStr.push_back(hexDigits[b & 0xF]); + } + return hexStr; +} \ No newline at end of file diff --git a/src/common/key_manager.h b/src/common/key_manager.h new file mode 100644 index 000000000..8925ccbd0 --- /dev/null +++ b/src/common/key_manager.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "common/types.h" +#include "nlohmann/json.hpp" + +using json = nlohmann::json; + +class KeyManager { +public: + // ------------------- Nested keysets ------------------- + struct TrophyKeySet { + std::vector ReleaseTrophyKey; + }; + + struct AllKeys { + KeyManager::TrophyKeySet TrophyKeySet; + }; + + // ------------------- Construction ------------------- + KeyManager(); + ~KeyManager(); + + // ------------------- Singleton ------------------- + static std::shared_ptr GetInstance(); + static void SetInstance(std::shared_ptr instance); + + // ------------------- File operations ------------------- + bool LoadFromFile(); + bool SaveToFile(); + + // ------------------- Key operations ------------------- + void SetDefaultKeys(); + bool HasKeys() const; + + // ------------------- Getters / Setters ------------------- + const AllKeys& GetAllKeys() const { + return m_keys; + } + void SetAllKeys(const AllKeys& keys) { + m_keys = keys; + } + + static std::vector HexStringToBytes(const std::string& hexStr); + static std::string BytesToHexString(const std::vector& bytes); + +private: + void KeysToJson(json& j) const; + void JsonToKeys(const json& j); + + AllKeys m_keys{}; + + static std::shared_ptr s_instance; + static std::mutex s_mutex; +}; + +// ------------------- NLOHMANN macros ------------------- +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::TrophyKeySet, ReleaseTrophyKey) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::AllKeys, TrophyKeySet) + +namespace nlohmann { +template <> +struct adl_serializer> { + static void to_json(json& j, const std::vector& vec) { + j = KeyManager::BytesToHexString(vec); + } + static void from_json(const json& j, std::vector& vec) { + vec = KeyManager::HexStringToBytes(j.get()); + } +}; +} // namespace nlohmann diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index d954f8601..9c8b80255 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -159,6 +160,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(ImGui) \ CLS(Input) \ CLS(Tty) \ + CLS(KeyManager) \ CLS(Loader) // GetClassName is a macro defined by Windows.h, grrr... diff --git a/src/common/logging/types.h b/src/common/logging/types.h index ee18cd161..dc7c561a4 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2023 Citra Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -127,6 +128,7 @@ enum class Class : u8 { Loader, ///< ROM loader Input, ///< Input emulation Tty, ///< Debug output from emu + KeyManager, ///< Key management system Count ///< Total number of logging classes }; diff --git a/src/core/file_format/npbind.cpp b/src/core/file_format/npbind.cpp new file mode 100644 index 000000000..b2900efa0 --- /dev/null +++ b/src/core/file_format/npbind.cpp @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include "npbind.h" + +bool NPBindFile::Load(const std::string& path) { + Clear(); // Clear any existing data + + std::ifstream f(path, std::ios::binary | std::ios::ate); + if (!f) + return false; + + std::streamsize sz = f.tellg(); + if (sz <= 0) + return false; + + f.seekg(0, std::ios::beg); + std::vector buf(static_cast(sz)); + if (!f.read(reinterpret_cast(buf.data()), sz)) + return false; + + const u64 size = buf.size(); + if (size < sizeof(NpBindHeader)) + return false; + + // Read header + memcpy(&m_header, buf.data(), sizeof(NpBindHeader)); + if (m_header.magic != NPBIND_MAGIC) + return false; + + // offset start of bodies + size_t offset = sizeof(NpBindHeader); + + m_bodies.reserve(static_cast(m_header.num_entries)); + + // For each body: read 4 TLV entries then skip padding (0x98 = 152 bytes) + const u64 body_padding = 0x98; // 152 + + for (u64 bi = 0; bi < m_header.num_entries; ++bi) { + // Ensure we have room for 4 entries' headers at least + if (offset + 4 * 4 > size) + return false; // 4 entries x (type+size) + + NPBindBody body; + + // helper lambda to read one entry + auto read_entry = [&](NPBindEntryRaw& e) -> bool { + if (offset + 4 > size) + return false; + + memcpy(&e.type, &buf[offset], 2); + memcpy(&e.size, &buf[offset + 2], 2); + offset += 4; + + if (offset + e.size > size) + return false; + + e.data.assign(buf.begin() + offset, buf.begin() + offset + e.size); + offset += e.size; + return true; + }; + + // read 4 entries in order + if (!read_entry(body.npcommid)) + return false; + if (!read_entry(body.trophy)) + return false; + if (!read_entry(body.unk1)) + return false; + if (!read_entry(body.unk2)) + return false; + + // skip fixed padding after body if present (but don't overrun) + if (offset + body_padding <= size) { + offset += body_padding; + } else { + // If padding not fully present, allow file to end (some variants may omit) + offset = size; + } + + m_bodies.push_back(std::move(body)); + } + + // Read digest if available + if (size >= 20) { + // Digest is typically the last 20 bytes, independent of offset + memcpy(m_digest, &buf[size - 20], 20); + } else { + memset(m_digest, 0, 20); + } + + return true; +} + +std::vector NPBindFile::GetNpCommIds() const { + std::vector npcommids; + npcommids.reserve(m_bodies.size()); + + for (const auto& body : m_bodies) { + // Convert binary data to string directly + if (!body.npcommid.data.empty()) { + std::string raw_string(reinterpret_cast(body.npcommid.data.data()), + body.npcommid.data.size()); + npcommids.push_back(raw_string); + } + } + + return npcommids; +} \ No newline at end of file diff --git a/src/core/file_format/npbind.h b/src/core/file_format/npbind.h new file mode 100644 index 000000000..44d6528bd --- /dev/null +++ b/src/core/file_format/npbind.h @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#include +#include "common/endian.h" +#include "common/types.h" + +#define NPBIND_MAGIC 0xD294A018u + +#pragma pack(push, 1) +struct NpBindHeader { + u32_be magic; + u32_be version; + u64_be file_size; + u64_be entry_size; + u64_be num_entries; + char padding[0x60]; // 96 bytes +}; +#pragma pack(pop) + +struct NPBindEntryRaw { + u16_be type; + u16_be size; // includes internal padding + std::vector data; +}; + +struct NPBindBody { + NPBindEntryRaw npcommid; // expected type 0x0010, size 12 + NPBindEntryRaw trophy; // expected type 0x0011, size 12 + NPBindEntryRaw unk1; // expected type 0x0012, size 176 + NPBindEntryRaw unk2; // expected type 0x0013, size 16 + // The 0x98 padding after these entries is skipped while parsing +}; + +class NPBindFile { +private: + NpBindHeader m_header; + std::vector m_bodies; + u8 m_digest[20]; // zeroed if absent + +public: + NPBindFile() { + memset(m_digest, 0, sizeof(m_digest)); + } + + // Load from file + bool Load(const std::string& path); + + // Accessors + const NpBindHeader& Header() const { + return m_header; + } + const std::vector& Bodies() const { + return m_bodies; + } + const u8* Digest() const { + return m_digest; + } + + // Get npcommid data + std::vector GetNpCommIds() const; + + // Get specific body + const NPBindBody& GetBody(size_t index) const { + return m_bodies.at(index); + } + + // Get number of bodies + u64 BodyCount() const { + return m_bodies.size(); + } + + // Check if file was loaded successfully + bool IsValid() const { + return m_header.magic == NPBIND_MAGIC; + } + + // Clear all data + void Clear() { + m_header = NpBindHeader{}; + m_bodies.clear(); + memset(m_digest, 0, sizeof(m_digest)); + } +}; \ No newline at end of file diff --git a/src/core/file_format/trp.cpp b/src/core/file_format/trp.cpp index ab873d451..f0a258c12 100644 --- a/src/core/file_format/trp.cpp +++ b/src/core/file_format/trp.cpp @@ -1,44 +1,31 @@ -// 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/aes.h" -#include "common/config.h" +#include "common/key_manager.h" #include "common/logging/log.h" #include "common/path_util.h" +#include "core/file_format/npbind.h" #include "core/file_format/trp.h" -static void DecryptEFSM(std::span trophyKey, std::span NPcommID, - std::span efsmIv, std::span ciphertext, +static void DecryptEFSM(std::span trophyKey, std::span NPcommID, + std::span efsmIv, std::span ciphertext, std::span decrypted) { // Step 1: Encrypt NPcommID std::array trophyIv{}; std::array trpKey; + // Convert spans to pointers for the aes functions aes::encrypt_cbc(NPcommID.data(), NPcommID.size(), trophyKey.data(), trophyKey.size(), trophyIv.data(), trpKey.data(), trpKey.size(), false); // Step 2: Decrypt EFSM aes::decrypt_cbc(ciphertext.data(), ciphertext.size(), trpKey.data(), trpKey.size(), - efsmIv.data(), decrypted.data(), decrypted.size(), nullptr); + const_cast(efsmIv.data()), decrypted.data(), decrypted.size(), nullptr); } TRP::TRP() = default; TRP::~TRP() = default; -void TRP::GetNPcommID(const std::filesystem::path& trophyPath, int index) { - std::filesystem::path trpPath = trophyPath / "sce_sys/npbind.dat"; - Common::FS::IOFile npbindFile(trpPath, Common::FS::FileAccessMode::Read); - if (!npbindFile.IsOpen()) { - LOG_CRITICAL(Common_Filesystem, "Failed to open npbind.dat file"); - return; - } - if (!npbindFile.Seek(0x84 + (index * 0x180))) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to NPbind offset"); - return; - } - npbindFile.ReadRaw(np_comm_id.data(), 12); - std::fill(np_comm_id.begin() + 12, np_comm_id.end(), 0); // fill with 0, we need 16 bytes. -} - static void removePadding(std::vector& vec) { for (auto it = vec.rbegin(); it != vec.rend(); ++it) { if (*it == '>') { @@ -63,91 +50,232 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit return false; } - const auto user_key_str = Config::getTrophyKey(); - if (user_key_str.size() != 32) { + const auto& user_key_vec = + KeyManager::GetInstance()->GetAllKeys().TrophyKeySet.ReleaseTrophyKey; + + if (user_key_vec.size() != 16) { LOG_INFO(Common_Filesystem, "Trophy decryption key is not specified"); return false; } std::array user_key{}; - hexToBytes(user_key_str.c_str(), user_key.data()); + std::copy(user_key_vec.begin(), user_key_vec.end(), user_key.begin()); - for (int index = 0; const auto& it : std::filesystem::directory_iterator(gameSysDir)) { - if (it.is_regular_file()) { - GetNPcommID(trophyPath, index); + // Load npbind.dat using the new class + std::filesystem::path npbindPath = trophyPath / "sce_sys/npbind.dat"; + NPBindFile npbind; + if (!npbind.Load(npbindPath.string())) { + LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file"); + } + + auto npCommIds = npbind.GetNpCommIds(); + if (npCommIds.empty()) { + LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat"); + } + + bool success = true; + int trpFileIndex = 0; + + try { + // Process each TRP file in the trophy directory + for (const auto& it : std::filesystem::directory_iterator(gameSysDir)) { + if (!it.is_regular_file() || it.path().extension() != ".trp") { + continue; // Skip non-TRP files + } + + // Get NPCommID for this TRP file (if available) + std::string npCommId; + if (trpFileIndex < static_cast(npCommIds.size())) { + npCommId = npCommIds[trpFileIndex]; + LOG_DEBUG(Common_Filesystem, "Using NPCommID: {} for {}", npCommId, + it.path().filename().string()); + } else { + LOG_WARNING(Common_Filesystem, "No NPCommID found for TRP file index {}", + trpFileIndex); + } Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read); if (!file.IsOpen()) { - LOG_CRITICAL(Common_Filesystem, "Unable to open trophy file for read"); - return false; + LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.path().string()); + success = false; + continue; } TrpHeader header; - file.Read(header); - if (header.magic != 0xDCA24D00) { - LOG_CRITICAL(Common_Filesystem, "Wrong trophy magic number"); - return false; + if (!file.Read(header)) { + LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}", + it.path().string()); + success = false; + continue; + } + + if (header.magic != TRP_MAGIC) { + LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.path().string()); + success = false; + continue; } s64 seekPos = sizeof(TrpHeader); std::filesystem::path trpFilesPath( Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId / "TrophyFiles" / it.path().stem()); - std::filesystem::create_directories(trpFilesPath / "Icons"); - std::filesystem::create_directory(trpFilesPath / "Xml"); + // Create output directories + if (!std::filesystem::create_directories(trpFilesPath / "Icons") || + !std::filesystem::create_directories(trpFilesPath / "Xml")) { + LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", titleId); + success = false; + continue; + } + + // Process each entry in the TRP file for (int i = 0; i < header.entry_num; i++) { if (!file.Seek(seekPos)) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); - return false; + LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset"); + success = false; + break; } - seekPos += (s64)header.entry_size; + seekPos += static_cast(header.entry_size); + TrpEntry entry; - file.Read(entry); - std::string_view name(entry.entry_name); - if (entry.flag == 0) { // PNG - if (!file.Seek(entry.entry_pos)) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); - return false; - } - std::vector icon(entry.entry_len); - file.Read(icon); - Common::FS::IOFile::WriteBytes(trpFilesPath / "Icons" / name, icon); + if (!file.Read(entry)) { + LOG_ERROR(Common_Filesystem, "Failed to read TRP entry"); + success = false; + break; } - if (entry.flag == 3 && np_comm_id[0] == 'N' && - np_comm_id[1] == 'P') { // ESFM, encrypted. - if (!file.Seek(entry.entry_pos)) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset"); - return false; + + std::string_view name(entry.entry_name); + + if (entry.flag == ENTRY_FLAG_PNG) { + if (!ProcessPngEntry(file, entry, trpFilesPath, name)) { + success = false; + // Continue with next entry } - file.Read(esfmIv); // get iv key. - // Skip the first 16 bytes which are the iv key on every entry as we want a - // clean xml file. - std::vector ESFM(entry.entry_len - iv_len); - std::vector XML(entry.entry_len - iv_len); - if (!file.Seek(entry.entry_pos + iv_len)) { - LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry + iv offset"); - return false; - } - file.Read(ESFM); - DecryptEFSM(user_key, np_comm_id, esfmIv, ESFM, XML); // decrypt - removePadding(XML); - std::string xml_name = entry.entry_name; - size_t pos = xml_name.find("ESFM"); - if (pos != std::string::npos) - xml_name.replace(pos, xml_name.length(), "XML"); - std::filesystem::path path = trpFilesPath / "Xml" / xml_name; - size_t written = Common::FS::IOFile::WriteBytes(path, XML); - if (written != XML.size()) { - LOG_CRITICAL( - Common_Filesystem, - "Trophy XML {} write failed, wanted to write {} bytes, wrote {}", - fmt::UTF(path.u8string()), XML.size(), written); + } else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) { + // Check if we have a valid NPCommID for decryption + if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') { + if (!ProcessEncryptedXmlEntry(file, entry, trpFilesPath, name, user_key, + npCommId)) { + success = false; + // Continue with next entry + } + } else { + LOG_WARNING(Common_Filesystem, + "Skipping encrypted XML entry - invalid NPCommID"); + // Skip this entry but continue } + } else { + LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}", + static_cast(entry.flag), name); } } + + trpFileIndex++; } - index++; + } catch (const std::filesystem::filesystem_error& e) { + LOG_CRITICAL(Common_Filesystem, "Filesystem error during trophy extraction: {}", e.what()); + return false; + } catch (const std::exception& e) { + LOG_CRITICAL(Common_Filesystem, "Error during trophy extraction: {}", e.what()); + return false; } + + if (success) { + LOG_INFO(Common_Filesystem, "Successfully extracted {} trophy files for {}", trpFileIndex, + titleId); + } + + return success; +} + +bool TRP::ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry, + const std::filesystem::path& outputPath, std::string_view name) { + if (!file.Seek(entry.entry_pos)) { + LOG_ERROR(Common_Filesystem, "Failed to seek to PNG entry offset"); + return false; + } + + std::vector icon(entry.entry_len); + if (!file.Read(icon)) { + LOG_ERROR(Common_Filesystem, "Failed to read PNG data"); + return false; + } + + auto outputFile = outputPath / "Icons" / name; + size_t written = Common::FS::IOFile::WriteBytes(outputFile, icon); + if (written != icon.size()) { + LOG_ERROR(Common_Filesystem, "PNG write failed: wanted {} bytes, wrote {}", icon.size(), + written); + return false; + } + return true; } + +bool TRP::ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry, + const std::filesystem::path& outputPath, std::string_view name, + const std::array& user_key, + const std::string& npCommId) { + constexpr size_t IV_LEN = 16; + + if (!file.Seek(entry.entry_pos)) { + LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted XML entry offset"); + return false; + } + + std::array esfmIv; + if (!file.Read(esfmIv)) { + LOG_ERROR(Common_Filesystem, "Failed to read IV for encrypted XML"); + return false; + } + + if (entry.entry_len <= IV_LEN) { + LOG_ERROR(Common_Filesystem, "Encrypted XML entry too small"); + return false; + } + + // Skip to the encrypted data (after IV) + if (!file.Seek(entry.entry_pos + IV_LEN)) { + LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted data"); + return false; + } + + std::vector ESFM(entry.entry_len - IV_LEN); + std::vector XML(entry.entry_len - IV_LEN); + + if (!file.Read(ESFM)) { + LOG_ERROR(Common_Filesystem, "Failed to read encrypted XML data"); + return false; + } + + // Decrypt the data - FIX: Don't check return value since DecryptEFSM returns void + std::span key_span(user_key); + + // Convert npCommId string to span (pad or truncate to 16 bytes) + std::array npcommid_array{}; + size_t copy_len = std::min(npCommId.size(), npcommid_array.size()); + std::memcpy(npcommid_array.data(), npCommId.data(), copy_len); + std::span npcommid_span(npcommid_array); + + DecryptEFSM(key_span, npcommid_span, esfmIv, ESFM, XML); + + // Remove padding + removePadding(XML); + + // Create output filename (replace ESFM with XML) + std::string xml_name(entry.entry_name); + size_t pos = xml_name.find("ESFM"); + if (pos != std::string::npos) { + xml_name.replace(pos, 4, "XML"); + } + + auto outputFile = outputPath / "Xml" / xml_name; + size_t written = Common::FS::IOFile::WriteBytes(outputFile, XML); + if (written != XML.size()) { + LOG_ERROR(Common_Filesystem, "XML write failed: wanted {} bytes, wrote {}", XML.size(), + written); + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/core/file_format/trp.h b/src/core/file_format/trp.h index 01207475b..2b52a4d57 100644 --- a/src/core/file_format/trp.h +++ b/src/core/file_format/trp.h @@ -8,6 +8,10 @@ #include "common/io_file.h" #include "common/types.h" +static constexpr u32 TRP_MAGIC = 0xDCA24D00; +static constexpr u8 ENTRY_FLAG_PNG = 0; +static constexpr u8 ENTRY_FLAG_ENCRYPTED_XML = 3; + struct TrpHeader { u32_be magic; // (0xDCA24D00) u32_be version; @@ -33,9 +37,14 @@ public: TRP(); ~TRP(); bool Extract(const std::filesystem::path& trophyPath, const std::string titleId); - void GetNPcommID(const std::filesystem::path& trophyPath, int index); private: + bool ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry, + const std::filesystem::path& outputPath, std::string_view name); + bool ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry, + const std::filesystem::path& outputPath, std::string_view name, + const std::array& user_key, const std::string& npCommId); + std::vector NPcommID = std::vector(12); std::array np_comm_id{}; std::array esfmIv{}; diff --git a/src/main.cpp b/src/main.cpp index aa3f4de45..b09ea7f4d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,6 +22,7 @@ #ifdef _WIN32 #include #endif +#include int main(int argc, char* argv[]) { #ifdef _WIN32 @@ -34,7 +35,17 @@ int main(int argc, char* argv[]) { // Load configurations const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); Config::load(user_dir / "config.toml"); + // temp copy the trophy key from old config to key manager if exists + auto key_manager = KeyManager::GetInstance(); + if (key_manager->GetAllKeys().TrophyKeySet.ReleaseTrophyKey.empty()) { + if (!Config::getTrophyKey().empty()) { + key_manager->SetAllKeys( + {.TrophyKeySet = {.ReleaseTrophyKey = + KeyManager::HexStringToBytes(Config::getTrophyKey())}}); + key_manager->SaveToFile(); + } + } bool has_game_argument = false; std::string game_path; std::vector game_args{}; From 3e1f5a0bfb402a5f175d84ac62ff5ff7b4687457 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:36:09 -0600 Subject: [PATCH 44/83] Kernel.Vmm: Handle sparse physical memory usage + other fixes (#3932) * Initial work * Bug fixing deadlocks and broken unmaps * Fix more bugs broken memory pools * More bug fixing Still plenty more to fix though * Even more bug fixing Finally got Final Fantasy XV back to running, haven't found anymore bugs yet. * More bugfixing * Update memory.cpp * Rewrite start * Fix for oversized unmaps * Oops * Update address_space.cpp * Clang * Mac fix? * Track VMA physical areas based on start in VMA Allows me to simplify some logic, and should (finally) allow merging VMAs in memory code. * Merge VMAs, fix some bugs Finally possible thanks to address space + phys tracking changes * Clang * Oops * Oops2 * Oops3 * Bugfixing * SDK check for coalescing Just to rule out any issues from games that wouldn't see coalescing in the first place. * More ReleaseDirectMemory fixes I really suck at logic some days * Merge physical areas within VMAs In games that perform a lot of similar mappings, you can wind up with 1000+ phys areas in one vma. This should reduce some of the overhead that might cause. * Hopefully fix Mac compile Why must their uint64_t be different? * Mac pt.2 Oops --- src/core/address_space.cpp | 311 ++++++---- src/core/address_space.h | 22 +- src/core/devtools/widget/memory_map.cpp | 7 +- src/core/devtools/widget/memory_map.h | 4 +- src/core/libraries/kernel/memory.cpp | 1 + src/core/memory.cpp | 743 ++++++++++++++---------- src/core/memory.h | 149 ++--- 7 files changed, 703 insertions(+), 534 deletions(-) diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index 3f063ea76..422c67e17 100644 --- a/src/core/address_space.cpp +++ b/src/core/address_space.cpp @@ -93,7 +93,10 @@ static u64 BackingSize = ORBIS_KERNEL_TOTAL_MEM_DEV_PRO; struct MemoryRegion { VAddr base; - size_t size; + PAddr phys_base; + u64 size; + u32 prot; + s32 fd; bool is_mapped; }; @@ -159,7 +162,8 @@ struct AddressSpace::Impl { // Restrict region size to avoid overly fragmenting the virtual memory space. if (info.State == MEM_FREE && info.RegionSize > 0x1000000) { VAddr addr = Common::AlignUp(reinterpret_cast(info.BaseAddress), alignment); - regions.emplace(addr, MemoryRegion{addr, size, false}); + regions.emplace(addr, + MemoryRegion{addr, PAddr(-1), size, PAGE_NOACCESS, -1, false}); } } @@ -207,29 +211,32 @@ struct AddressSpace::Impl { ~Impl() { if (virtual_base) { if (!VirtualFree(virtual_base, 0, MEM_RELEASE)) { - LOG_CRITICAL(Render, "Failed to free virtual memory"); + LOG_CRITICAL(Core, "Failed to free virtual memory"); } } if (backing_base) { if (!UnmapViewOfFile2(process, backing_base, MEM_PRESERVE_PLACEHOLDER)) { - LOG_CRITICAL(Render, "Failed to unmap backing memory placeholder"); + LOG_CRITICAL(Core, "Failed to unmap backing memory placeholder"); } if (!VirtualFreeEx(process, backing_base, 0, MEM_RELEASE)) { - LOG_CRITICAL(Render, "Failed to free backing memory"); + LOG_CRITICAL(Core, "Failed to free backing memory"); } } if (!CloseHandle(backing_handle)) { - LOG_CRITICAL(Render, "Failed to free backing memory file handle"); + LOG_CRITICAL(Core, "Failed to free backing memory file handle"); } } - void* Map(VAddr virtual_addr, PAddr phys_addr, size_t size, ULONG prot, uintptr_t fd = 0) { - // Before mapping we must carve a placeholder with the exact properties of our mapping. - auto* region = EnsureSplitRegionForMapping(virtual_addr, size); - region->is_mapped = true; + void* MapRegion(MemoryRegion* region) { + VAddr virtual_addr = region->base; + PAddr phys_addr = region->phys_base; + u64 size = region->size; + ULONG prot = region->prot; + s32 fd = region->fd; + void* ptr = nullptr; if (phys_addr != -1) { - HANDLE backing = fd ? reinterpret_cast(fd) : backing_handle; + HANDLE backing = fd != -1 ? reinterpret_cast(fd) : backing_handle; if (fd && prot == PAGE_READONLY) { DWORD resultvar; ptr = VirtualAlloc2(process, reinterpret_cast(virtual_addr), size, @@ -257,110 +264,136 @@ struct AddressSpace::Impl { return ptr; } - void Unmap(VAddr virtual_addr, size_t size, bool has_backing) { - bool ret; - if (has_backing) { + void UnmapRegion(MemoryRegion* region) { + VAddr virtual_addr = region->base; + PAddr phys_base = region->phys_base; + u64 size = region->size; + + bool ret = false; + if (phys_base != -1) { ret = UnmapViewOfFile2(process, reinterpret_cast(virtual_addr), MEM_PRESERVE_PLACEHOLDER); } else { ret = VirtualFreeEx(process, reinterpret_cast(virtual_addr), size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER); } - ASSERT_MSG(ret, "Unmap operation on virtual_addr={:#X} failed: {}", virtual_addr, + ASSERT_MSG(ret, "Unmap on virtual_addr {:#x}, size {:#x} failed: {}", virtual_addr, size, Common::GetLastErrorMsg()); - - // The unmap call will create a new placeholder region. We need to see if we can coalesce it - // with neighbors. - JoinRegionsAfterUnmap(virtual_addr, size); } - // The following code is inspired from Dolphin's MemArena - // https://github.com/dolphin-emu/dolphin/blob/deee3ee4/Source/Core/Common/MemArenaWin.cpp#L212 - MemoryRegion* EnsureSplitRegionForMapping(VAddr address, size_t size) { - // Find closest region that is <= the given address by using upper bound and decrementing - auto it = regions.upper_bound(address); - ASSERT_MSG(it != regions.begin(), "Invalid address {:#x}", address); - --it; - ASSERT_MSG(!it->second.is_mapped, - "Attempt to map {:#x} with size {:#x} which overlaps with {:#x} mapping", - address, size, it->second.base); - auto& [base, region] = *it; + void SplitRegion(VAddr virtual_addr, u64 size) { + // First, get the region this range covers + auto it = std::prev(regions.upper_bound(virtual_addr)); - const VAddr mapping_address = region.base; - const size_t region_size = region.size; - if (mapping_address == address) { - // If this region is already split up correctly we don't have to do anything - if (region_size == size) { - return ®ion; + // All unmapped areas will coalesce, so there should be a region + // containing the full requested range. If not, then something is mapped here. + ASSERT_MSG(it->second.base + it->second.size >= virtual_addr + size, + "Cannot fit region into one placeholder"); + + // If the region is mapped, we need to unmap first before we can modify the placeholders. + if (it->second.is_mapped) { + ASSERT_MSG(it->second.phys_base != -1 || !it->second.is_mapped, + "Cannot split unbacked mapping"); + UnmapRegion(&it->second); + } + + // We need to split this region to create a matching placeholder. + if (it->second.base != virtual_addr) { + // Requested address is not the start of the containing region, + // create a new region to represent the memory before the requested range. + auto& region = it->second; + u64 base_offset = virtual_addr - region.base; + u64 next_region_size = region.size - base_offset; + PAddr next_region_phys_base = -1; + if (region.is_mapped) { + next_region_phys_base = region.phys_base + base_offset; } + region.size = base_offset; - ASSERT_MSG(region_size >= size, - "Region with address {:#x} and size {:#x} can't fit {:#x}", mapping_address, - region_size, size); - - // Split the placeholder. - if (!VirtualFreeEx(process, LPVOID(address), size, + // Use VirtualFreeEx to create the split. + if (!VirtualFreeEx(process, LPVOID(region.base), region.size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) { UNREACHABLE_MSG("Region splitting failed: {}", Common::GetLastErrorMsg()); - return nullptr; } - // Update tracked mappings and return the first of the two + // If the mapping was mapped, remap the region. + if (region.is_mapped) { + MapRegion(®ion); + } + + // Store a new region matching the removed area. + it = regions.emplace_hint(std::next(it), virtual_addr, + MemoryRegion(virtual_addr, next_region_phys_base, + next_region_size, region.prot, region.fd, + region.is_mapped)); + } + + // At this point, the region's base will match virtual_addr. + // Now check for a size difference. + if (it->second.size != size) { + // The requested size is smaller than the current region placeholder. + // Update region to match the requested region, + // then make a new region to represent the remaining space. + auto& region = it->second; + VAddr next_region_addr = region.base + size; + u64 next_region_size = region.size - size; + PAddr next_region_phys_base = -1; + if (region.is_mapped) { + next_region_phys_base = region.phys_base + size; + } region.size = size; - const VAddr new_mapping_start = address + size; - regions.emplace_hint(std::next(it), new_mapping_start, - MemoryRegion(new_mapping_start, region_size - size, false)); - return ®ion; + + // Store the new region matching the remaining space + regions.emplace_hint(std::next(it), next_region_addr, + MemoryRegion(next_region_addr, next_region_phys_base, + next_region_size, region.prot, region.fd, + region.is_mapped)); + + // Use VirtualFreeEx to create the split. + if (!VirtualFreeEx(process, LPVOID(region.base), region.size, + MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) { + UNREACHABLE_MSG("Region splitting failed: {}", Common::GetLastErrorMsg()); + } + + // If these regions were mapped, then map the unmapped area beyond the requested range. + if (region.is_mapped) { + MapRegion(&std::next(it)->second); + } } - ASSERT(mapping_address < address); - - // Is there enough space to map this? - const size_t offset_in_region = address - mapping_address; - const size_t minimum_size = size + offset_in_region; - ASSERT(region_size >= minimum_size); - - // Split the placeholder. - if (!VirtualFreeEx(process, LPVOID(address), size, - MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) { - UNREACHABLE_MSG("Region splitting failed: {}", Common::GetLastErrorMsg()); - return nullptr; - } - - // Do we now have two regions or three regions? - if (region_size == minimum_size) { - // Split into two; update tracked mappings and return the second one - region.size = offset_in_region; - it = regions.emplace_hint(std::next(it), address, MemoryRegion(address, size, false)); - return &it->second; - } else { - // Split into three; update tracked mappings and return the middle one - region.size = offset_in_region; - const VAddr middle_mapping_start = address; - const size_t middle_mapping_size = size; - const VAddr after_mapping_start = address + size; - const size_t after_mapping_size = region_size - minimum_size; - it = regions.emplace_hint(std::next(it), after_mapping_start, - MemoryRegion(after_mapping_start, after_mapping_size, false)); - it = regions.emplace_hint( - it, middle_mapping_start, - MemoryRegion(middle_mapping_start, middle_mapping_size, false)); - return &it->second; + // If the requested region was mapped, remap it. + if (it->second.is_mapped) { + MapRegion(&it->second); } } - void JoinRegionsAfterUnmap(VAddr address, size_t size) { - // There should be a mapping that matches the request exactly, find it - auto it = regions.find(address); - ASSERT_MSG(it != regions.end() && it->second.size == size, - "Invalid address/size given to unmap."); + void* Map(VAddr virtual_addr, PAddr phys_addr, u64 size, ULONG prot, s32 fd = -1) { + // Split surrounding regions to create a placeholder + SplitRegion(virtual_addr, size); + + // Get the region this range covers + auto it = std::prev(regions.upper_bound(virtual_addr)); auto& [base, region] = *it; - region.is_mapped = false; + + ASSERT_MSG(!region.is_mapped, "Cannot overwrite mapped region"); + + // Now we have a region matching the requested region, perform the actual mapping. + region.is_mapped = true; + region.phys_base = phys_addr; + region.prot = prot; + region.fd = fd; + return MapRegion(®ion); + } + + void CoalesceFreeRegions(VAddr virtual_addr) { + // First, get the region to update + auto it = std::prev(regions.upper_bound(virtual_addr)); + ASSERT_MSG(!it->second.is_mapped, "Cannot coalesce mapped regions"); // Check if a placeholder exists right before us. auto it_prev = it != regions.begin() ? std::prev(it) : regions.end(); if (it_prev != regions.end() && !it_prev->second.is_mapped) { - const size_t total_size = it_prev->second.size + size; + const u64 total_size = it_prev->second.size + it->second.size; if (!VirtualFreeEx(process, LPVOID(it_prev->first), total_size, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) { UNREACHABLE_MSG("Region coalescing failed: {}", Common::GetLastErrorMsg()); @@ -374,7 +407,7 @@ struct AddressSpace::Impl { // Check if a placeholder exists right after us. auto it_next = std::next(it); if (it_next != regions.end() && !it_next->second.is_mapped) { - const size_t total_size = it->second.size + it_next->second.size; + const u64 total_size = it->second.size + it_next->second.size; if (!VirtualFreeEx(process, LPVOID(it->first), total_size, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) { UNREACHABLE_MSG("Region coalescing failed: {}", Common::GetLastErrorMsg()); @@ -385,7 +418,47 @@ struct AddressSpace::Impl { } } - void Protect(VAddr virtual_addr, size_t size, bool read, bool write, bool execute) { + void Unmap(VAddr virtual_addr, u64 size) { + // Loop through all regions in the requested range + u64 remaining_size = size; + VAddr current_addr = virtual_addr; + while (remaining_size > 0) { + // Get the region containing our current address. + auto it = std::prev(regions.upper_bound(current_addr)); + + // If necessary, split regions to ensure a valid unmap. + // To prevent complication, ensure size is within the bounds of the current region. + u64 base_offset = current_addr - it->second.base; + u64 size_to_unmap = std::min(it->second.size - base_offset, remaining_size); + if (current_addr != it->second.base || size_to_unmap != it->second.size) { + SplitRegion(current_addr, size_to_unmap); + } + + // Repair the region pointer, as SplitRegion modifies the regions map. + it = std::prev(regions.upper_bound(current_addr)); + auto& [base, region] = *it; + + // Unmap the region if it was previously mapped + if (region.is_mapped) { + UnmapRegion(®ion); + } + + // Update region data + region.is_mapped = false; + region.fd = -1; + region.phys_base = -1; + region.prot = PAGE_NOACCESS; + + // Coalesce any free space + CoalesceFreeRegions(current_addr); + + // Update loop variables + remaining_size -= size_to_unmap; + current_addr += size_to_unmap; + } + } + + void Protect(VAddr virtual_addr, u64 size, bool read, bool write, bool execute) { DWORD new_flags{}; if (write && !read) { @@ -415,7 +488,7 @@ struct AddressSpace::Impl { // If no flags are assigned, then something's gone wrong. if (new_flags == 0) { - LOG_CRITICAL(Common_Memory, + LOG_CRITICAL(Core, "Unsupported protection flag combination for address {:#x}, size {}, " "read={}, write={}, execute={}", virtual_addr, size, read, write, execute); @@ -429,8 +502,8 @@ struct AddressSpace::Impl { continue; } const auto& region = it->second; - const size_t range_addr = std::max(region.base, virtual_addr); - const size_t range_size = std::min(region.base + region.size, virtual_end) - range_addr; + const u64 range_addr = std::max(region.base, virtual_addr); + const u64 range_size = std::min(region.base + region.size, virtual_end) - range_addr; DWORD old_flags{}; if (!VirtualProtectEx(process, LPVOID(range_addr), range_size, new_flags, &old_flags)) { UNREACHABLE_MSG( @@ -453,11 +526,11 @@ struct AddressSpace::Impl { u8* backing_base{}; u8* virtual_base{}; u8* system_managed_base{}; - size_t system_managed_size{}; + u64 system_managed_size{}; u8* system_reserved_base{}; - size_t system_reserved_size{}; + u64 system_reserved_size{}; u8* user_base{}; - size_t user_size{}; + u64 user_size{}; std::map regions; }; #else @@ -601,7 +674,7 @@ struct AddressSpace::Impl { } } - void* Map(VAddr virtual_addr, PAddr phys_addr, size_t size, PosixPageProtection prot, + void* Map(VAddr virtual_addr, PAddr phys_addr, u64 size, PosixPageProtection prot, int fd = -1) { m_free_regions.subtract({virtual_addr, virtual_addr + size}); const int handle = phys_addr != -1 ? (fd == -1 ? backing_fd : fd) : -1; @@ -613,10 +686,10 @@ struct AddressSpace::Impl { return ret; } - void Unmap(VAddr virtual_addr, size_t size, bool) { + void Unmap(VAddr virtual_addr, u64 size, bool) { // Check to see if we are adjacent to any regions. - auto start_address = virtual_addr; - auto end_address = start_address + size; + VAddr start_address = virtual_addr; + VAddr end_address = start_address + size; auto it = m_free_regions.find({start_address - 1, end_address + 1}); // If we are, join with them, ensuring we stay in bounds. @@ -634,7 +707,7 @@ struct AddressSpace::Impl { ASSERT_MSG(ret != MAP_FAILED, "mmap failed: {}", strerror(errno)); } - void Protect(VAddr virtual_addr, size_t size, bool read, bool write, bool execute) { + void Protect(VAddr virtual_addr, u64 size, bool read, bool write, bool execute) { int flags = PROT_NONE; if (read) { flags |= PROT_READ; @@ -654,11 +727,11 @@ struct AddressSpace::Impl { int backing_fd; u8* backing_base{}; u8* system_managed_base{}; - size_t system_managed_size{}; + u64 system_managed_size{}; u8* system_reserved_base{}; - size_t system_reserved_size{}; + u64 system_reserved_size{}; u8* user_base{}; - size_t user_size{}; + u64 user_size{}; boost::icl::interval_set m_free_regions; }; #endif @@ -675,8 +748,7 @@ AddressSpace::AddressSpace() : impl{std::make_unique()} { AddressSpace::~AddressSpace() = default; -void* AddressSpace::Map(VAddr virtual_addr, size_t size, u64 alignment, PAddr phys_addr, - bool is_exec) { +void* AddressSpace::Map(VAddr virtual_addr, u64 size, PAddr phys_addr, bool is_exec) { #if ARCH_X86_64 const auto prot = is_exec ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE; #else @@ -687,8 +759,7 @@ void* AddressSpace::Map(VAddr virtual_addr, size_t size, u64 alignment, PAddr ph return impl->Map(virtual_addr, phys_addr, size, prot); } -void* AddressSpace::MapFile(VAddr virtual_addr, size_t size, size_t offset, u32 prot, - uintptr_t fd) { +void* AddressSpace::MapFile(VAddr virtual_addr, u64 size, u64 offset, u32 prot, uintptr_t fd) { #ifdef _WIN32 return impl->Map(virtual_addr, offset, size, ToWindowsProt(std::bit_cast(prot)), fd); @@ -698,31 +769,15 @@ void* AddressSpace::MapFile(VAddr virtual_addr, size_t size, size_t offset, u32 #endif } -void AddressSpace::Unmap(VAddr virtual_addr, size_t size, VAddr start_in_vma, VAddr end_in_vma, - PAddr phys_base, bool is_exec, bool has_backing, bool readonly_file) { +void AddressSpace::Unmap(VAddr virtual_addr, u64 size, bool has_backing) { #ifdef _WIN32 - // There does not appear to be comparable support for partial unmapping on Windows. - // Unfortunately, a least one title was found to require this. The workaround is to unmap - // the entire allocation and remap the portions outside of the requested unmapping range. - impl->Unmap(virtual_addr, size, has_backing && !readonly_file); - - // TODO: Determine if any titles require partial unmapping support for un-backed allocations. - ASSERT_MSG(has_backing || (start_in_vma == 0 && end_in_vma == size), - "Partial unmapping of un-backed allocations is not supported"); - - if (start_in_vma != 0) { - Map(virtual_addr, start_in_vma, 0, phys_base, is_exec); - } - - if (end_in_vma != size) { - Map(virtual_addr + end_in_vma, size - end_in_vma, 0, phys_base + end_in_vma, is_exec); - } + impl->Unmap(virtual_addr, size); #else - impl->Unmap(virtual_addr + start_in_vma, end_in_vma - start_in_vma, has_backing); + impl->Unmap(virtual_addr, size, has_backing); #endif } -void AddressSpace::Protect(VAddr virtual_addr, size_t size, MemoryPermission perms) { +void AddressSpace::Protect(VAddr virtual_addr, u64 size, MemoryPermission perms) { const bool read = True(perms & MemoryPermission::Read); const bool write = True(perms & MemoryPermission::Write); const bool execute = True(perms & MemoryPermission::Execute); diff --git a/src/core/address_space.h b/src/core/address_space.h index 5c50039bd..fa47bb47e 100644 --- a/src/core/address_space.h +++ b/src/core/address_space.h @@ -39,7 +39,7 @@ public: [[nodiscard]] const u8* SystemManagedVirtualBase() const noexcept { return system_managed_base; } - [[nodiscard]] size_t SystemManagedVirtualSize() const noexcept { + [[nodiscard]] u64 SystemManagedVirtualSize() const noexcept { return system_managed_size; } @@ -49,7 +49,7 @@ public: [[nodiscard]] const u8* SystemReservedVirtualBase() const noexcept { return system_reserved_base; } - [[nodiscard]] size_t SystemReservedVirtualSize() const noexcept { + [[nodiscard]] u64 SystemReservedVirtualSize() const noexcept { return system_reserved_size; } @@ -59,7 +59,7 @@ public: [[nodiscard]] const u8* UserVirtualBase() const noexcept { return user_base; } - [[nodiscard]] size_t UserVirtualSize() const noexcept { + [[nodiscard]] u64 UserVirtualSize() const noexcept { return user_size; } @@ -73,17 +73,15 @@ public: * If zero is provided the mapping is considered as private. * @return A pointer to the mapped memory. */ - void* Map(VAddr virtual_addr, size_t size, u64 alignment = 0, PAddr phys_addr = -1, - bool exec = false); + void* Map(VAddr virtual_addr, u64 size, PAddr phys_addr = -1, bool exec = false); /// Memory maps a specified file descriptor. - void* MapFile(VAddr virtual_addr, size_t size, size_t offset, u32 prot, uintptr_t fd); + void* MapFile(VAddr virtual_addr, u64 size, u64 offset, u32 prot, uintptr_t fd); /// Unmaps specified virtual memory area. - void Unmap(VAddr virtual_addr, size_t size, VAddr start_in_vma, VAddr end_in_vma, - PAddr phys_base, bool is_exec, bool has_backing, bool readonly_file); + void Unmap(VAddr virtual_addr, u64 size, bool has_backing); - void Protect(VAddr virtual_addr, size_t size, MemoryPermission perms); + void Protect(VAddr virtual_addr, u64 size, MemoryPermission perms); // Returns an interval set containing all usable regions. boost::icl::interval_set GetUsableRegions(); @@ -93,11 +91,11 @@ private: std::unique_ptr impl; u8* backing_base{}; u8* system_managed_base{}; - size_t system_managed_size{}; + u64 system_managed_size{}; u8* system_reserved_base{}; - size_t system_reserved_size{}; + u64 system_reserved_size{}; u8* user_base{}; - size_t user_size{}; + u64 user_size{}; }; } // namespace Core diff --git a/src/core/devtools/widget/memory_map.cpp b/src/core/devtools/widget/memory_map.cpp index 278c6595c..d1d1eb410 100644 --- a/src/core/devtools/widget/memory_map.cpp +++ b/src/core/devtools/widget/memory_map.cpp @@ -32,7 +32,7 @@ bool MemoryMapViewer::Iterator::DrawLine() { TableNextColumn(); Text("%s", magic_enum::enum_name(m.prot).data()); TableNextColumn(); - if (m.is_exec) { + if (True(m.prot & MemoryProt::CpuExec)) { Text("X"); } TableNextColumn(); @@ -44,7 +44,7 @@ bool MemoryMapViewer::Iterator::DrawLine() { return false; } auto m = dmem.it->second; - if (m.dma_type == DMAType::Free) { + if (m.dma_type == PhysicalMemoryType::Free) { ++dmem.it; return DrawLine(); } @@ -56,7 +56,8 @@ bool MemoryMapViewer::Iterator::DrawLine() { auto type = static_cast<::Libraries::Kernel::MemoryTypes>(m.memory_type); Text("%s", magic_enum::enum_name(type).data()); TableNextColumn(); - Text("%d", m.dma_type == DMAType::Pooled || m.dma_type == DMAType::Committed); + Text("%d", + m.dma_type == PhysicalMemoryType::Pooled || m.dma_type == PhysicalMemoryType::Committed); ++dmem.it; return true; } diff --git a/src/core/devtools/widget/memory_map.h b/src/core/devtools/widget/memory_map.h index cc7697c8c..3bbec4643 100644 --- a/src/core/devtools/widget/memory_map.h +++ b/src/core/devtools/widget/memory_map.h @@ -11,8 +11,8 @@ class MemoryMapViewer { struct Iterator { bool is_vma; struct { - MemoryManager::DMemMap::iterator it; - MemoryManager::DMemMap::iterator end; + MemoryManager::PhysMap::iterator it; + MemoryManager::PhysMap::iterator end; } dmem; struct { MemoryManager::VMAMap::iterator it; diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 62903ff72..3aec8193a 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -102,6 +102,7 @@ s32 PS4_SYSV_ABI sceKernelReleaseDirectMemory(u64 start, u64 len) { if (len == 0) { return ORBIS_OK; } + LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len); auto* memory = Core::Memory::Instance(); memory->Free(start, len); return ORBIS_OK; diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 3a4a16933..4567475cd 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -5,6 +5,7 @@ #include "common/assert.h" #include "common/config.h" #include "common/debug.h" +#include "common/elf_info.h" #include "core/file_sys/fs.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/orbis_error.h" @@ -25,6 +26,9 @@ MemoryManager::MemoryManager() { VirtualMemoryArea{region.lower(), region.upper() - region.lower()}); LOG_INFO(Kernel_Vmm, "{:#x} - {:#x}", region.lower(), region.upper()); } + + ASSERT_MSG(Libraries::Kernel::sceKernelGetCompiledSdkVersion(&sdk_version) == 0, + "Failed to get compiled SDK version"); } MemoryManager::~MemoryManager() = default; @@ -55,14 +59,14 @@ void MemoryManager::SetupMemoryRegions(u64 flexible_size, bool use_extended_mem1 // Insert an area that covers the direct memory physical address block. // Note that this should never be called after direct memory allocations have been made. dmem_map.clear(); - dmem_map.emplace(0, DirectMemoryArea{0, total_direct_size}); + dmem_map.emplace(0, PhysicalMemoryArea{0, total_direct_size}); // Insert an area that covers the flexible memory physical address block. // Note that this should never be called after flexible memory allocations have been made. const auto remaining_physical_space = total_size - total_direct_size; fmem_map.clear(); fmem_map.emplace(total_direct_size, - FlexibleMemoryArea{total_direct_size, remaining_physical_space}); + PhysicalMemoryArea{total_direct_size, remaining_physical_space}); LOG_INFO(Kernel_Vmm, "Configured memory regions: flexible size = {:#x}, direct size = {:#x}", total_flexible_size, total_direct_size); @@ -115,6 +119,7 @@ void MemoryManager::SetPrtArea(u32 id, VAddr address, u64 size) { void MemoryManager::CopySparseMemory(VAddr virtual_addr, u8* dest, u64 size) { ASSERT_MSG(IsValidMapping(virtual_addr), "Attempted to access invalid address {:#x}", virtual_addr); + mutex.lock_shared(); auto vma = FindVMA(virtual_addr); while (size) { @@ -129,23 +134,46 @@ void MemoryManager::CopySparseMemory(VAddr virtual_addr, u8* dest, u64 size) { dest += copy_size; ++vma; } + + mutex.unlock_shared(); } -bool MemoryManager::TryWriteBacking(void* address, const void* data, u32 num_bytes) { +bool MemoryManager::TryWriteBacking(void* address, const void* data, u64 size) { const VAddr virtual_addr = std::bit_cast(address); - ASSERT_MSG(IsValidMapping(virtual_addr, num_bytes), "Attempted to access invalid address {:#x}", + ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); - const auto& vma = FindVMA(virtual_addr)->second; - if (!HasPhysicalBacking(vma)) { - return false; + mutex.lock_shared(); + + std::vector vmas_to_write; + auto current_vma = FindVMA(virtual_addr); + while (virtual_addr + size < current_vma->second.base + current_vma->second.size) { + if (!HasPhysicalBacking(current_vma->second)) { + mutex.unlock_shared(); + return false; + } + vmas_to_write.emplace_back(current_vma->second); + current_vma++; } - u8* backing = impl.BackingBase() + vma.phys_base + (virtual_addr - vma.base); - memcpy(backing, data, num_bytes); + + for (auto& vma : vmas_to_write) { + auto start_in_vma = std::max(virtual_addr, vma.base) - vma.base; + for (auto& phys_area : vma.phys_areas) { + if (!size) { + break; + } + u8* backing = impl.BackingBase() + phys_area.second.base + start_in_vma; + u64 copy_size = std::min(size, phys_area.second.size); + memcpy(backing, data, copy_size); + size -= copy_size; + } + } + + mutex.unlock_shared(); return true; } PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, u64 size, u64 alignment) { - std::scoped_lock lk{mutex}; + mutex.lock(); alignment = alignment > 0 ? alignment : 64_KB; auto dmem_area = FindDmemArea(search_start); @@ -155,7 +183,7 @@ PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, u64 size, auto mapping_end = mapping_start + size; // Find the first free, large enough dmem area in the range. - while (dmem_area->second.dma_type != DMAType::Free || + while (dmem_area->second.dma_type != PhysicalMemoryType::Free || dmem_area->second.GetEnd() < mapping_end) { // The current dmem_area isn't suitable, move to the next one. dmem_area++; @@ -171,33 +199,34 @@ PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, u64 size, if (dmem_area == dmem_map.end()) { // There are no suitable mappings in this range LOG_ERROR(Kernel_Vmm, "Unable to find free direct memory area: size = {:#x}", size); + mutex.unlock(); return -1; } // Add the allocated region to the list and commit its pages. - auto& area = CarveDmemArea(mapping_start, size)->second; - area.dma_type = DMAType::Pooled; + auto& area = CarvePhysArea(dmem_map, mapping_start, size)->second; + area.dma_type = PhysicalMemoryType::Pooled; area.memory_type = 3; // Track how much dmem was allocated for pools. pool_budget += size; + mutex.unlock(); return mapping_start; } PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, u64 size, u64 alignment, s32 memory_type) { - std::scoped_lock lk{mutex}; + mutex.lock(); alignment = alignment > 0 ? alignment : 16_KB; auto dmem_area = FindDmemArea(search_start); - auto mapping_start = search_start > dmem_area->second.base - ? Common::AlignUp(search_start, alignment) - : Common::AlignUp(dmem_area->second.base, alignment); + auto mapping_start = + Common::AlignUp(std::max(search_start, dmem_area->second.base), alignment); auto mapping_end = mapping_start + size; // Find the first free, large enough dmem area in the range. - while (dmem_area->second.dma_type != DMAType::Free || + while (dmem_area->second.dma_type != PhysicalMemoryType::Free || dmem_area->second.GetEnd() < mapping_end) { // The current dmem_area isn't suitable, move to the next one. dmem_area++; @@ -213,19 +242,22 @@ PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, u64 size, u6 if (dmem_area == dmem_map.end()) { // There are no suitable mappings in this range LOG_ERROR(Kernel_Vmm, "Unable to find free direct memory area: size = {:#x}", size); + mutex.unlock(); return -1; } // Add the allocated region to the list and commit its pages. - auto& area = CarveDmemArea(mapping_start, size)->second; + auto& area = CarvePhysArea(dmem_map, mapping_start, size)->second; area.memory_type = memory_type; - area.dma_type = DMAType::Allocated; + area.dma_type = PhysicalMemoryType::Allocated; MergeAdjacent(dmem_map, dmem_area); + + mutex.unlock(); return mapping_start; } void MemoryManager::Free(PAddr phys_addr, u64 size) { - std::scoped_lock lk{mutex}; + mutex.lock(); // Release any dmem mappings that reference this physical block. std::vector> remove_list; @@ -233,19 +265,21 @@ void MemoryManager::Free(PAddr phys_addr, u64 size) { if (mapping.type != VMAType::Direct) { continue; } - if (mapping.phys_base <= phys_addr && phys_addr < mapping.phys_base + mapping.size) { - const auto vma_start_offset = phys_addr - mapping.phys_base; - const auto addr_in_vma = mapping.base + vma_start_offset; - const auto size_in_vma = - mapping.size - vma_start_offset > size ? size : mapping.size - vma_start_offset; + for (auto& [offset_in_vma, phys_mapping] : mapping.phys_areas) { + if (phys_addr + size > phys_mapping.base && + phys_addr < phys_mapping.base + phys_mapping.size) { + const u64 phys_offset = + std::max(phys_mapping.base, phys_addr) - phys_mapping.base; + const VAddr addr_in_vma = mapping.base + offset_in_vma + phys_offset; + const u64 unmap_size = std::min(phys_mapping.size - phys_offset, size); - LOG_INFO(Kernel_Vmm, "Unmaping direct mapping {:#x} with size {:#x}", addr_in_vma, - size_in_vma); - // Unmaping might erase from vma_map. We can't do it here. - remove_list.emplace_back(addr_in_vma, size_in_vma); + // Unmapping might erase from vma_map. We can't do it here. + remove_list.emplace_back(addr_in_vma, unmap_size); + } } } for (const auto& [addr, size] : remove_list) { + LOG_INFO(Kernel_Vmm, "Unmapping direct mapping {:#x} with size {:#x}", addr, size); UnmapMemoryImpl(addr, size); } @@ -255,15 +289,13 @@ void MemoryManager::Free(PAddr phys_addr, u64 size) { auto dmem_area = FindDmemArea(phys_addr); while (dmem_area != dmem_map.end() && remaining_size > 0) { // Carve a free dmem area in place of this one. - const auto start_phys_addr = - phys_addr > dmem_area->second.base ? phys_addr : dmem_area->second.base; + const auto start_phys_addr = std::max(phys_addr, dmem_area->second.base); const auto offset_in_dma = start_phys_addr - dmem_area->second.base; - const auto size_in_dma = dmem_area->second.size - offset_in_dma > remaining_size - ? remaining_size - : dmem_area->second.size - offset_in_dma; - const auto dmem_handle = CarveDmemArea(start_phys_addr, size_in_dma); + const auto size_in_dma = + std::min(dmem_area->second.size - offset_in_dma, remaining_size); + const auto dmem_handle = CarvePhysArea(dmem_map, start_phys_addr, size_in_dma); auto& new_dmem_area = dmem_handle->second; - new_dmem_area.dma_type = DMAType::Free; + new_dmem_area.dma_type = PhysicalMemoryType::Free; new_dmem_area.memory_type = 0; // Merge the new dmem_area with dmem_map @@ -274,12 +306,14 @@ void MemoryManager::Free(PAddr phys_addr, u64 size) { remaining_size -= size_in_dma; dmem_area = FindDmemArea(phys_addr_to_search); } + + mutex.unlock(); } s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 mtype) { ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); - std::scoped_lock lk{mutex}; + mutex.lock(); // Input addresses to PoolCommit are treated as fixed, and have a constant alignment. const u64 alignment = 64_KB; @@ -289,6 +323,7 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 if (vma.type != VMAType::PoolReserved) { // If we're attempting to commit non-pooled memory, return EINVAL LOG_ERROR(Kernel_Vmm, "Attempting to commit non-pooled memory at {:#x}", mapped_addr); + mutex.unlock(); return ORBIS_KERNEL_ERROR_EINVAL; } @@ -297,12 +332,14 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 LOG_ERROR(Kernel_Vmm, "Pooled region {:#x} to {:#x} is not large enough to commit from {:#x} to {:#x}", vma.base, vma.base + vma.size, mapped_addr, mapped_addr + size); + mutex.unlock(); return ORBIS_KERNEL_ERROR_EINVAL; } if (pool_budget <= size) { // If there isn't enough pooled memory to perform the mapping, return ENOMEM LOG_ERROR(Kernel_Vmm, "Not enough pooled memory to perform mapping"); + mutex.unlock(); return ORBIS_KERNEL_ERROR_ENOMEM; } else { // Track how much pooled memory this commit will take @@ -314,48 +351,54 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 prot |= MemoryProt::CpuRead; } + // Create the virtual mapping for the commit + const auto new_vma_handle = CarveVMA(virtual_addr, size); + auto& new_vma = new_vma_handle->second; + new_vma.disallow_merge = false; + new_vma.prot = prot; + new_vma.name = "anon"; + new_vma.type = Core::VMAType::Pooled; + new_vma.phys_areas.clear(); + // Find suitable physical addresses auto handle = dmem_map.begin(); u64 remaining_size = size; VAddr current_addr = mapped_addr; - while (handle != dmem_map.end() && remaining_size != 0) { - if (handle->second.dma_type != DMAType::Pooled) { + while (handle != dmem_map.end() && remaining_size > 0) { + if (handle->second.dma_type != PhysicalMemoryType::Pooled) { // Non-pooled means it's either not for pool use, or already committed. handle++; continue; } // On PS4, commits can make sparse physical mappings. - // For now, it's easier to create separate memory mappings for each physical mapping. u64 size_to_map = std::min(remaining_size, handle->second.size); - // Carve out the new VMA representing this mapping - const auto new_vma_handle = CarveVMA(current_addr, size_to_map); - auto& new_vma = new_vma_handle->second; - new_vma.disallow_merge = false; - new_vma.prot = prot; - new_vma.name = "anon"; - new_vma.type = Core::VMAType::Pooled; - new_vma.is_exec = false; - // Use the start of this area as the physical backing for this mapping. - const auto new_dmem_handle = CarveDmemArea(handle->second.base, size_to_map); + const auto new_dmem_handle = CarvePhysArea(dmem_map, handle->second.base, size_to_map); auto& new_dmem_area = new_dmem_handle->second; - new_dmem_area.dma_type = DMAType::Committed; + new_dmem_area.dma_type = PhysicalMemoryType::Committed; new_dmem_area.memory_type = mtype; - new_vma.phys_base = new_dmem_area.base; - handle = MergeAdjacent(dmem_map, new_dmem_handle); - // Perform the mapping - void* out_addr = impl.Map(current_addr, size_to_map, alignment, new_vma.phys_base, false); + // Add the dmem area to this vma, merge it with any similar tracked areas. + new_vma.phys_areas[current_addr - mapped_addr] = new_dmem_handle->second; + MergeAdjacent(new_vma.phys_areas, new_vma.phys_areas.find(current_addr - mapped_addr)); + + // Perform an address space mapping for each physical area + void* out_addr = impl.Map(current_addr, size_to_map, new_dmem_area.base); TRACK_ALLOC(out_addr, size_to_map, "VMEM"); + handle = MergeAdjacent(dmem_map, new_dmem_handle); current_addr += size_to_map; remaining_size -= size_to_map; handle++; } - ASSERT_MSG(remaining_size == 0, "Unable to map physical memory"); + ASSERT_MSG(remaining_size == 0, "Failed to commit pooled memory"); + // Merge this VMA with similar nearby areas + MergeAdjacent(vma_map, new_vma_handle); + + mutex.unlock(); if (IsValidGpuMapping(mapped_addr, size)) { rasterizer->MapMemory(mapped_addr, size); } @@ -376,13 +419,15 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo return ORBIS_KERNEL_ERROR_EINVAL; } - std::scoped_lock lk{mutex}; + mutex.lock(); + PhysHandle dmem_area; // Validate the requested physical address range if (phys_addr != -1) { if (total_direct_size < phys_addr + size) { LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at physical address {:#x}", size, phys_addr); + mutex.unlock(); return ORBIS_KERNEL_ERROR_ENOMEM; } @@ -390,49 +435,24 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo auto dmem_area = FindDmemArea(phys_addr); while (dmem_area != dmem_map.end() && dmem_area->second.base < phys_addr + size) { // If any requested dmem area is not allocated, return an error. - if (dmem_area->second.dma_type != DMAType::Allocated && - dmem_area->second.dma_type != DMAType::Mapped) { + if (dmem_area->second.dma_type != PhysicalMemoryType::Allocated && + dmem_area->second.dma_type != PhysicalMemoryType::Mapped) { LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at physical address {:#x}", size, phys_addr); + mutex.unlock(); return ORBIS_KERNEL_ERROR_ENOMEM; } // If we need to perform extra validation, then check for Mapped dmem areas too. - if (validate_dmem && dmem_area->second.dma_type == DMAType::Mapped) { + if (validate_dmem && dmem_area->second.dma_type == PhysicalMemoryType::Mapped) { LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at physical address {:#x}", size, phys_addr); + mutex.unlock(); return ORBIS_KERNEL_ERROR_EBUSY; } dmem_area++; } - - // If the prior loop succeeds, we need to loop through again and carve out mapped dmas. - // This needs to be a separate loop to avoid modifying dmem map during failed calls. - auto phys_addr_to_search = phys_addr; - auto remaining_size = size; - 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 = - phys_addr > dmem_area->second.base ? phys_addr : dmem_area->second.base; - const auto offset_in_dma = start_phys_addr - dmem_area->second.base; - const auto size_in_dma = dmem_area->second.size - offset_in_dma > remaining_size - ? remaining_size - : dmem_area->second.size - offset_in_dma; - const auto dmem_handle = CarveDmemArea(start_phys_addr, size_in_dma); - auto& new_dmem_area = dmem_handle->second; - new_dmem_area.dma_type = DMAType::Mapped; - - // 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; - remaining_size -= size_in_dma; - dmem_area = FindDmemArea(phys_addr_to_search); - } } // Limit the minimum address to SystemManagedVirtualBase to prevent hardware-specific issues. @@ -463,6 +483,7 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo 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); + mutex.unlock(); return ORBIS_KERNEL_ERROR_ENOMEM; } } else { @@ -473,6 +494,7 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo mapped_addr = SearchFree(mapped_addr, size, alignment); if (mapped_addr == -1) { // No suitable memory areas to map to + mutex.unlock(); return ORBIS_KERNEL_ERROR_ENOMEM; } } @@ -480,62 +502,109 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo // Create a memory area representing this mapping. const auto new_vma_handle = CarveVMA(mapped_addr, size); auto& new_vma = new_vma_handle->second; - - // If type is Flexible, we need to track how much flexible memory is used here. - // We also need to determine a reasonable physical base to perform this mapping at. - if (type == VMAType::Flexible) { - flexible_usage += size; - - // Find a suitable physical address - auto handle = fmem_map.begin(); - while (handle != fmem_map.end() && - (!handle->second.is_free || handle->second.size < size)) { - handle++; - } - - // Some games will end up fragmenting the flexible address space. - ASSERT_MSG(handle != fmem_map.end() && handle->second.is_free, - "No suitable physical memory areas to map"); - - // We'll use the start of this area as the physical backing for this mapping. - const auto new_fmem_handle = CarveFmemArea(handle->second.base, size); - auto& new_fmem_area = new_fmem_handle->second; - new_fmem_area.is_free = false; - phys_addr = new_fmem_area.base; - MergeAdjacent(fmem_map, new_fmem_handle); - } - + const bool is_exec = True(prot & MemoryProt::CpuExec); if (True(prot & MemoryProt::CpuWrite)) { // On PS4, read is appended to write mappings. prot |= MemoryProt::CpuRead; } - const bool is_exec = True(prot & MemoryProt::CpuExec); new_vma.disallow_merge = True(flags & MemoryMapFlags::NoCoalesce); new_vma.prot = prot; new_vma.name = name; new_vma.type = type; - new_vma.phys_base = phys_addr == -1 ? 0 : phys_addr; - new_vma.is_exec = is_exec; + new_vma.phys_areas.clear(); - if (type == VMAType::Reserved) { - // Technically this should be done for direct and flexible mappings too, - // But some Windows-specific limitations make that hard to accomplish. + // If type is Flexible, we need to track how much flexible memory is used here. + // We also need to determine a reasonable physical base to perform this mapping at. + if (type == VMAType::Flexible) { + // Find suitable physical addresses + auto handle = fmem_map.begin(); + u64 remaining_size = size; + VAddr current_addr = mapped_addr; + while (handle != fmem_map.end() && remaining_size != 0) { + if (handle->second.dma_type != PhysicalMemoryType::Free) { + // If the handle isn't free, we cannot use it. + handle++; + continue; + } + + // Determine the size we can map here. + u64 size_to_map = std::min(remaining_size, handle->second.size); + + // Create a physical area + const auto new_fmem_handle = CarvePhysArea(fmem_map, handle->second.base, size_to_map); + auto& new_fmem_area = new_fmem_handle->second; + new_fmem_area.dma_type = PhysicalMemoryType::Flexible; + + // Add the new area to the vma, merge it with any similar tracked areas. + new_vma.phys_areas[current_addr - mapped_addr] = new_fmem_handle->second; + MergeAdjacent(new_vma.phys_areas, new_vma.phys_areas.find(current_addr - mapped_addr)); + + // Perform an address space mapping for each physical area + void* out_addr = impl.Map(current_addr, size_to_map, new_fmem_area.base, is_exec); + TRACK_ALLOC(out_addr, size_to_map, "VMEM"); + + handle = MergeAdjacent(fmem_map, new_fmem_handle); + current_addr += size_to_map; + remaining_size -= size_to_map; + flexible_usage += size_to_map; + handle++; + } + 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; + u64 remaining_size = size; + 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 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); + const auto dmem_handle = CarvePhysArea(dmem_map, start_phys_addr, size_in_dma); + auto& new_dmem_area = dmem_handle->second; + 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)); + + // 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; + remaining_size -= size_in_dma; + dmem_area = FindDmemArea(phys_addr_to_search); + } + ASSERT_MSG(remaining_size == 0, "Failed to map physical memory"); + } + + if (new_vma.type != VMAType::Direct || sdk_version >= Common::ElfInfo::FW_20) { + // Merge this VMA with similar nearby areas + // Direct memory mappings only coalesce on SDK version 2.00 or later. MergeAdjacent(vma_map, new_vma_handle); } - if (type == VMAType::Reserved || type == VMAType::PoolReserved) { - // For Reserved/PoolReserved mappings, we don't perform any address space allocations. - // Just set out_addr to mapped_addr instead. - *out_addr = std::bit_cast(mapped_addr); - } else { + *out_addr = std::bit_cast(mapped_addr); + if (type != VMAType::Reserved && type != VMAType::PoolReserved) { + // Flexible address space mappings were performed while finding direct memory areas. + if (type != VMAType::Flexible) { + impl.Map(mapped_addr, size, phys_addr, is_exec); + } + TRACK_ALLOC(*out_addr, size, "VMEM"); + + mutex.unlock(); + // If this is not a reservation, then map to GPU and address space if (IsValidGpuMapping(mapped_addr, size)) { rasterizer->MapMemory(mapped_addr, size); } - *out_addr = impl.Map(mapped_addr, size, alignment, phys_addr, is_exec); - - TRACK_ALLOC(*out_addr, size, "VMEM"); + } else { + mutex.unlock(); } return ORBIS_OK; @@ -547,13 +616,14 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory ASSERT_MSG(IsValidMapping(mapped_addr, size), "Attempted to access invalid address {:#x}", mapped_addr); - std::scoped_lock lk{mutex}; + mutex.lock(); // Find first free area to map the file. if (False(flags & MemoryMapFlags::Fixed)) { mapped_addr = SearchFree(mapped_addr, size, 1); if (mapped_addr == -1) { // No suitable memory areas to map to + mutex.unlock(); return ORBIS_KERNEL_ERROR_ENOMEM; } } @@ -571,11 +641,13 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory auto file = h->GetFile(fd); if (file == nullptr) { LOG_WARNING(Kernel_Vmm, "Invalid file for mmap, fd {}", fd); + mutex.unlock(); return ORBIS_KERNEL_ERROR_EBADF; } if (file->type != Core::FileSys::FileType::Regular) { LOG_WARNING(Kernel_Vmm, "Unsupported file type for mmap, fd {}", fd); + mutex.unlock(); return ORBIS_KERNEL_ERROR_EBADF; } @@ -612,6 +684,8 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory new_vma.fd = fd; new_vma.type = VMAType::File; + mutex.unlock(); + *out_addr = std::bit_cast(mapped_addr); return ORBIS_OK; } @@ -619,13 +693,14 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); - std::scoped_lock lk{mutex}; + mutex.lock(); // Do an initial search to ensure this decommit is valid. auto it = FindVMA(virtual_addr); while (it != vma_map.end() && it->second.base + it->second.size <= virtual_addr + size) { if (it->second.type != VMAType::PoolReserved && it->second.type != VMAType::Pooled) { LOG_ERROR(Kernel_Vmm, "Attempting to decommit non-pooled memory!"); + mutex.unlock(); return ORBIS_KERNEL_ERROR_EINVAL; } it++; @@ -635,36 +710,46 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { u64 remaining_size = size; VAddr current_addr = virtual_addr; while (remaining_size != 0) { - const auto it = FindVMA(current_addr); - const auto& vma_base = it->second; - const bool is_exec = vma_base.is_exec; + const auto handle = FindVMA(current_addr); + 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)) { + mutex.unlock(); rasterizer->UnmapMemory(current_addr, size_in_vma); + mutex.lock(); } // Track how much pooled memory is decommitted pool_budget += size_in_vma; // Re-pool the direct memory used by this mapping - const auto unmap_phys_base = vma_base.phys_base + start_in_vma; - const auto new_dmem_handle = CarveDmemArea(unmap_phys_base, size_in_vma); - auto& new_dmem_area = new_dmem_handle->second; - new_dmem_area.dma_type = DMAType::Pooled; + u64 size_to_free = size_in_vma; + auto phys_handle = std::prev(vma_base.phys_areas.upper_bound(start_in_vma)); + while (phys_handle != vma_base.phys_areas.end() && size_to_free > 0) { + // Calculate physical memory offset, address, and size + u64 dma_offset = + std::max(phys_handle->first, start_in_vma) - phys_handle->first; + PAddr phys_addr = phys_handle->second.base + dma_offset; + u64 size_in_dma = + std::min(size_to_free, phys_handle->second.size - dma_offset); - // Coalesce with nearby direct memory areas. - MergeAdjacent(dmem_map, new_dmem_handle); - } + // Create a new dmem area reflecting the pooled region + const auto new_dmem_handle = CarvePhysArea(dmem_map, phys_addr, size_in_dma); + auto& new_dmem_area = new_dmem_handle->second; + new_dmem_area.dma_type = PhysicalMemoryType::Pooled; - if (vma_base.type != VMAType::PoolReserved) { - // Unmap the memory region. - impl.Unmap(vma_base.base, vma_base.size, start_in_vma, start_in_vma + size_in_vma, - vma_base.phys_base, vma_base.is_exec, true, false); - TRACK_FREE(virtual_addr, "VMEM"); + // Coalesce with nearby direct memory areas. + MergeAdjacent(dmem_map, new_dmem_handle); + + // Increment loop + size_to_free -= size_in_dma; + phys_handle++; + } + ASSERT_MSG(size_to_free == 0, "Failed to decommit pooled memory"); } // Mark region as pool reserved and attempt to coalesce it with neighbours. @@ -672,108 +757,116 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { auto& vma = new_it->second; vma.type = VMAType::PoolReserved; vma.prot = MemoryProt::NoAccess; - vma.phys_base = 0; vma.disallow_merge = false; vma.name = "anon"; + vma.phys_areas.clear(); MergeAdjacent(vma_map, new_it); current_addr += size_in_vma; remaining_size -= size_in_vma; } + // Unmap from address space + impl.Unmap(virtual_addr, size, true); + TRACK_FREE(virtual_addr, "VMEM"); + + mutex.unlock(); return ORBIS_OK; } s32 MemoryManager::UnmapMemory(VAddr virtual_addr, u64 size) { - std::scoped_lock lk{mutex}; if (size == 0) { return ORBIS_OK; } + mutex.lock(); 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); - return UnmapMemoryImpl(virtual_addr, size); + u64 bytes_unmapped = UnmapMemoryImpl(virtual_addr, size); + mutex.unlock(); + return bytes_unmapped; } u64 MemoryManager::UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma_base, u64 size) { - const auto vma_base_addr = vma_base.base; - const auto vma_base_size = vma_base.size; - const auto type = vma_base.type; - const auto phys_base = vma_base.phys_base; - const bool is_exec = vma_base.is_exec; - const auto start_in_vma = virtual_addr - vma_base_addr; - const auto adjusted_size = - vma_base_size - start_in_vma < size ? vma_base_size - start_in_vma : size; - const bool has_backing = HasPhysicalBacking(vma_base) || type == VMAType::File; - const auto prot = vma_base.prot; - const bool readonly_file = prot == MemoryProt::CpuRead && type == VMAType::File; + 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 (type == VMAType::Free) { - return adjusted_size; + if (vma_base.type == VMAType::Free || vma_base.type == VMAType::Pooled) { + return size_in_vma; } - if (type == VMAType::Direct) { - // Unmap all direct memory areas covered by this unmap. - auto phys_addr = phys_base + start_in_vma; - auto remaining_size = adjusted_size; - DMemHandle dmem_handle = FindDmemArea(phys_addr); - while (dmem_handle != dmem_map.end() && remaining_size > 0) { - const auto start_in_dma = phys_addr - dmem_handle->second.base; - const auto size_in_dma = dmem_handle->second.size - start_in_dma > remaining_size - ? remaining_size - : dmem_handle->second.size - start_in_dma; - dmem_handle = CarveDmemArea(phys_addr, size_in_dma); - auto& dmem_area = dmem_handle->second; - dmem_area.dma_type = DMAType::Allocated; - remaining_size -= dmem_area.size; - phys_addr += dmem_area.size; + PAddr phys_base = 0; + VAddr current_addr = virtual_addr; + if (vma_base.phys_areas.size() > 0) { + u64 size_to_free = size_in_vma; + auto phys_handle = std::prev(vma_base.phys_areas.upper_bound(start_in_vma)); + while (phys_handle != vma_base.phys_areas.end() && size_to_free > 0) { + // Calculate physical memory offset, address, and size + u64 dma_offset = std::max(phys_handle->first, start_in_vma) - phys_handle->first; + PAddr phys_addr = phys_handle->second.base + dma_offset; + u64 size_in_dma = std::min(size_to_free, phys_handle->second.size - dma_offset); - // Check if we can coalesce any dmem areas. - MergeAdjacent(dmem_map, dmem_handle); - dmem_handle = FindDmemArea(phys_addr); + // Create a new dmem area reflecting the pooled region + if (vma_type == VMAType::Direct) { + const auto new_dmem_handle = CarvePhysArea(dmem_map, phys_addr, size_in_dma); + auto& new_dmem_area = new_dmem_handle->second; + new_dmem_area.dma_type = PhysicalMemoryType::Allocated; + + // Coalesce with nearby direct memory areas. + MergeAdjacent(dmem_map, new_dmem_handle); + } else if (vma_type == VMAType::Flexible) { + // Update fmem_map + const auto new_fmem_handle = CarvePhysArea(fmem_map, phys_addr, size_in_dma); + auto& new_fmem_area = new_fmem_handle->second; + new_fmem_area.dma_type = PhysicalMemoryType::Free; + + // Coalesce with nearby flexible memory areas. + MergeAdjacent(fmem_map, new_fmem_handle); + + // Zero out the old memory data + const auto unmap_hardware_address = impl.BackingBase() + phys_addr; + std::memset(unmap_hardware_address, 0, size_in_dma); + + // Update flexible usage + flexible_usage -= size_in_dma; + } + + // Increment through loop + size_to_free -= size_in_dma; + phys_handle++; } - } - - if (type == VMAType::Flexible) { - flexible_usage -= adjusted_size; - - // Now that there is a physical backing used for flexible memory, - // manually erase the contents before unmapping to prevent possible issues. - const auto unmap_hardware_address = impl.BackingBase() + phys_base + start_in_vma; - std::memset(unmap_hardware_address, 0, adjusted_size); - - // Address space unmap needs the physical_base from the start of the vma, - // so calculate the phys_base to unmap from here. - const auto unmap_phys_base = phys_base + start_in_vma; - const auto new_fmem_handle = CarveFmemArea(unmap_phys_base, adjusted_size); - auto& new_fmem_area = new_fmem_handle->second; - new_fmem_area.is_free = true; - MergeAdjacent(fmem_map, new_fmem_handle); + ASSERT_MSG(size_to_free == 0, "Failed to unmap physical memory"); } // Mark region as free and attempt to coalesce it with neighbours. - const auto new_it = CarveVMA(virtual_addr, adjusted_size); + const auto new_it = CarveVMA(virtual_addr, size_in_vma); auto& vma = new_it->second; vma.type = VMAType::Free; vma.prot = MemoryProt::NoAccess; - vma.phys_base = 0; + vma.phys_areas.clear(); vma.disallow_merge = false; vma.name = ""; MergeAdjacent(vma_map, new_it); - if (type != VMAType::Reserved && type != VMAType::PoolReserved) { + if (vma_type != VMAType::Reserved && vma_type != VMAType::PoolReserved) { + // Unmap the memory region. + impl.Unmap(virtual_addr, size_in_vma, has_backing); + TRACK_FREE(virtual_addr, "VMEM"); + // If this mapping has GPU access, unmap from GPU. if (IsValidGpuMapping(virtual_addr, size)) { + mutex.unlock(); rasterizer->UnmapMemory(virtual_addr, size); + mutex.lock(); } - - // Unmap the memory region. - impl.Unmap(vma_base_addr, vma_base_size, start_in_vma, start_in_vma + adjusted_size, - phys_base, is_exec, has_backing, readonly_file); - TRACK_FREE(virtual_addr, "VMEM"); } - return adjusted_size; + return size_in_vma; } s32 MemoryManager::UnmapMemoryImpl(VAddr virtual_addr, u64 size) { @@ -792,12 +885,13 @@ s32 MemoryManager::UnmapMemoryImpl(VAddr virtual_addr, u64 size) { s32 MemoryManager::QueryProtection(VAddr addr, void** start, void** end, u32* prot) { ASSERT_MSG(IsValidMapping(addr), "Attempted to access invalid address {:#x}", addr); - std::scoped_lock lk{mutex}; + mutex.lock_shared(); const auto it = FindVMA(addr); const auto& vma = it->second; if (vma.IsFree()) { LOG_ERROR(Kernel_Vmm, "Address {:#x} is not mapped", addr); + mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_EACCES; } @@ -810,6 +904,8 @@ s32 MemoryManager::QueryProtection(VAddr addr, void** start, void** end, u32* pr if (prot != nullptr) { *prot = static_cast(vma.prot); } + + mutex.unlock_shared(); return ORBIS_OK; } @@ -872,8 +968,6 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea& vma_base, u64 siz } s32 MemoryManager::Protect(VAddr addr, u64 size, MemoryProt prot) { - std::scoped_lock lk{mutex}; - // If size is zero, then there's nothing to protect if (size == 0) { return ORBIS_OK; @@ -887,6 +981,7 @@ s32 MemoryManager::Protect(VAddr addr, u64 size, MemoryProt prot) { MemoryProt::CpuReadWrite | MemoryProt::CpuExec | MemoryProt::GpuReadWrite; MemoryProt valid_flags = prot & flag_mask; + mutex.lock(); // Protect all VMAs between addr and addr + size. s64 protected_bytes = 0; while (protected_bytes < size) { @@ -899,18 +994,18 @@ s32 MemoryManager::Protect(VAddr addr, u64 size, MemoryProt prot) { auto result = ProtectBytes(addr + protected_bytes, vma_base, size - protected_bytes, prot); if (result < 0) { // ProtectBytes returned an error, return it + mutex.unlock(); return result; } protected_bytes += result; } + mutex.unlock(); return ORBIS_OK; } s32 MemoryManager::VirtualQuery(VAddr addr, s32 flags, ::Libraries::Kernel::OrbisVirtualQueryInfo* info) { - std::scoped_lock lk{mutex}; - // FindVMA on addresses before the vma_map return garbage data. auto query_addr = addr < impl.SystemManagedVirtualBase() ? impl.SystemManagedVirtualBase() : addr; @@ -918,6 +1013,8 @@ s32 MemoryManager::VirtualQuery(VAddr addr, s32 flags, LOG_WARNING(Kernel_Vmm, "VirtualQuery on free memory region"); return ORBIS_KERNEL_ERROR_EACCES; } + + mutex.lock_shared(); auto it = FindVMA(query_addr); while (it != vma_map.end() && it->second.type == VMAType::Free && flags == 1) { @@ -925,6 +1022,7 @@ s32 MemoryManager::VirtualQuery(VAddr addr, s32 flags, } if (it == vma_map.end() || it->second.type == VMAType::Free) { LOG_WARNING(Kernel_Vmm, "VirtualQuery on free memory region"); + mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_EACCES; } @@ -938,9 +1036,12 @@ s32 MemoryManager::VirtualQuery(VAddr addr, s32 flags, info->is_stack = vma.type == VMAType::Stack ? 1 : 0; info->is_pooled = vma.type == VMAType::PoolReserved || vma.type == VMAType::Pooled ? 1 : 0; info->is_committed = vma.IsMapped() ? 1 : 0; - if (vma.type == VMAType::Direct || vma.type == VMAType::Pooled) { - // Offset is only assigned for direct and pooled mappings. - info->offset = vma.phys_base; + info->memory_type = 0; + if (vma.type == VMAType::Direct) { + // Offset is only assigned for direct mappings. + ASSERT_MSG(vma.phys_areas.size() > 0, "No physical backing for direct mapping?"); + info->offset = vma.phys_areas.begin()->second.base; + info->memory_type = vma.phys_areas.begin()->second.memory_type; } if (vma.type == VMAType::Reserved || vma.type == VMAType::PoolReserved) { // Protection is hidden from reserved mappings. @@ -949,34 +1050,27 @@ s32 MemoryManager::VirtualQuery(VAddr addr, s32 flags, strncpy(info->name, vma.name.data(), ::Libraries::Kernel::ORBIS_KERNEL_MAXIMUM_NAME_LENGTH); - if (vma.type == VMAType::Direct) { - const auto dmem_it = FindDmemArea(vma.phys_base); - ASSERT_MSG(vma.phys_base <= dmem_it->second.GetEnd(), "vma.phys_base is not in dmem_map!"); - info->memory_type = dmem_it->second.memory_type; - } else { - info->memory_type = ::Libraries::Kernel::ORBIS_KERNEL_WB_ONION; - } - + mutex.unlock_shared(); return ORBIS_OK; } s32 MemoryManager::DirectMemoryQuery(PAddr addr, bool find_next, ::Libraries::Kernel::OrbisQueryInfo* out_info) { - std::scoped_lock lk{mutex}; - if (addr >= total_direct_size) { LOG_WARNING(Kernel_Vmm, "Unable to find allocated direct memory region to query!"); return ORBIS_KERNEL_ERROR_EACCES; } + mutex.lock_shared(); auto dmem_area = FindDmemArea(addr); - while (dmem_area != dmem_map.end() && dmem_area->second.dma_type == DMAType::Free && + while (dmem_area != dmem_map.end() && dmem_area->second.dma_type == PhysicalMemoryType::Free && find_next) { dmem_area++; } - if (dmem_area == dmem_map.end() || dmem_area->second.dma_type == DMAType::Free) { + if (dmem_area == dmem_map.end() || dmem_area->second.dma_type == PhysicalMemoryType::Free) { LOG_WARNING(Kernel_Vmm, "Unable to find allocated direct memory region to query!"); + mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_EACCES; } @@ -986,25 +1080,26 @@ s32 MemoryManager::DirectMemoryQuery(PAddr addr, bool find_next, // Loop through all sequential mapped or allocated dmem areas // to determine the hardware accurate end. while (dmem_area != dmem_map.end() && dmem_area->second.memory_type == out_info->memoryType && - (dmem_area->second.dma_type == DMAType::Mapped || - dmem_area->second.dma_type == DMAType::Allocated)) { + (dmem_area->second.dma_type == PhysicalMemoryType::Mapped || + dmem_area->second.dma_type == PhysicalMemoryType::Allocated)) { out_info->end = dmem_area->second.GetEnd(); dmem_area++; } + mutex.unlock_shared(); return ORBIS_OK; } s32 MemoryManager::DirectQueryAvailable(PAddr search_start, PAddr search_end, u64 alignment, PAddr* phys_addr_out, u64* size_out) { - std::scoped_lock lk{mutex}; + mutex.lock_shared(); auto dmem_area = FindDmemArea(search_start); PAddr paddr{}; u64 max_size{}; while (dmem_area != dmem_map.end()) { - if (dmem_area->second.dma_type != DMAType::Free) { + if (dmem_area->second.dma_type != PhysicalMemoryType::Free) { dmem_area++; continue; } @@ -1037,18 +1132,20 @@ s32 MemoryManager::DirectQueryAvailable(PAddr search_start, PAddr search_end, u6 dmem_area++; } + mutex.unlock_shared(); *phys_addr_out = paddr; *size_out = max_size; return ORBIS_OK; } s32 MemoryManager::SetDirectMemoryType(VAddr addr, u64 size, s32 memory_type) { - std::scoped_lock lk{mutex}; + mutex.lock(); ASSERT_MSG(IsValidMapping(addr, size), "Attempted to access invalid address {:#x}", addr); // Search through all VMAs covered by the provided range. // We aren't modifying these VMAs, so it's safe to iterate through them. + VAddr current_addr = addr; auto remaining_size = size; auto vma_handle = FindVMA(addr); while (vma_handle != vma_map.end() && vma_handle->second.base < addr + size) { @@ -1056,40 +1153,42 @@ s32 MemoryManager::SetDirectMemoryType(VAddr addr, u64 size, s32 memory_type) { if (vma_handle->second.type == VMAType::Direct || vma_handle->second.type == VMAType::Pooled) { // Calculate position in vma - const auto start_in_vma = addr - vma_handle->second.base; + const auto start_in_vma = current_addr - vma_handle->second.base; const auto size_in_vma = vma_handle->second.size - start_in_vma; - auto phys_addr = vma_handle->second.phys_base + start_in_vma; - auto size_to_modify = remaining_size > size_in_vma ? size_in_vma : remaining_size; + const auto base_phys_addr = vma_handle->second.phys_areas.begin()->second.base; + auto size_to_modify = std::min(remaining_size, size_in_vma); + for (auto& phys_handle : vma_handle->second.phys_areas) { + if (size_to_modify == 0) { + break; + } - // Loop through remaining dmem areas until the physical addresses represented - // are all adjusted. - DMemHandle dmem_handle = FindDmemArea(phys_addr); - while (dmem_handle != dmem_map.end() && size_in_vma >= size_to_modify && - size_to_modify > 0) { - const auto start_in_dma = phys_addr - dmem_handle->second.base; - const auto size_in_dma = dmem_handle->second.size - start_in_dma > size_to_modify - ? size_to_modify - : dmem_handle->second.size - start_in_dma; - dmem_handle = CarveDmemArea(phys_addr, size_in_dma); + const auto current_phys_addr = + std::max(base_phys_addr, phys_handle.second.base); + if (current_phys_addr >= phys_handle.second.base + phys_handle.second.size) { + continue; + } + const auto start_in_dma = current_phys_addr - phys_handle.second.base; + const auto size_in_dma = phys_handle.second.size - start_in_dma; + + phys_handle.second.memory_type = memory_type; + + auto dmem_handle = CarvePhysArea(dmem_map, current_phys_addr, size_in_dma); auto& dmem_area = dmem_handle->second; dmem_area.memory_type = memory_type; size_to_modify -= dmem_area.size; - phys_addr += dmem_area.size; - - // Check if we can coalesce any dmem areas now that the types are different. MergeAdjacent(dmem_map, dmem_handle); - dmem_handle = FindDmemArea(phys_addr); } } remaining_size -= vma_handle->second.size; vma_handle++; } + mutex.unlock(); return ORBIS_OK; } void MemoryManager::NameVirtualRange(VAddr virtual_addr, u64 size, std::string_view name) { - std::scoped_lock lk{mutex}; + mutex.lock(); // Sizes are aligned up to the nearest 16_KB auto aligned_size = Common::AlignUp(size, 16_KB); @@ -1116,6 +1215,8 @@ void MemoryManager::NameVirtualRange(VAddr virtual_addr, u64 size, std::string_v current_addr += it->second.size; it = FindVMA(current_addr); } + + mutex.unlock(); } s32 MemoryManager::GetDirectMemoryType(PAddr addr, s32* directMemoryTypeOut, @@ -1125,22 +1226,27 @@ s32 MemoryManager::GetDirectMemoryType(PAddr addr, s32* directMemoryTypeOut, return ORBIS_KERNEL_ERROR_ENOENT; } + mutex.lock_shared(); const auto& dmem_area = FindDmemArea(addr)->second; - if (dmem_area.dma_type == DMAType::Free) { + if (dmem_area.dma_type == PhysicalMemoryType::Free) { LOG_ERROR(Kernel_Vmm, "Unable to find allocated direct memory region to check type!"); + mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_ENOENT; } *directMemoryStartOut = reinterpret_cast(dmem_area.base); *directMemoryEndOut = reinterpret_cast(dmem_area.GetEnd()); *directMemoryTypeOut = dmem_area.memory_type; + mutex.unlock_shared(); return ORBIS_OK; } s32 MemoryManager::IsStack(VAddr addr, void** start, void** end) { ASSERT_MSG(IsValidMapping(addr), "Attempted to access invalid address {:#x}", addr); + mutex.lock_shared(); const auto& vma = FindVMA(addr)->second; if (vma.IsFree()) { + mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_EACCES; } @@ -1159,11 +1265,12 @@ s32 MemoryManager::IsStack(VAddr addr, void** start, void** end) { *end = reinterpret_cast(stack_end); } + mutex.unlock_shared(); return ORBIS_OK; } s32 MemoryManager::GetMemoryPoolStats(::Libraries::Kernel::OrbisKernelMemoryPoolBlockStats* stats) { - std::scoped_lock lk{mutex}; + mutex.lock_shared(); // Run through dmem_map, determine how much physical memory is currently committed constexpr u64 block_size = 64_KB; @@ -1171,7 +1278,7 @@ s32 MemoryManager::GetMemoryPoolStats(::Libraries::Kernel::OrbisKernelMemoryPool auto dma_handle = dmem_map.begin(); while (dma_handle != dmem_map.end()) { - if (dma_handle->second.dma_type == DMAType::Committed) { + if (dma_handle->second.dma_type == PhysicalMemoryType::Committed) { committed_size += dma_handle->second.size; } dma_handle++; @@ -1182,6 +1289,8 @@ s32 MemoryManager::GetMemoryPoolStats(::Libraries::Kernel::OrbisKernelMemoryPool // TODO: Determine how "cached blocks" work stats->allocated_cached_blocks = 0; stats->available_cached_blocks = 0; + + mutex.unlock_shared(); return ORBIS_OK; } @@ -1251,6 +1360,52 @@ VAddr MemoryManager::SearchFree(VAddr virtual_addr, u64 size, u32 alignment) { return -1; } +MemoryManager::VMAHandle MemoryManager::MergeAdjacent(VMAMap& handle_map, VMAHandle iter) { + const auto next_vma = std::next(iter); + if (next_vma != handle_map.end() && iter->second.CanMergeWith(next_vma->second)) { + u64 base_offset = iter->second.size; + iter->second.size += next_vma->second.size; + for (auto& area : next_vma->second.phys_areas) { + iter->second.phys_areas[base_offset + area.first] = area.second; + } + handle_map.erase(next_vma); + } + + if (iter != handle_map.begin()) { + auto prev_vma = std::prev(iter); + if (prev_vma->second.CanMergeWith(iter->second)) { + u64 base_offset = prev_vma->second.size; + prev_vma->second.size += iter->second.size; + for (auto& area : iter->second.phys_areas) { + prev_vma->second.phys_areas[base_offset + area.first] = area.second; + } + handle_map.erase(iter); + iter = prev_vma; + } + } + + return iter; +} + +MemoryManager::PhysHandle MemoryManager::MergeAdjacent(PhysMap& handle_map, PhysHandle iter) { + const auto next_vma = std::next(iter); + if (next_vma != handle_map.end() && iter->second.CanMergeWith(next_vma->second)) { + iter->second.size += next_vma->second.size; + handle_map.erase(next_vma); + } + + if (iter != handle_map.begin()) { + auto prev_vma = std::prev(iter); + if (prev_vma->second.CanMergeWith(iter->second)) { + prev_vma->second.size += iter->second.size; + handle_map.erase(iter); + iter = prev_vma; + } + } + + return iter; +} + MemoryManager::VMAHandle MemoryManager::CarveVMA(VAddr virtual_addr, u64 size) { auto vma_handle = FindVMA(virtual_addr); @@ -1279,11 +1434,11 @@ MemoryManager::VMAHandle MemoryManager::CarveVMA(VAddr virtual_addr, u64 size) { return vma_handle; } -MemoryManager::DMemHandle MemoryManager::CarveDmemArea(PAddr addr, u64 size) { - auto dmem_handle = FindDmemArea(addr); - ASSERT_MSG(addr <= dmem_handle->second.GetEnd(), "Physical address not in dmem_map"); +MemoryManager::PhysHandle MemoryManager::CarvePhysArea(PhysMap& map, PAddr addr, u64 size) { + auto pmem_handle = std::prev(map.upper_bound(addr)); + ASSERT_MSG(addr <= pmem_handle->second.GetEnd(), "Physical address not in map"); - const DirectMemoryArea& area = dmem_handle->second; + const PhysicalMemoryArea& area = pmem_handle->second; ASSERT_MSG(area.base <= addr, "Adding an allocation to already allocated region"); const PAddr start_in_area = addr - area.base; @@ -1293,38 +1448,14 @@ MemoryManager::DMemHandle MemoryManager::CarveDmemArea(PAddr addr, u64 size) { if (end_in_vma != area.size) { // Split VMA at the end of the allocated region - Split(dmem_handle, end_in_vma); + Split(map, pmem_handle, end_in_vma); } if (start_in_area != 0) { // Split VMA at the start of the allocated region - dmem_handle = Split(dmem_handle, start_in_area); + pmem_handle = Split(map, pmem_handle, start_in_area); } - return dmem_handle; -} - -MemoryManager::FMemHandle MemoryManager::CarveFmemArea(PAddr addr, u64 size) { - auto fmem_handle = FindFmemArea(addr); - ASSERT_MSG(addr <= fmem_handle->second.GetEnd(), "Physical address not in fmem_map"); - - const FlexibleMemoryArea& area = fmem_handle->second; - ASSERT_MSG(area.base <= addr, "Adding an allocation to already allocated region"); - - const PAddr start_in_area = addr - area.base; - const PAddr end_in_vma = start_in_area + size; - ASSERT_MSG(end_in_vma <= area.size, "Mapping cannot fit inside free region: size = {:#x}", - size); - - if (end_in_vma != area.size) { - // Split VMA at the end of the allocated region - Split(fmem_handle, end_in_vma); - } - if (start_in_area != 0) { - // Split VMA at the start of the allocated region - fmem_handle = Split(fmem_handle, start_in_area); - } - - return fmem_handle; + return pmem_handle; } MemoryManager::VMAHandle MemoryManager::Split(VMAHandle vma_handle, u64 offset_in_vma) { @@ -1337,13 +1468,43 @@ MemoryManager::VMAHandle MemoryManager::Split(VMAHandle vma_handle, u64 offset_i new_vma.size -= offset_in_vma; if (HasPhysicalBacking(new_vma)) { - new_vma.phys_base += offset_in_vma; + // Update physical areas map for both areas + new_vma.phys_areas.clear(); + + std::map old_vma_phys_areas; + for (auto& [offset, region] : old_vma.phys_areas) { + // Fully contained in first VMA + if (offset + region.size <= offset_in_vma) { + old_vma_phys_areas[offset] = region; + } + // Split between both VMAs + if (offset < offset_in_vma && offset + region.size > offset_in_vma) { + // Create region in old VMA + u64 size_in_old = offset_in_vma - offset; + old_vma_phys_areas[offset] = PhysicalMemoryArea{ + region.base, size_in_old, region.memory_type, region.dma_type}; + // Create region in new VMA + PAddr new_base = region.base + size_in_old; + u64 size_in_new = region.size - size_in_old; + new_vma.phys_areas[0] = + PhysicalMemoryArea{new_base, size_in_new, region.memory_type, region.dma_type}; + } + // Fully contained in new VMA + if (offset >= offset_in_vma) { + new_vma.phys_areas[offset - offset_in_vma] = region; + } + } + + // Set old_vma's physical areas map to the newly created map + old_vma.phys_areas = old_vma_phys_areas; } + return vma_map.emplace_hint(std::next(vma_handle), new_vma.base, new_vma); } -MemoryManager::DMemHandle MemoryManager::Split(DMemHandle dmem_handle, u64 offset_in_area) { - auto& old_area = dmem_handle->second; +MemoryManager::PhysHandle MemoryManager::Split(PhysMap& map, PhysHandle phys_handle, + u64 offset_in_area) { + auto& old_area = phys_handle->second; ASSERT(offset_in_area < old_area.size && offset_in_area > 0); auto new_area = old_area; @@ -1352,19 +1513,7 @@ MemoryManager::DMemHandle MemoryManager::Split(DMemHandle dmem_handle, u64 offse new_area.base += offset_in_area; new_area.size -= offset_in_area; - return dmem_map.emplace_hint(std::next(dmem_handle), new_area.base, new_area); -} - -MemoryManager::FMemHandle MemoryManager::Split(FMemHandle fmem_handle, u64 offset_in_area) { - auto& old_area = fmem_handle->second; - ASSERT(offset_in_area < old_area.size && offset_in_area > 0); - - auto new_area = old_area; - old_area.size = offset_in_area; - new_area.base += offset_in_area; - new_area.size -= offset_in_area; - - return fmem_map.emplace_hint(std::next(fmem_handle), new_area.base, new_area); + return map.emplace_hint(std::next(phys_handle), new_area.base, new_area); } } // namespace Core diff --git a/src/core/memory.h b/src/core/memory.h index 7ebf9d34c..0664ed592 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -8,6 +8,7 @@ #include #include #include "common/enum.h" +#include "common/shared_first_mutex.h" #include "common/singleton.h" #include "common/types.h" #include "core/address_space.h" @@ -54,12 +55,37 @@ enum class MemoryMapFlags : u32 { }; DECLARE_ENUM_FLAG_OPERATORS(MemoryMapFlags) -enum class DMAType : u32 { +enum class PhysicalMemoryType : u32 { Free = 0, Allocated = 1, Mapped = 2, Pooled = 3, Committed = 4, + Flexible = 5, +}; + +struct PhysicalMemoryArea { + PAddr base = 0; + u64 size = 0; + s32 memory_type = 0; + PhysicalMemoryType dma_type = PhysicalMemoryType::Free; + + PAddr GetEnd() const { + return base + size; + } + + bool CanMergeWith(const PhysicalMemoryArea& next) const { + if (base + size != next.base) { + return false; + } + if (memory_type != next.memory_type) { + return false; + } + if (dma_type != next.dma_type) { + return false; + } + return true; + } }; enum class VMAType : u32 { @@ -74,60 +100,15 @@ enum class VMAType : u32 { File = 8, }; -struct DirectMemoryArea { - PAddr base = 0; - u64 size = 0; - s32 memory_type = 0; - DMAType dma_type = DMAType::Free; - - PAddr GetEnd() const { - return base + size; - } - - bool CanMergeWith(const DirectMemoryArea& next) const { - if (base + size != next.base) { - return false; - } - if (memory_type != next.memory_type) { - return false; - } - if (dma_type != next.dma_type) { - return false; - } - return true; - } -}; - -struct FlexibleMemoryArea { - PAddr base = 0; - u64 size = 0; - bool is_free = true; - - PAddr GetEnd() const { - return base + size; - } - - bool CanMergeWith(const FlexibleMemoryArea& next) const { - if (base + size != next.base) { - return false; - } - if (is_free != next.is_free) { - return false; - } - return true; - } -}; - struct VirtualMemoryArea { VAddr base = 0; u64 size = 0; - PAddr phys_base = 0; + std::map phys_areas; VMAType type = VMAType::Free; MemoryProt prot = MemoryProt::NoAccess; - bool disallow_merge = false; std::string name = ""; - uintptr_t fd = 0; - bool is_exec = false; + s32 fd = 0; + bool disallow_merge = false; bool Contains(VAddr addr, u64 size) const { return addr >= base && (addr + size) <= (base + this->size); @@ -141,30 +122,32 @@ struct VirtualMemoryArea { return type != VMAType::Free && type != VMAType::Reserved && type != VMAType::PoolReserved; } - bool CanMergeWith(const VirtualMemoryArea& next) const { + bool CanMergeWith(VirtualMemoryArea& next) { if (disallow_merge || next.disallow_merge) { return false; } if (base + size != next.base) { return false; } - if ((type == VMAType::Direct || type == VMAType::Flexible || type == VMAType::Pooled) && - phys_base + size != next.phys_base) { - return false; + if (type == VMAType::Direct && next.type == VMAType::Direct) { + auto& last_phys = std::prev(phys_areas.end())->second; + auto& first_next_phys = next.phys_areas.begin()->second; + if (last_phys.base + last_phys.size != first_next_phys.base || + last_phys.memory_type != first_next_phys.memory_type) { + return false; + } } if (prot != next.prot || type != next.type) { return false; } + return true; } }; class MemoryManager { - using DMemMap = std::map; - using DMemHandle = DMemMap::iterator; - - using FMemMap = std::map; - using FMemHandle = FMemMap::iterator; + using PhysMap = std::map; + using PhysHandle = PhysMap::iterator; using VMAMap = std::map; using VMAHandle = VMAMap::iterator; @@ -220,10 +203,11 @@ public: // Now make sure the full address range is contained in vma_map. auto vma_handle = FindVMA(virtual_addr); auto addr_to_check = virtual_addr; - s64 size_to_validate = size; + u64 size_to_validate = size; while (vma_handle != vma_map.end() && size_to_validate > 0) { const auto offset_in_vma = addr_to_check - vma_handle->second.base; - const auto size_in_vma = vma_handle->second.size - offset_in_vma; + const auto size_in_vma = + std::min(vma_handle->second.size - offset_in_vma, size_to_validate); size_to_validate -= size_in_vma; addr_to_check += size_in_vma; vma_handle++; @@ -245,7 +229,7 @@ public: void CopySparseMemory(VAddr source, u8* dest, u64 size); - bool TryWriteBacking(void* address, const void* data, u32 num_bytes); + bool TryWriteBacking(void* address, const void* data, u64 size); void SetupMemoryRegions(u64 flexible_size, bool use_extended_mem1, bool use_extended_mem2); @@ -300,34 +284,14 @@ private: return std::prev(vma_map.upper_bound(target)); } - DMemHandle FindDmemArea(PAddr target) { + PhysHandle FindDmemArea(PAddr target) { return std::prev(dmem_map.upper_bound(target)); } - FMemHandle FindFmemArea(PAddr target) { + PhysHandle FindFmemArea(PAddr target) { return std::prev(fmem_map.upper_bound(target)); } - template - Handle MergeAdjacent(auto& handle_map, Handle iter) { - const auto next_vma = std::next(iter); - if (next_vma != handle_map.end() && iter->second.CanMergeWith(next_vma->second)) { - iter->second.size += next_vma->second.size; - handle_map.erase(next_vma); - } - - if (iter != handle_map.begin()) { - auto prev_vma = std::prev(iter); - if (prev_vma->second.CanMergeWith(iter->second)) { - prev_vma->second.size += iter->second.size; - handle_map.erase(iter); - iter = prev_vma; - } - } - - return iter; - } - bool HasPhysicalBacking(VirtualMemoryArea vma) { return vma.type == VMAType::Direct || vma.type == VMAType::Flexible || vma.type == VMAType::Pooled; @@ -335,17 +299,17 @@ private: VAddr SearchFree(VAddr virtual_addr, u64 size, u32 alignment); + VMAHandle MergeAdjacent(VMAMap& map, VMAHandle iter); + + PhysHandle MergeAdjacent(PhysMap& map, PhysHandle iter); + VMAHandle CarveVMA(VAddr virtual_addr, u64 size); - DMemHandle CarveDmemArea(PAddr addr, u64 size); - - FMemHandle CarveFmemArea(PAddr addr, u64 size); + PhysHandle CarvePhysArea(PhysMap& map, PAddr addr, u64 size); VMAHandle Split(VMAHandle vma_handle, u64 offset_in_vma); - DMemHandle Split(DMemHandle dmem_handle, u64 offset_in_area); - - FMemHandle Split(FMemHandle fmem_handle, u64 offset_in_area); + PhysHandle Split(PhysMap& map, PhysHandle dmem_handle, u64 offset_in_area); u64 UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma_base, u64 size); @@ -353,14 +317,15 @@ private: private: AddressSpace impl; - DMemMap dmem_map; - FMemMap fmem_map; + PhysMap dmem_map; + PhysMap fmem_map; VMAMap vma_map; - std::mutex mutex; + Common::SharedFirstMutex mutex{}; u64 total_direct_size{}; u64 total_flexible_size{}; u64 flexible_usage{}; u64 pool_budget{}; + s32 sdk_version{}; Vulkan::Rasterizer* rasterizer{}; struct PrtArea { From 0d5c5f81a60701f15af32953b8aedeb201bc3f05 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Wed, 21 Jan 2026 22:49:35 +0200 Subject: [PATCH 45/83] video_core: Small readback optimization (#3941) * pm4_cmds: Handle nop packet overflow * liverpool: Detect DispatchDirect patches and promote to DispatchIndirect * clang.. * log removed --------- Co-authored-by: georgemoralis --- src/video_core/amdgpu/liverpool.cpp | 27 +++++++++++++++++++++++++-- src/video_core/amdgpu/pm4_cmds.h | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 3f307c51b..32ea1e8ed 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -830,7 +830,14 @@ Liverpool::Task Liverpool::ProcessCompute(std::span acb, u32 vqid) { FIBER_ENTER(acb_task_name[vqid]); auto& queue = asc_queues[{vqid}]; + struct IndirectPatch { + const PM4Header* header; + VAddr indirect_addr; + }; + boost::container::small_vector indirect_patches; + auto base_addr = reinterpret_cast(acb.data()); + size_t acb_size = acb.size_bytes(); while (!acb.empty()) { ProcessCommands(); @@ -919,8 +926,18 @@ Liverpool::Task Liverpool::ProcessCompute(std::span acb, u32 vqid) { dma_data->src_sel == DmaDataSrc::MemoryUsingL2) && (dma_data->dst_sel == DmaDataDst::Memory || dma_data->dst_sel == DmaDataDst::MemoryUsingL2)) { - rasterizer->CopyBuffer(dma_data->DstAddress(), dma_data->SrcAddress(), - dma_data->NumBytes(), false, false); + const u32 num_bytes = dma_data->NumBytes(); + const VAddr src_addr = dma_data->SrcAddress(); + const VAddr dst_addr = dma_data->DstAddress(); + const PM4Header* header = + reinterpret_cast(dst_addr - sizeof(PM4Header)); + if (dst_addr >= base_addr && dst_addr < base_addr + acb_size && + num_bytes == sizeof(PM4CmdDispatchIndirect::GroupDimensions) && + header->type == 3 && header->type3.opcode == PM4ItOpcode::DispatchDirect) { + indirect_patches.emplace_back(header, src_addr); + } else { + rasterizer->CopyBuffer(dst_addr, src_addr, num_bytes, false, false); + } } else { UNREACHABLE_MSG("WriteData src_sel = {}, dst_sel = {}", u32(dma_data->src_sel.Value()), u32(dma_data->dst_sel.Value())); @@ -964,6 +981,12 @@ Liverpool::Task Liverpool::ProcessCompute(std::span acb, u32 vqid) { } case PM4ItOpcode::DispatchDirect: { const auto* dispatch_direct = reinterpret_cast(header); + if (auto it = std::ranges::find(indirect_patches, header, &IndirectPatch::header); + it != indirect_patches.end()) { + const auto size = sizeof(PM4CmdDispatchIndirect::GroupDimensions); + rasterizer->DispatchIndirect(it->indirect_addr, 0, size); + break; + } auto& cs_program = GetCsRegs(); cs_program.dim_x = dispatch_direct->dim_x; cs_program.dim_y = dispatch_direct->dim_y; diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index eb48f3568..46ecb09d6 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -50,7 +50,7 @@ union PM4Type3Header { } u32 NumWords() const { - return count + 1; + return (count + 1) & 0x3fff; } u32 raw; From 62813c0106f4602a1b1fac9e40e827e344434931 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 22 Jan 2026 17:03:22 +0200 Subject: [PATCH 46/83] Unified UserId (#3949) * added OrbisUserServiceUserId in generic way to all classes that uses it * clang --- .../libraries/app_content/app_content.cpp | 4 +- src/core/libraries/audio3d/audio3d.cpp | 10 ++--- src/core/libraries/audio3d/audio3d.h | 9 ++-- .../libraries/companion/companion_httpd.cpp | 4 +- .../game_live_streaming/gamelivestreaming.h | 7 ++-- src/core/libraries/np/np_auth.h | 7 ++-- src/core/libraries/np/np_manager.h | 4 +- src/core/libraries/np/np_trophy.cpp | 3 +- src/core/libraries/np/np_trophy.h | 6 ++- src/core/libraries/pad/pad.cpp | 10 +++-- src/core/libraries/pad/pad.h | 12 ++++-- src/core/libraries/remote_play/remoteplay.cpp | 3 +- src/core/libraries/remote_play/remoteplay.h | 6 ++- src/core/libraries/save_data/save_backup.cpp | 2 +- src/core/libraries/save_data/save_backup.h | 9 ++-- .../libraries/save_data/save_instance.cpp | 10 ++--- src/core/libraries/save_data/save_instance.h | 17 ++++---- src/core/libraries/save_data/save_memory.cpp | 8 ++-- src/core/libraries/save_data/save_memory.h | 14 +++---- src/core/libraries/save_data/savedata.cpp | 42 ++++++++++--------- src/core/libraries/save_data/savedata.h | 18 ++++---- src/core/libraries/share_play/shareplay.h | 7 ++-- src/core/libraries/system/systemservice.h | 7 ++-- src/core/libraries/videoout/video_out.cpp | 4 +- src/core/libraries/videoout/video_out.h | 9 ++-- 25 files changed, 124 insertions(+), 108 deletions(-) diff --git a/src/core/libraries/app_content/app_content.cpp b/src/core/libraries/app_content/app_content.cpp index 1523c2703..a5952c7ea 100644 --- a/src/core/libraries/app_content/app_content.cpp +++ b/src/core/libraries/app_content/app_content.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 @@ -345,7 +345,7 @@ int PS4_SYSV_ABI sceAppContentInitialize(const OrbisAppContentInitParam* initPar if (addcont_count > 0) { SystemService::OrbisSystemServiceEvent event{}; event.event_type = SystemService::OrbisSystemServiceEventType::EntitlementUpdate; - event.service_entitlement_update.user_id = 0; + event.service_entitlement_update.userId = 0; event.service_entitlement_update.np_service_label = 0; SystemService::PushSystemServiceEvent(event); } diff --git a/src/core/libraries/audio3d/audio3d.cpp b/src/core/libraries/audio3d/audio3d.cpp index 2ddbcd890..3f5fdcf78 100644 --- a/src/core/libraries/audio3d/audio3d.cpp +++ b/src/core/libraries/audio3d/audio3d.cpp @@ -29,10 +29,10 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) { return AudioOut::sceAudioOutClose(handle); } -s32 PS4_SYSV_ABI -sceAudio3dAudioOutOpen(const OrbisAudio3dPortId port_id, const OrbisUserServiceUserId user_id, - s32 type, const s32 index, const u32 len, const u32 freq, - const AudioOut::OrbisAudioOutParamExtendedInformation param) { +s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen( + const OrbisAudio3dPortId port_id, const Libraries::UserService::OrbisUserServiceUserId user_id, + s32 type, const s32 index, const u32 len, const u32 freq, + const AudioOut::OrbisAudioOutParamExtendedInformation param) { LOG_INFO(Lib_Audio3d, "called, port_id = {}, user_id = {}, type = {}, index = {}, len = {}, freq = {}", port_id, user_id, type, index, len, freq); @@ -422,7 +422,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetStatus() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceAudio3dPortOpen(const OrbisUserServiceUserId user_id, +s32 PS4_SYSV_ABI sceAudio3dPortOpen(const Libraries::UserService::OrbisUserServiceUserId user_id, const OrbisAudio3dOpenParameters* parameters, OrbisAudio3dPortId* port_id) { LOG_INFO(Lib_Audio3d, "called, user_id = {}, parameters = {}, id = {}", user_id, diff --git a/src/core/libraries/audio3d/audio3d.h b/src/core/libraries/audio3d/audio3d.h index 1057c1f31..ae20e39a8 100644 --- a/src/core/libraries/audio3d/audio3d.h +++ b/src/core/libraries/audio3d/audio3d.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 @@ -15,8 +15,6 @@ class SymbolsResolver; namespace Libraries::Audio3d { -using OrbisUserServiceUserId = s32; - enum class OrbisAudio3dRate : u32 { ORBIS_AUDIO3D_RATE_48000 = 0, }; @@ -91,7 +89,8 @@ struct Audio3dState { }; s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(s32 handle); -s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id, OrbisUserServiceUserId user_id, +s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id, + Libraries::UserService::OrbisUserServiceUserId user_id, s32 type, s32 index, u32 len, u32 freq, AudioOut::OrbisAudioOutParamExtendedInformation param); s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(s32 handle, void* ptr); @@ -127,7 +126,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(OrbisAudio3dPortId port_id, u32* qu u32* queue_available); s32 PS4_SYSV_ABI sceAudio3dPortGetState(); s32 PS4_SYSV_ABI sceAudio3dPortGetStatus(); -s32 PS4_SYSV_ABI sceAudio3dPortOpen(OrbisUserServiceUserId user_id, +s32 PS4_SYSV_ABI sceAudio3dPortOpen(Libraries::UserService::OrbisUserServiceUserId user_id, const OrbisAudio3dOpenParameters* parameters, OrbisAudio3dPortId* port_id); s32 PS4_SYSV_ABI sceAudio3dPortPush(OrbisAudio3dPortId port_id, OrbisAudio3dBlocking blocking); diff --git a/src/core/libraries/companion/companion_httpd.cpp b/src/core/libraries/companion/companion_httpd.cpp index a8756c4cd..6ffc8c052 100644 --- a/src/core/libraries/companion/companion_httpd.cpp +++ b/src/core/libraries/companion/companion_httpd.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "common/logging/log.h" @@ -16,7 +16,7 @@ s32 PS4_SYSV_ABI sceCompanionHttpdAddHeader(const char* key, const char* value, } s32 PS4_SYSV_ABI -sceCompanionHttpdGet2ndScreenStatus(Libraries::UserService::OrbisUserServiceUserId) { +sceCompanionHttpdGet2ndScreenStatus(Libraries::UserService::OrbisUserServiceUserId userId) { LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); return ORBIS_OK; } diff --git a/src/core/libraries/game_live_streaming/gamelivestreaming.h b/src/core/libraries/game_live_streaming/gamelivestreaming.h index 0bab969bd..5ceee8ff5 100644 --- a/src/core/libraries/game_live_streaming/gamelivestreaming.h +++ b/src/core/libraries/game_live_streaming/gamelivestreaming.h @@ -1,9 +1,10 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "common/types.h" +#include "core/libraries/system/userservice.h" namespace Core::Loader { class SymbolsResolver; @@ -15,11 +16,11 @@ struct OrbisGameLiveStreamingStatus { bool isOnAir; u8 align[3]; u32 spectatorCounts; - s32 userId; + Libraries::UserService::OrbisUserServiceUserId userId; u8 reserved[60]; }; struct OrbisGameLiveStreamingStatus2 { - s32 userId; + Libraries::UserService::OrbisUserServiceUserId userId; bool isOnAir; u8 align[3]; u32 spectatorCounts; diff --git a/src/core/libraries/np/np_auth.h b/src/core/libraries/np/np_auth.h index 636210772..0894bd85d 100644 --- a/src/core/libraries/np/np_auth.h +++ b/src/core/libraries/np/np_auth.h @@ -1,10 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "common/types.h" #include "core/libraries/np/np_types.h" +#include "core/libraries/system/userservice.h" namespace Core::Loader { class SymbolsResolver; @@ -31,7 +32,7 @@ struct OrbisNpAuthGetAuthorizationCodeParameter { struct OrbisNpAuthGetAuthorizationCodeParameterA { u64 size; - s32 user_id; + Libraries::UserService::OrbisUserServiceUserId user_id; u8 padding[4]; const OrbisNpClientId* client_id; const char* scope; @@ -47,7 +48,7 @@ struct OrbisNpAuthGetIdTokenParameter { struct OrbisNpAuthGetIdTokenParameterA { u64 size; - s32 user_id; + Libraries::UserService::OrbisUserServiceUserId user_id; u8 padding[4]; const OrbisNpClientId* client_id; const OrbisNpClientSecret* client_secret; diff --git a/src/core/libraries/np/np_manager.h b/src/core/libraries/np/np_manager.h index 61a283ba7..59864c173 100644 --- a/src/core/libraries/np/np_manager.h +++ b/src/core/libraries/np/np_manager.h @@ -23,8 +23,8 @@ enum class OrbisNpState : u32 { SignedIn = 2, }; -using OrbisNpStateCallbackForNpToolkit = PS4_SYSV_ABI void (*)(s32 userId, OrbisNpState state, - void* userdata); +using OrbisNpStateCallbackForNpToolkit = PS4_SYSV_ABI void (*)( + Libraries::UserService::OrbisUserServiceUserId userId, OrbisNpState state, void* userdata); enum class OrbisNpGamePresenseStatus { Offline = 0, diff --git a/src/core/libraries/np/np_trophy.cpp b/src/core/libraries/np/np_trophy.cpp index 7468de13b..976d614c0 100644 --- a/src/core/libraries/np/np_trophy.cpp +++ b/src/core/libraries/np/np_trophy.cpp @@ -149,7 +149,8 @@ int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, s32 user_id, +s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, + Libraries::UserService::OrbisUserServiceUserId user_id, uint32_t service_label, u64 options) { ASSERT(options == 0ull); if (!context) { diff --git a/src/core/libraries/np/np_trophy.h b/src/core/libraries/np/np_trophy.h index 36e59e537..ab187ae13 100644 --- a/src/core/libraries/np/np_trophy.h +++ b/src/core/libraries/np/np_trophy.h @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include "common/types.h" #include "core/libraries/rtc/rtc.h" @@ -132,7 +133,8 @@ int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup(); int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion(); int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails(); int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature(); -s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, s32 user_id, +s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, + Libraries::UserService::OrbisUserServiceUserId user_id, u32 service_label, u64 options); s32 PS4_SYSV_ABI sceNpTrophyCreateHandle(OrbisNpTrophyHandle* handle); int PS4_SYSV_ABI sceNpTrophyDestroyContext(OrbisNpTrophyContext context); diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index 09f404969..f433a87cc 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -159,7 +159,8 @@ int PS4_SYSV_ABI scePadGetFeatureReport() { return ORBIS_OK; } -int PS4_SYSV_ABI scePadGetHandle(s32 userId, s32 type, s32 index) { +int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, + s32 index) { if (!g_initialized) { return ORBIS_PAD_ERROR_NOT_INITIALIZED; } @@ -256,7 +257,8 @@ int PS4_SYSV_ABI scePadMbusTerm() { return ORBIS_OK; } -int PS4_SYSV_ABI scePadOpen(s32 userId, s32 type, s32 index, const OrbisPadOpenParam* pParam) { +int PS4_SYSV_ABI scePadOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, + s32 index, const OrbisPadOpenParam* pParam) { if (!g_initialized) { return ORBIS_PAD_ERROR_NOT_INITIALIZED; } @@ -277,8 +279,8 @@ int PS4_SYSV_ABI scePadOpen(s32 userId, s32 type, s32 index, const OrbisPadOpenP return 1; // dummy } -int PS4_SYSV_ABI scePadOpenExt(s32 userId, s32 type, s32 index, - const OrbisPadOpenExtParam* pParam) { +int PS4_SYSV_ABI scePadOpenExt(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, + s32 index, const OrbisPadOpenExtParam* pParam) { LOG_ERROR(Lib_Pad, "(STUBBED) called"); if (Config::getUseSpecialPad()) { if (type != ORBIS_PAD_PORT_TYPE_SPECIAL) diff --git a/src/core/libraries/pad/pad.h b/src/core/libraries/pad/pad.h index ca6e8a73f..02ceaf3d9 100644 --- a/src/core/libraries/pad/pad.h +++ b/src/core/libraries/pad/pad.h @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include "common/enum.h" #include "common/types.h" @@ -276,7 +277,8 @@ int PS4_SYSV_ABI scePadGetExtControllerInformation(s32 handle, OrbisPadExtendedControllerInformation* pInfo); int PS4_SYSV_ABI scePadGetExtensionUnitInfo(); int PS4_SYSV_ABI scePadGetFeatureReport(); -int PS4_SYSV_ABI scePadGetHandle(s32 userId, s32 type, s32 index); +int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, + s32 index); int PS4_SYSV_ABI scePadGetIdleCount(); int PS4_SYSV_ABI scePadGetInfo(); int PS4_SYSV_ABI scePadGetInfoByPortType(); @@ -294,8 +296,10 @@ int PS4_SYSV_ABI scePadIsMoveReproductionModel(); int PS4_SYSV_ABI scePadIsValidHandle(); int PS4_SYSV_ABI scePadMbusInit(); int PS4_SYSV_ABI scePadMbusTerm(); -int PS4_SYSV_ABI scePadOpen(s32 userId, s32 type, s32 index, const OrbisPadOpenParam* pParam); -int PS4_SYSV_ABI scePadOpenExt(s32 userId, s32 type, s32 index, const OrbisPadOpenExtParam* pParam); +int PS4_SYSV_ABI scePadOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, + s32 index, const OrbisPadOpenParam* pParam); +int PS4_SYSV_ABI scePadOpenExt(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, + s32 index, const OrbisPadOpenExtParam* pParam); int PS4_SYSV_ABI scePadOpenExt2(); int PS4_SYSV_ABI scePadOutputReport(); int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num); diff --git a/src/core/libraries/remote_play/remoteplay.cpp b/src/core/libraries/remote_play/remoteplay.cpp index 06d9fccfb..775450d26 100644 --- a/src/core/libraries/remote_play/remoteplay.cpp +++ b/src/core/libraries/remote_play/remoteplay.cpp @@ -54,7 +54,8 @@ int PS4_SYSV_ABI sceRemoteplayGetConnectHistory() { return ORBIS_OK; } -int PS4_SYSV_ABI sceRemoteplayGetConnectionStatus(s32 userId, int* pStatus) { +int PS4_SYSV_ABI sceRemoteplayGetConnectionStatus( + Libraries::UserService::OrbisUserServiceUserId userId, int* pStatus) { *pStatus = ORBIS_REMOTEPLAY_CONNECTION_STATUS_DISCONNECT; return ORBIS_OK; } diff --git a/src/core/libraries/remote_play/remoteplay.h b/src/core/libraries/remote_play/remoteplay.h index 35465d6df..b4614dca0 100644 --- a/src/core/libraries/remote_play/remoteplay.h +++ b/src/core/libraries/remote_play/remoteplay.h @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include "common/types.h" namespace Core::Loader { @@ -24,7 +25,8 @@ int PS4_SYSV_ABI sceRemoteplayDisconnect(); int PS4_SYSV_ABI sceRemoteplayGeneratePinCode(); int PS4_SYSV_ABI sceRemoteplayGetApMode(); int PS4_SYSV_ABI sceRemoteplayGetConnectHistory(); -int PS4_SYSV_ABI sceRemoteplayGetConnectionStatus(s32 userId, int* pStatus); +int PS4_SYSV_ABI sceRemoteplayGetConnectionStatus( + Libraries::UserService::OrbisUserServiceUserId userId, int* pStatus); int PS4_SYSV_ABI sceRemoteplayGetConnectUserId(); int PS4_SYSV_ABI sceRemoteplayGetMbusDeviceInfo(); int PS4_SYSV_ABI sceRemoteplayGetOperationStatus(); diff --git a/src/core/libraries/save_data/save_backup.cpp b/src/core/libraries/save_data/save_backup.cpp index f85845f70..c5f66d883 100644 --- a/src/core/libraries/save_data/save_backup.cpp +++ b/src/core/libraries/save_data/save_backup.cpp @@ -167,7 +167,7 @@ void StopThread() { g_backup_thread_semaphore.release(); } -bool NewRequest(OrbisUserServiceUserId user_id, std::string_view title_id, +bool NewRequest(Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view title_id, std::string_view dir_name, OrbisSaveDataEventType origin) { auto save_path = SaveInstance::MakeDirSavePath(user_id, title_id, dir_name); diff --git a/src/core/libraries/save_data/save_backup.h b/src/core/libraries/save_data/save_backup.h index 83a263c9b..2a5e54d49 100644 --- a/src/core/libraries/save_data/save_backup.h +++ b/src/core/libraries/save_data/save_backup.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,12 +6,11 @@ #include #include +#include #include "common/types.h" namespace Libraries::SaveData { -using OrbisUserServiceUserId = s32; - namespace Backup { enum class WorkerStatus { @@ -32,7 +31,7 @@ enum class OrbisSaveDataEventType : u32 { struct BackupRequest { bool done{}; - OrbisUserServiceUserId user_id{}; + Libraries::UserService::OrbisUserServiceUserId user_id{}; std::string title_id{}; std::string dir_name{}; OrbisSaveDataEventType origin{}; @@ -45,7 +44,7 @@ void StartThread(); void StopThread(); -bool NewRequest(OrbisUserServiceUserId user_id, std::string_view title_id, +bool NewRequest(Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view title_id, std::string_view dir_name, OrbisSaveDataEventType origin); bool Restore(const std::filesystem::path& save_path); diff --git a/src/core/libraries/save_data/save_instance.cpp b/src/core/libraries/save_data/save_instance.cpp index 75a644fdb..bc6bbfd72 100644 --- a/src/core/libraries/save_data/save_instance.cpp +++ b/src/core/libraries/save_data/save_instance.cpp @@ -46,13 +46,13 @@ static const std::unordered_map default_title = { namespace Libraries::SaveData { -fs::path SaveInstance::MakeTitleSavePath(OrbisUserServiceUserId user_id, +fs::path SaveInstance::MakeTitleSavePath(Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view game_serial) { return Config::GetSaveDataPath() / std::to_string(user_id) / game_serial; } -fs::path SaveInstance::MakeDirSavePath(OrbisUserServiceUserId user_id, std::string_view game_serial, - std::string_view dir_name) { +fs::path SaveInstance::MakeDirSavePath(Libraries::UserService::OrbisUserServiceUserId user_id, + std::string_view game_serial, std::string_view dir_name) { return Config::GetSaveDataPath() / std::to_string(user_id) / game_serial / dir_name; } @@ -89,8 +89,8 @@ void SaveInstance::SetupDefaultParamSFO(PSF& param_sfo, std::string dir_name, #undef P } -SaveInstance::SaveInstance(int slot_num, OrbisUserServiceUserId user_id, std::string _game_serial, - std::string_view _dir_name, int max_blocks) +SaveInstance::SaveInstance(int slot_num, Libraries::UserService::OrbisUserServiceUserId user_id, + std::string _game_serial, std::string_view _dir_name, int max_blocks) : slot_num(slot_num), user_id(user_id), game_serial(std::move(_game_serial)), dir_name(_dir_name), max_blocks(std::clamp(max_blocks, OrbisSaveDataBlocksMin2, OrbisSaveDataBlocksMax)) { diff --git a/src/core/libraries/save_data/save_instance.h b/src/core/libraries/save_data/save_instance.h index 6e7ac8f66..b758649b2 100644 --- a/src/core/libraries/save_data/save_instance.h +++ b/src/core/libraries/save_data/save_instance.h @@ -1,10 +1,11 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include +#include #include "common/io_file.h" #include "core/file_format/psf.h" @@ -52,13 +53,13 @@ class SaveInstance { public: // Location of all save data for a title - static std::filesystem::path MakeTitleSavePath(OrbisUserServiceUserId user_id, - std::string_view game_serial); + static std::filesystem::path MakeTitleSavePath( + Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view game_serial); // Location of a specific save data directory - static std::filesystem::path MakeDirSavePath(OrbisUserServiceUserId user_id, - std::string_view game_serial, - std::string_view dir_name); + static std::filesystem::path MakeDirSavePath( + Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view game_serial, + std::string_view dir_name); static uint64_t GetMaxBlockFromSFO(const PSF& psf); @@ -67,8 +68,8 @@ public: static void SetupDefaultParamSFO(PSF& param_sfo, std::string dir_name, std::string game_serial); - explicit SaveInstance(int slot_num, OrbisUserServiceUserId user_id, std::string game_serial, - std::string_view dir_name, int max_blocks = 0); + explicit SaveInstance(int slot_num, Libraries::UserService::OrbisUserServiceUserId user_id, + std::string game_serial, std::string_view dir_name, int max_blocks = 0); ~SaveInstance(); diff --git a/src/core/libraries/save_data/save_memory.cpp b/src/core/libraries/save_data/save_memory.cpp index 5f5ba8fea..0a16d4756 100644 --- a/src/core/libraries/save_data/save_memory.cpp +++ b/src/core/libraries/save_data/save_memory.cpp @@ -88,8 +88,8 @@ std::string GetSaveDir(u32 slot_id) { return dir; } -std::filesystem::path GetSavePath(OrbisUserServiceUserId user_id, u32 slot_id, - std::string_view game_serial) { +std::filesystem::path GetSavePath(Libraries::UserService::OrbisUserServiceUserId user_id, + u32 slot_id, std::string_view game_serial) { std::string dir(StandardDirnameSaveDataMemory); if (slot_id > 0) { dir += std::to_string(slot_id); @@ -97,8 +97,8 @@ std::filesystem::path GetSavePath(OrbisUserServiceUserId user_id, u32 slot_id, return SaveInstance::MakeDirSavePath(user_id, game_serial, dir); } -size_t SetupSaveMemory(OrbisUserServiceUserId user_id, u32 slot_id, std::string_view game_serial, - size_t memory_size) { +size_t SetupSaveMemory(Libraries::UserService::OrbisUserServiceUserId user_id, u32 slot_id, + std::string_view game_serial, size_t memory_size) { std::lock_guard lck{g_slot_mtx}; const auto save_dir = GetSavePath(user_id, slot_id, game_serial); diff --git a/src/core/libraries/save_data/save_memory.h b/src/core/libraries/save_data/save_memory.h index 7765b04cd..b524de8bc 100644 --- a/src/core/libraries/save_data/save_memory.h +++ b/src/core/libraries/save_data/save_memory.h @@ -4,26 +4,24 @@ #pragma once #include +#include #include "core/libraries/save_data/save_backup.h" class PSF; -namespace Libraries::SaveData { -using OrbisUserServiceUserId = s32; -} // namespace Libraries::SaveData - namespace Libraries::SaveData::SaveMemory { void PersistMemory(u32 slot_id, bool lock = true); [[nodiscard]] std::string GetSaveDir(u32 slot_id); -[[nodiscard]] std::filesystem::path GetSavePath(OrbisUserServiceUserId user_id, u32 slot_id, - std::string_view game_serial); +[[nodiscard]] std::filesystem::path GetSavePath( + Libraries::UserService::OrbisUserServiceUserId user_id, u32 slot_id, + std::string_view game_serial); // returns the size of the save memory if exists -size_t SetupSaveMemory(OrbisUserServiceUserId user_id, u32 slot_id, std::string_view game_serial, - size_t memory_size); +size_t SetupSaveMemory(Libraries::UserService::OrbisUserServiceUserId user_id, u32 slot_id, + std::string_view game_serial, size_t memory_size); // Write the icon. Set buf to null to read the standard icon. void SetIcon(u32 slot_id, void* buf = nullptr, size_t buf_size = 0); diff --git a/src/core/libraries/save_data/savedata.cpp b/src/core/libraries/save_data/savedata.cpp index 7fba8ed21..a5199c297 100644 --- a/src/core/libraries/save_data/savedata.cpp +++ b/src/core/libraries/save_data/savedata.cpp @@ -42,7 +42,6 @@ enum class OrbisSaveDataSaveDataMemoryOption : u32 { UNLOCK_LIMITATIONS = 1 << 2, }; -using OrbisUserServiceUserId = s32; using OrbisSaveDataBlocks = u64; constexpr u32 OrbisSaveDataBlockSize = 32768; // 32 KiB @@ -97,7 +96,7 @@ struct OrbisSaveDataFingerprint { }; struct OrbisSaveDataBackup { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; s32 : 32; const OrbisSaveDataTitleId* titleId; const OrbisSaveDataDirName* dirName; @@ -106,7 +105,7 @@ struct OrbisSaveDataBackup { }; struct OrbisSaveDataCheckBackupData { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; s32 : 32; const OrbisSaveDataTitleId* titleId; const OrbisSaveDataDirName* dirName; @@ -116,7 +115,7 @@ struct OrbisSaveDataCheckBackupData { }; struct OrbisSaveDataDelete { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; s32 : 32; const OrbisSaveDataTitleId* titleId; const OrbisSaveDataDirName* dirName; @@ -153,7 +152,7 @@ struct OrbisSaveDataMemoryData { }; struct OrbisSaveDataMemoryGet2 { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; std::array _pad; OrbisSaveDataMemoryData* data; OrbisSaveDataParam* param; @@ -163,7 +162,7 @@ struct OrbisSaveDataMemoryGet2 { }; struct OrbisSaveDataMemorySet2 { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; std::array _pad; const OrbisSaveDataMemoryData* data; const OrbisSaveDataParam* param; @@ -175,7 +174,7 @@ struct OrbisSaveDataMemorySet2 { struct OrbisSaveDataMemorySetup2 { OrbisSaveDataSaveDataMemoryOption option; - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; size_t memorySize; size_t iconMemorySize; // +4.5 @@ -197,14 +196,14 @@ enum OrbisSaveDataMemorySyncOption : u32 { }; struct OrbisSaveDataMemorySync { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; u32 slotId; OrbisSaveDataMemorySyncOption option; std::array _reserved; }; struct OrbisSaveDataMount2 { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; s32 : 32; const OrbisSaveDataDirName* dirName; OrbisSaveDataBlocks blocks; @@ -214,7 +213,7 @@ struct OrbisSaveDataMount2 { }; struct OrbisSaveDataMount { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; s32 : 32; const OrbisSaveDataTitleId* titleId; const OrbisSaveDataDirName* dirName; @@ -245,7 +244,7 @@ struct OrbisSaveDataMountResult { }; struct OrbisSaveDataRestoreBackupData { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; s32 : 32; const OrbisSaveDataTitleId* titleId; const OrbisSaveDataDirName* dirName; @@ -256,7 +255,7 @@ struct OrbisSaveDataRestoreBackupData { }; struct OrbisSaveDataTransferringMount { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; const OrbisSaveDataTitleId* titleId; const OrbisSaveDataDirName* dirName; const OrbisSaveDataFingerprint* fingerprint; @@ -264,7 +263,7 @@ struct OrbisSaveDataTransferringMount { }; struct OrbisSaveDataDirNameSearchCond { - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; int : 32; const OrbisSaveDataTitleId* titleId; const OrbisSaveDataDirName* dirName; @@ -303,7 +302,7 @@ using OrbisSaveDataEventType = Backup::OrbisSaveDataEventType; struct OrbisSaveDataEvent { OrbisSaveDataEventType type; s32 errorCode; - OrbisUserServiceUserId userId; + Libraries::UserService::OrbisUserServiceUserId userId; std::array _pad; OrbisSaveDataTitleId titleId; OrbisSaveDataDirName dirName; @@ -1106,8 +1105,9 @@ int PS4_SYSV_ABI sceSaveDataGetSaveDataCount() { return ORBIS_OK; } -Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory(const OrbisUserServiceUserId userId, void* buf, - const size_t bufSize, const int64_t offset) { +Error PS4_SYSV_ABI +sceSaveDataGetSaveDataMemory(const Libraries::UserService::OrbisUserServiceUserId userId, void* buf, + const size_t bufSize, const int64_t offset) { LOG_DEBUG(Lib_SaveData, "Redirecting to sceSaveDataGetSaveDataMemory2"); OrbisSaveDataMemoryData data{}; data.buf = buf; @@ -1469,8 +1469,9 @@ int PS4_SYSV_ABI sceSaveDataSetSaveDataLibraryUser() { return ORBIS_OK; } -Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory(OrbisUserServiceUserId userId, void* buf, - size_t bufSize, int64_t offset) { +Error PS4_SYSV_ABI +sceSaveDataSetSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId, void* buf, + size_t bufSize, int64_t offset) { LOG_DEBUG(Lib_SaveData, "Redirecting to sceSaveDataSetSaveDataMemory2"); OrbisSaveDataMemoryData data{}; data.buf = buf; @@ -1527,8 +1528,9 @@ Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory2(const OrbisSaveDataMemorySet2* return Error::OK; } -Error PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory(OrbisUserServiceUserId userId, size_t memorySize, - OrbisSaveDataParam* param) { +Error PS4_SYSV_ABI +sceSaveDataSetupSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId, + size_t memorySize, OrbisSaveDataParam* param) { LOG_DEBUG(Lib_SaveData, "called: userId = {}, memorySize = {}", userId, memorySize); OrbisSaveDataMemorySetup2 setupParam{}; setupParam.userId = userId; diff --git a/src/core/libraries/save_data/savedata.h b/src/core/libraries/save_data/savedata.h index d1c625980..37a21dbc7 100644 --- a/src/core/libraries/save_data/savedata.h +++ b/src/core/libraries/save_data/savedata.h @@ -3,6 +3,7 @@ #pragma once +#include #include "common/cstring.h" #include "common/types.h" @@ -21,8 +22,6 @@ constexpr size_t OrbisSaveDataDetailMaxsize = 1024; // Maximum detail name size enum class Error : u32; enum class OrbisSaveDataParamType : u32; -using OrbisUserServiceUserId = s32; - // Maximum size for a title ID (4 uppercase letters + 5 digits) constexpr int OrbisSaveDataTitleIdDataSize = 10; // Maximum save directory name size @@ -126,8 +125,9 @@ Error PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint size_t paramBufSize, size_t* gotSize); Error PS4_SYSV_ABI sceSaveDataGetProgress(float* progress); int PS4_SYSV_ABI sceSaveDataGetSaveDataCount(); -Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory(OrbisUserServiceUserId userId, void* buf, - size_t bufSize, int64_t offset); +Error PS4_SYSV_ABI +sceSaveDataGetSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId, void* buf, + size_t bufSize, int64_t offset); Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory2(OrbisSaveDataMemoryGet2* getParam); int PS4_SYSV_ABI sceSaveDataGetSaveDataRootDir(); int PS4_SYSV_ABI sceSaveDataGetSaveDataRootPath(); @@ -163,11 +163,13 @@ Error PS4_SYSV_ABI sceSaveDataSetParam(const OrbisSaveDataMountPoint* mountPoint OrbisSaveDataParamType paramType, const void* paramBuf, size_t paramBufSize); int PS4_SYSV_ABI sceSaveDataSetSaveDataLibraryUser(); -Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory(OrbisUserServiceUserId userId, void* buf, - size_t bufSize, int64_t offset); +Error PS4_SYSV_ABI +sceSaveDataSetSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId, void* buf, + size_t bufSize, int64_t offset); Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory2(const OrbisSaveDataMemorySet2* setParam); -Error PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory(OrbisUserServiceUserId userId, size_t memorySize, - OrbisSaveDataParam* param); +Error PS4_SYSV_ABI +sceSaveDataSetupSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId, + size_t memorySize, OrbisSaveDataParam* param); Error PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory2(const OrbisSaveDataMemorySetup2* setupParam, OrbisSaveDataMemorySetupResult* result); int PS4_SYSV_ABI sceSaveDataShutdownStart(); diff --git a/src/core/libraries/share_play/shareplay.h b/src/core/libraries/share_play/shareplay.h index ca65c9a9d..b67b01e93 100644 --- a/src/core/libraries/share_play/shareplay.h +++ b/src/core/libraries/share_play/shareplay.h @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include "common/types.h" #include "core/libraries/np/np_types.h" @@ -21,8 +22,8 @@ struct OrbisSharePlayConnectionInfo { int mode; Libraries::Np::OrbisNpOnlineId hostOnlineId; Libraries::Np::OrbisNpOnlineId visitorOnlineId; - s32 hostUserId; - s32 visitorUserId; + Libraries::UserService::OrbisUserServiceUserId hostUserId; + Libraries::UserService::OrbisUserServiceUserId visitorUserId; }; int PS4_SYSV_ABI sceSharePlayCrashDaemon(); diff --git a/src/core/libraries/system/systemservice.h b/src/core/libraries/system/systemservice.h index b8bdf0b5f..e3eeb21dc 100644 --- a/src/core/libraries/system/systemservice.h +++ b/src/core/libraries/system/systemservice.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 // reference // https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/master/include/orbis/_types/sys_service.h @@ -7,6 +7,7 @@ #include #include #include "common/types.h" +#include "userservice.h" namespace Core::Loader { class SymbolsResolver; @@ -119,12 +120,12 @@ struct OrbisSystemServiceEvent { char boot_argument[7169]; } join_event; struct { - s32 user_id; + Libraries::UserService::OrbisUserServiceUserId userId; u32 np_service_label; u8 reserved[8184]; } service_entitlement_update; struct { - s32 user_id; + Libraries::UserService::OrbisUserServiceUserId userId; u32 np_service_label; u8 reserved[8184]; } unified_entitlement_update; diff --git a/src/core/libraries/videoout/video_out.cpp b/src/core/libraries/videoout/video_out.cpp index ece2640c9..e9176afdc 100644 --- a/src/core/libraries/videoout/video_out.cpp +++ b/src/core/libraries/videoout/video_out.cpp @@ -291,8 +291,8 @@ s32 PS4_SYSV_ABI sceVideoOutGetResolutionStatus(s32 handle, SceVideoOutResolutio return ORBIS_OK; } -s32 PS4_SYSV_ABI sceVideoOutOpen(SceUserServiceUserId userId, s32 busType, s32 index, - const void* param) { +s32 PS4_SYSV_ABI sceVideoOutOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 busType, + s32 index, const void* param) { LOG_INFO(Lib_VideoOut, "called"); ASSERT(busType == SCE_VIDEO_OUT_BUS_TYPE_MAIN); diff --git a/src/core/libraries/videoout/video_out.h b/src/core/libraries/videoout/video_out.h index 7db09530b..ba2732ff7 100644 --- a/src/core/libraries/videoout/video_out.h +++ b/src/core/libraries/videoout/video_out.h @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include "core/libraries/kernel/equeue.h" #include "core/libraries/videoout/buffer.h" @@ -12,8 +13,6 @@ class SymbolsResolver; namespace Libraries::VideoOut { -using SceUserServiceUserId = s32; // TODO move it to proper place - // SceVideoOutBusType constexpr int SCE_VIDEO_OUT_BUS_TYPE_MAIN = 0; // Main output constexpr int SCE_VIDEO_OUT_BUS_TYPE_AUX_SOCIAL_SCREEN = 5; // Aux output for social @@ -131,8 +130,8 @@ s32 PS4_SYSV_ABI sceVideoOutWaitVblank(s32 handle); s32 PS4_SYSV_ABI sceVideoOutSubmitFlip(s32 handle, s32 bufferIndex, s32 flipMode, s64 flipArg); s32 PS4_SYSV_ABI sceVideoOutGetFlipStatus(s32 handle, FlipStatus* status); s32 PS4_SYSV_ABI sceVideoOutGetResolutionStatus(s32 handle, SceVideoOutResolutionStatus* status); -s32 PS4_SYSV_ABI sceVideoOutOpen(SceUserServiceUserId userId, s32 busType, s32 index, - const void* param); +s32 PS4_SYSV_ABI sceVideoOutOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 busType, + s32 index, const void* param); s32 PS4_SYSV_ABI sceVideoOutClose(s32 handle); s32 PS4_SYSV_ABI sceVideoOutGetEventId(const Kernel::SceKernelEvent* ev); s32 PS4_SYSV_ABI sceVideoOutGetEventData(const Kernel::SceKernelEvent* ev, s64* data); From 508bad87d51a697cb6d4d4565ea8032438de73c5 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 22 Jan 2026 17:04:57 +0200 Subject: [PATCH 47/83] WebDialogBrowser module (#3938) * initial * added sceWebBrowserDialogUpdateStatus * sceWebBrowserDialogInitialize --- .../web_browser_dialog/webbrowserdialog.cpp | 40 +++++++++++++------ .../web_browser_dialog/webbrowserdialog.h | 9 +++-- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/core/libraries/web_browser_dialog/webbrowserdialog.cpp b/src/core/libraries/web_browser_dialog/webbrowserdialog.cpp index 444eaa765..5844affa2 100644 --- a/src/core/libraries/web_browser_dialog/webbrowserdialog.cpp +++ b/src/core/libraries/web_browser_dialog/webbrowserdialog.cpp @@ -5,9 +5,12 @@ #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/web_browser_dialog/webbrowserdialog.h" +#include "magic_enum/magic_enum.hpp" namespace Libraries::WebBrowserDialog { +static auto g_status = Libraries::CommonDialog::Status::NONE; + s32 PS4_SYSV_ABI sceWebBrowserDialogClose() { LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called"); return ORBIS_OK; @@ -23,14 +26,19 @@ s32 PS4_SYSV_ABI sceWebBrowserDialogGetResult() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceWebBrowserDialogGetStatus() { - LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called"); - return ORBIS_OK; +Libraries::CommonDialog::Status PS4_SYSV_ABI sceWebBrowserDialogGetStatus() { + LOG_TRACE(Lib_MsgDlg, "called status={}", magic_enum::enum_name(g_status)); + return g_status; } -s32 PS4_SYSV_ABI sceWebBrowserDialogInitialize() { - LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called"); - return ORBIS_OK; +Libraries::CommonDialog::Error PS4_SYSV_ABI sceWebBrowserDialogInitialize() { + if (CommonDialog::g_isInitialized) { + LOG_INFO(Lib_WebBrowserDialog, "already initialized"); + return Libraries::CommonDialog::Error::ALREADY_SYSTEM_INITIALIZED; + } + LOG_DEBUG(Lib_WebBrowserDialog, "initialized"); + CommonDialog::g_isInitialized = true; + return Libraries::CommonDialog::Error::OK; } s32 PS4_SYSV_ABI sceWebBrowserDialogNavigate() { @@ -63,14 +71,22 @@ s32 PS4_SYSV_ABI sceWebBrowserDialogSetZoom() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceWebBrowserDialogTerminate() { - LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called"); - return ORBIS_OK; +Libraries::CommonDialog::Error PS4_SYSV_ABI sceWebBrowserDialogTerminate() { + if (g_status == Libraries::CommonDialog::Status::RUNNING) { + LOG_ERROR(Lib_WebBrowserDialog, + "CloseWebBrowser Dialog unimplemented"); // sceWebBrowserDialogClose(); + } + if (g_status == Libraries::CommonDialog::Status::NONE) { + return Libraries::CommonDialog::Error::NOT_INITIALIZED; + } + g_status = Libraries::CommonDialog::Status::NONE; + CommonDialog::g_isUsed = false; + return Libraries::CommonDialog::Error::OK; } -s32 PS4_SYSV_ABI sceWebBrowserDialogUpdateStatus() { - LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called"); - return ORBIS_OK; +Libraries::CommonDialog::Status PS4_SYSV_ABI sceWebBrowserDialogUpdateStatus() { + LOG_TRACE(Lib_MsgDlg, "called status={}", magic_enum::enum_name(g_status)); + return g_status; } s32 PS4_SYSV_ABI Func_F2BE042771625F8C() { diff --git a/src/core/libraries/web_browser_dialog/webbrowserdialog.h b/src/core/libraries/web_browser_dialog/webbrowserdialog.h index 08f76a4fe..3dad7e1e9 100644 --- a/src/core/libraries/web_browser_dialog/webbrowserdialog.h +++ b/src/core/libraries/web_browser_dialog/webbrowserdialog.h @@ -3,6 +3,7 @@ #pragma once +#include #include "common/types.h" namespace Core::Loader { @@ -14,16 +15,16 @@ namespace Libraries::WebBrowserDialog { s32 PS4_SYSV_ABI sceWebBrowserDialogClose(); s32 PS4_SYSV_ABI sceWebBrowserDialogGetEvent(); s32 PS4_SYSV_ABI sceWebBrowserDialogGetResult(); -s32 PS4_SYSV_ABI sceWebBrowserDialogGetStatus(); -s32 PS4_SYSV_ABI sceWebBrowserDialogInitialize(); +Libraries::CommonDialog::Status PS4_SYSV_ABI sceWebBrowserDialogGetStatus(); +Libraries::CommonDialog::Error PS4_SYSV_ABI sceWebBrowserDialogInitialize(); s32 PS4_SYSV_ABI sceWebBrowserDialogNavigate(); s32 PS4_SYSV_ABI sceWebBrowserDialogOpen(); s32 PS4_SYSV_ABI sceWebBrowserDialogOpenForPredeterminedContent(); s32 PS4_SYSV_ABI sceWebBrowserDialogResetCookie(); s32 PS4_SYSV_ABI sceWebBrowserDialogSetCookie(); s32 PS4_SYSV_ABI sceWebBrowserDialogSetZoom(); -s32 PS4_SYSV_ABI sceWebBrowserDialogTerminate(); -s32 PS4_SYSV_ABI sceWebBrowserDialogUpdateStatus(); +Libraries::CommonDialog::Error PS4_SYSV_ABI sceWebBrowserDialogTerminate(); +Libraries::CommonDialog::Status PS4_SYSV_ABI sceWebBrowserDialogUpdateStatus(); s32 PS4_SYSV_ABI Func_F2BE042771625F8C(); void RegisterLib(Core::Loader::SymbolsResolver* sym); From fecfbb6b4af531bf768a2422baeab478e794df6b Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Thu, 22 Jan 2026 17:05:16 +0200 Subject: [PATCH 48/83] video_core: Small fixes regarding GDS (#3942) * shader_recompiler: Add missing descriptor type for GDS buffer * liverpool: Implement gds to memory store * macOS fix? --------- Co-authored-by: georgemoralis --- .../ir/passes/resource_tracking_pass.cpp | 17 ++++++++++++---- src/video_core/amdgpu/liverpool.cpp | 10 +++++++--- src/video_core/amdgpu/pm4_cmds.h | 20 ++++++++++++------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 53b161149..93129ac0e 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -660,6 +660,7 @@ void PatchGlobalDataShareAccess(IR::Block& block, IR::Inst& inst, Info& info, inst.SetArg(1, ir.Imm32(binding)); } else { // Convert shared memory opcode to storage buffer atomic to GDS buffer. + auto& buffer = info.buffers[binding]; const IR::U32 offset = IR::U32{inst.Arg(0)}; const IR::U32 address_words = ir.ShiftRightLogical(offset, ir.Imm32(1)); const IR::U32 address_dwords = ir.ShiftRightLogical(offset, ir.Imm32(2)); @@ -705,27 +706,35 @@ void PatchGlobalDataShareAccess(IR::Block& block, IR::Inst& inst, Info& info, case IR::Opcode::SharedAtomicXor32: inst.ReplaceUsesWith(ir.BufferAtomicXor(handle, address_dwords, inst.Arg(1), {})); break; - case IR::Opcode::LoadSharedU16: + case IR::Opcode::LoadSharedU16: { inst.ReplaceUsesWith(ir.LoadBufferU16(handle, address_words, {})); + buffer.used_types |= IR::Type::U16; break; + } case IR::Opcode::LoadSharedU32: inst.ReplaceUsesWith(ir.LoadBufferU32(1, handle, address_dwords, {})); break; - case IR::Opcode::LoadSharedU64: + case IR::Opcode::LoadSharedU64: { inst.ReplaceUsesWith(ir.LoadBufferU64(handle, address_qwords, {})); + buffer.used_types |= IR::Type::U64; break; - case IR::Opcode::WriteSharedU16: + } + case IR::Opcode::WriteSharedU16: { ir.StoreBufferU16(handle, address_words, IR::U16{inst.Arg(1)}, {}); inst.Invalidate(); + buffer.used_types |= IR::Type::U16; break; + } case IR::Opcode::WriteSharedU32: ir.StoreBufferU32(1, handle, address_dwords, inst.Arg(1), {}); inst.Invalidate(); break; - case IR::Opcode::WriteSharedU64: + case IR::Opcode::WriteSharedU64: { ir.StoreBufferU64(handle, address_qwords, IR::U64{inst.Arg(1)}, {}); inst.Invalidate(); + buffer.used_types |= IR::Type::U64; break; + } default: UNREACHABLE(); } diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 32ea1e8ed..b2a4d7a61 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -1057,9 +1057,13 @@ Liverpool::Task Liverpool::ProcessCompute(std::span acb, u32 vqid) { } case PM4ItOpcode::ReleaseMem: { const auto* release_mem = reinterpret_cast(header); - release_mem->SignalFence([pipe_id = queue.pipe_id] { - Platform::IrqC::Instance()->Signal(static_cast(pipe_id)); - }); + release_mem->SignalFence( + [pipe_id = queue.pipe_id] { + Platform::IrqC::Instance()->Signal(static_cast(pipe_id)); + }, + [this](VAddr dst, u16 gds_index, u16 num_dwords) { + rasterizer->CopyBuffer(dst, gds_index, num_dwords * sizeof(u32), false, true); + }); break; } case PM4ItOpcode::EventWrite: { diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index 46ecb09d6..17511d0a2 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -327,6 +327,7 @@ enum class DataSelect : u32 { Data64 = 2, GpuClock64 = 3, PerfCounter = 4, + GdsMemStore = 5, }; enum class InterruptSelect : u32 { @@ -920,8 +921,9 @@ struct PM4CmdReleaseMem { u32 data_hi; template - T* Address() const { - return reinterpret_cast(address_lo | u64(address_hi) << 32); + T Address() const { + u64 full_address = address_lo | (u64(address_hi) << 32); + return std::bit_cast(full_address); } u32 DataDWord() const { @@ -932,22 +934,26 @@ struct PM4CmdReleaseMem { return data_lo | u64(data_hi) << 32; } - void SignalFence(auto&& signal_irq) const { + void SignalFence(auto&& signal_irq, auto&& gds_to_mem) const { switch (data_sel.Value()) { case DataSelect::Data32Low: { - *Address() = DataDWord(); + *Address() = DataDWord(); break; } case DataSelect::Data64: { - *Address() = DataQWord(); + *Address() = DataQWord(); break; } case DataSelect::GpuClock64: { - *Address() = GetGpuClock64(); + *Address() = GetGpuClock64(); break; } case DataSelect::PerfCounter: { - *Address() = GetGpuPerfCounter(); + *Address() = GetGpuPerfCounter(); + break; + } + case DataSelect::GdsMemStore: { + gds_to_mem(Address(), gds_index, num_dw); break; } default: { From 46a7c4e1f55b819c4ddec2a853f3f89a83198d2f Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:17:57 -0600 Subject: [PATCH 49/83] Core: Miscellaneous memory fixes and slight optimizations (#3946) * Optimizations Microsoft allows you to coalesce multiple free placeholders in one VirtualFreeEx call, so we can perform the VirtualFreeEx after coalescing with neighboring regions to eliminate a VirtualFreeEx call in some situations. * Remove unnecessary VirtualProtect call As far as I can tell, this call wastes a bunch of time, and is completely unnecessary. With our current codebase, simply supplying prot to MapViewOfFile3 works properly. * Properly handle file mmaps with offsets Pretty easy fix to perform while I'm here, so I might as well include it. * Oops Leftover stuff from local things + clang * Disable tracy memory tracking Tracy's memory tracking is built around a typical malloc/free API, so each individual alloc must correspond to a free. Moving these to address space would fix issues on Windows, but Linux/Mac would have the same issues with our current code. Disabling VMA merging is technically a fix, but since that's hardware-accurate behavior, I'd rather not disable it. I'm sure there's a simple solution I'm missing, but unless other devs have a better idea of how this should be handled, the best I can do is disable it so we can keep using Tracy to trace performance. * Update address_space.cpp * Debug logging Should give a decent idea of how nasty these AddressSpace calls are in games that lost perf. * test removing thread safety Just for testing, will revert afterwards. * Check name before merging Fixes a regression in Apex Legends * Revert "test removing thread safety" This reverts commit ab897f4b1ce7e56e8600ed72c9de5f2762e8693b. * Move mutex locks before IsValidMapping calls These aren't thread safe, this fixes a rare race condition that I ran into with Apex Legends. * Revert "Debug logging" This reverts commit eb2b12a46c6d8d49d7fd93284c6975651caaa34a. * Proper VMA splitting in ProtectBytes, SetDirectMemoryType, and NameVirtualRange Also slight optimization by eliminating AddressSpace protect calls when requested prot matches the previous prot. Fixes a regression in God of War: Ragnarok * Clang * Fixes to SetDirectMemoryType logic Fixes some regressions in Marvel's Spider-Man that occurred with my previous commits to this PR. * Fix Genshin Impact again * Assert on out-of-bounds protect calls Our page tracking code is prone to causing this. * test mutex again This time, remember all mutex stuff * Revert hack I'll work on a better way to deal with mutexes in a bit, first I'm pushing up some extra fixes * Proper logic for checked ReleaseDirectMemory, added bounds checks Should help some games. * Better logging for ReleaseDirectMemory errors. * Only perform region coalescing after all unmap operations. A small optimization for unmapping multiple regions. Since Microsoft lets you coalesce multiple placeholders at once, we can save doing any VirtualFreeEx calls for coalescing until after we unmap everything in the requested range. * Separate VMA creation logic into a separate method, update MapFile to use it MapFile is technically another "emulation" of MapMemory, both should follow similar logic. To avoid duplicating code, move shared logic to a different function that both MapMemory and MapFile can call. This fixes memory asserts in a couple of online-only apps I have. * Clang * Fix TryWriteBacking This fixes a lot of regressions that got misattributed Co-Authored-By: TheTurtle <47210458+raphaelthegreat@users.noreply.github.com> * Fix again Fixes device lost crashes with some games after my last commit. * Oops * Mutex cleanup Avoided changing anything in MapMemory, UnmapMemory, PoolCommit, or PoolDecommit since those all need a little extra granularity to prevent GPU deadlocking. Everything else now uses standard library locks to make things a little simpler. * Swap MapMemory and PoolCommit to use scoped lock GPU maps are safe, so this is fine. Unmaps are the primary issue. --------- Co-authored-by: TheTurtle <47210458+raphaelthegreat@users.noreply.github.com> --- src/core/address_space.cpp | 91 +++--- src/core/libraries/kernel/memory.cpp | 19 +- src/core/memory.cpp | 420 ++++++++++++++------------- src/core/memory.h | 14 +- 4 files changed, 296 insertions(+), 248 deletions(-) diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index 422c67e17..965dfdc31 100644 --- a/src/core/address_space.cpp +++ b/src/core/address_space.cpp @@ -237,23 +237,26 @@ struct AddressSpace::Impl { void* ptr = nullptr; if (phys_addr != -1) { HANDLE backing = fd != -1 ? reinterpret_cast(fd) : backing_handle; - if (fd && prot == PAGE_READONLY) { + if (fd != -1 && prot == PAGE_READONLY) { DWORD resultvar; ptr = VirtualAlloc2(process, reinterpret_cast(virtual_addr), size, MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0); - bool ret = ReadFile(backing, ptr, size, &resultvar, NULL); + + // phys_addr serves as an offset for file mmaps. + // Create an OVERLAPPED with the offset, then supply that to ReadFile + OVERLAPPED param{}; + // Offset is the least-significant 32 bits, OffsetHigh is the most-significant. + param.Offset = phys_addr & 0xffffffffull; + param.OffsetHigh = (phys_addr & 0xffffffff00000000ull) >> 32; + bool ret = ReadFile(backing, ptr, size, &resultvar, ¶m); ASSERT_MSG(ret, "ReadFile failed. {}", Common::GetLastErrorMsg()); ret = VirtualProtect(ptr, size, prot, &resultvar); ASSERT_MSG(ret, "VirtualProtect failed. {}", Common::GetLastErrorMsg()); } else { ptr = MapViewOfFile3(backing, process, reinterpret_cast(virtual_addr), - phys_addr, size, MEM_REPLACE_PLACEHOLDER, - PAGE_EXECUTE_READWRITE, nullptr, 0); + phys_addr, size, MEM_REPLACE_PLACEHOLDER, prot, nullptr, 0); ASSERT_MSG(ptr, "MapViewOfFile3 failed. {}", Common::GetLastErrorMsg()); - DWORD resultvar; - bool ret = VirtualProtect(ptr, size, prot, &resultvar); - ASSERT_MSG(ret, "VirtualProtect failed. {}", Common::GetLastErrorMsg()); } } else { ptr = @@ -268,9 +271,11 @@ struct AddressSpace::Impl { VAddr virtual_addr = region->base; PAddr phys_base = region->phys_base; u64 size = region->size; + ULONG prot = region->prot; + s32 fd = region->fd; bool ret = false; - if (phys_base != -1) { + if ((fd != -1 && prot != PAGE_READONLY) || (fd == -1 && phys_base != -1)) { ret = UnmapViewOfFile2(process, reinterpret_cast(virtual_addr), MEM_PRESERVE_PLACEHOLDER); } else { @@ -368,13 +373,17 @@ struct AddressSpace::Impl { } void* Map(VAddr virtual_addr, PAddr phys_addr, u64 size, ULONG prot, s32 fd = -1) { - // Split surrounding regions to create a placeholder - SplitRegion(virtual_addr, size); - - // Get the region this range covers + // Get a pointer to the region containing virtual_addr auto it = std::prev(regions.upper_bound(virtual_addr)); - auto& [base, region] = *it; + // If needed, split surrounding regions to create a placeholder + if (it->first != virtual_addr || it->second.size != size) { + SplitRegion(virtual_addr, size); + it = std::prev(regions.upper_bound(virtual_addr)); + } + + // Get the address and region for this range. + auto& [base, region] = *it; ASSERT_MSG(!region.is_mapped, "Cannot overwrite mapped region"); // Now we have a region matching the requested region, perform the actual mapping. @@ -390,31 +399,42 @@ struct AddressSpace::Impl { auto it = std::prev(regions.upper_bound(virtual_addr)); ASSERT_MSG(!it->second.is_mapped, "Cannot coalesce mapped regions"); - // Check if a placeholder exists right before us. + // Check if there are free placeholders before this area. + bool can_coalesce = false; auto it_prev = it != regions.begin() ? std::prev(it) : regions.end(); - if (it_prev != regions.end() && !it_prev->second.is_mapped) { - const u64 total_size = it_prev->second.size + it->second.size; - if (!VirtualFreeEx(process, LPVOID(it_prev->first), total_size, - MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) { - UNREACHABLE_MSG("Region coalescing failed: {}", Common::GetLastErrorMsg()); - } - - it_prev->second.size = total_size; + while (it_prev != regions.end() && !it_prev->second.is_mapped) { + // If there is an earlier region, move our iterator to that and increase size. + it_prev->second.size = it_prev->second.size + it->second.size; regions.erase(it); it = it_prev; + + // Mark this region as coalesce-able. + can_coalesce = true; + + // Get the next previous region. + it_prev = it != regions.begin() ? std::prev(it) : regions.end(); } - // Check if a placeholder exists right after us. + // Check if there are free placeholders after this area. auto it_next = std::next(it); - if (it_next != regions.end() && !it_next->second.is_mapped) { - const u64 total_size = it->second.size + it_next->second.size; - if (!VirtualFreeEx(process, LPVOID(it->first), total_size, + while (it_next != regions.end() && !it_next->second.is_mapped) { + // If there is a later region, increase our current region's size + it->second.size = it->second.size + it_next->second.size; + regions.erase(it_next); + + // Mark this region as coalesce-able. + can_coalesce = true; + + // Get the next region + it_next = std::next(it); + } + + // If there are placeholders to coalesce, then coalesce them. + if (can_coalesce) { + if (!VirtualFreeEx(process, LPVOID(it->first), it->second.size, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) { UNREACHABLE_MSG("Region coalescing failed: {}", Common::GetLastErrorMsg()); } - - it->second.size = total_size; - regions.erase(it_next); } } @@ -423,7 +443,7 @@ struct AddressSpace::Impl { u64 remaining_size = size; VAddr current_addr = virtual_addr; while (remaining_size > 0) { - // Get the region containing our current address. + // Get a pointer to the region containing virtual_addr auto it = std::prev(regions.upper_bound(current_addr)); // If necessary, split regions to ensure a valid unmap. @@ -432,10 +452,10 @@ struct AddressSpace::Impl { u64 size_to_unmap = std::min(it->second.size - base_offset, remaining_size); if (current_addr != it->second.base || size_to_unmap != it->second.size) { SplitRegion(current_addr, size_to_unmap); + it = std::prev(regions.upper_bound(current_addr)); } - // Repair the region pointer, as SplitRegion modifies the regions map. - it = std::prev(regions.upper_bound(current_addr)); + // Get the address and region corresponding to this range. auto& [base, region] = *it; // Unmap the region if it was previously mapped @@ -449,13 +469,13 @@ struct AddressSpace::Impl { region.phys_base = -1; region.prot = PAGE_NOACCESS; - // Coalesce any free space - CoalesceFreeRegions(current_addr); - // Update loop variables remaining_size -= size_to_unmap; current_addr += size_to_unmap; } + + // Coalesce any free space produced from these unmaps. + CoalesceFreeRegions(virtual_addr); } void Protect(VAddr virtual_addr, u64 size, bool read, bool write, bool execute) { @@ -497,6 +517,7 @@ struct AddressSpace::Impl { const VAddr virtual_end = virtual_addr + size; auto it = --regions.upper_bound(virtual_addr); + ASSERT_MSG(it != regions.end(), "addr {:#x} out of bounds", virtual_addr); for (; it->first < virtual_end; it++) { if (!it->second.is_mapped) { continue; diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 3aec8193a..378064e44 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -89,22 +89,31 @@ s32 PS4_SYSV_ABI sceKernelAllocateMainDirectMemory(u64 len, u64 alignment, s32 m } s32 PS4_SYSV_ABI sceKernelCheckedReleaseDirectMemory(u64 start, u64 len) { + LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len); + if (!Common::Is16KBAligned(start) || !Common::Is16KBAligned(len)) { + LOG_ERROR(Kernel_Vmm, "Misaligned start or length, start = {:#x}, length = {:#x}", start, + len); + return ORBIS_KERNEL_ERROR_EINVAL; + } if (len == 0) { return ORBIS_OK; } - LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len); auto* memory = Core::Memory::Instance(); - memory->Free(start, len); - return ORBIS_OK; + return memory->Free(start, len, true); } s32 PS4_SYSV_ABI sceKernelReleaseDirectMemory(u64 start, u64 len) { + LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len); + if (!Common::Is16KBAligned(start) || !Common::Is16KBAligned(len)) { + LOG_ERROR(Kernel_Vmm, "Misaligned start or length, start = {:#x}, length = {:#x}", start, + len); + return ORBIS_KERNEL_ERROR_EINVAL; + } if (len == 0) { return ORBIS_OK; } - LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len); auto* memory = Core::Memory::Instance(); - memory->Free(start, len); + memory->Free(start, len, false); return ORBIS_OK; } diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 4567475cd..0726e8711 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -117,9 +117,9 @@ 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); - mutex.lock_shared(); auto vma = FindVMA(virtual_addr); while (size) { @@ -134,46 +134,49 @@ void MemoryManager::CopySparseMemory(VAddr virtual_addr, u8* dest, u64 size) { dest += copy_size; ++vma; } - - mutex.unlock_shared(); } 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); - mutex.lock_shared(); std::vector vmas_to_write; auto current_vma = FindVMA(virtual_addr); - while (virtual_addr + size < current_vma->second.base + current_vma->second.size) { + while (current_vma->second.Overlaps(virtual_addr, size)) { if (!HasPhysicalBacking(current_vma->second)) { - mutex.unlock_shared(); - return false; + break; } vmas_to_write.emplace_back(current_vma->second); current_vma++; } + if (vmas_to_write.empty()) { + return false; + } + for (auto& vma : vmas_to_write) { auto start_in_vma = std::max(virtual_addr, vma.base) - vma.base; - for (auto& phys_area : vma.phys_areas) { + auto phys_handle = std::prev(vma.phys_areas.upper_bound(start_in_vma)); + for (; phys_handle != vma.phys_areas.end(); phys_handle++) { if (!size) { break; } - u8* backing = impl.BackingBase() + phys_area.second.base + start_in_vma; - u64 copy_size = std::min(size, phys_area.second.size); + const u64 start_in_dma = + std::max(start_in_vma, phys_handle->first) - phys_handle->first; + u8* backing = impl.BackingBase() + phys_handle->second.base + start_in_dma; + u64 copy_size = std::min(size, phys_handle->second.size - start_in_dma); memcpy(backing, data, copy_size); size -= copy_size; } } - mutex.unlock_shared(); return true; } PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, u64 size, u64 alignment) { - mutex.lock(); + std::scoped_lock lk{mutex}; alignment = alignment > 0 ? alignment : 64_KB; auto dmem_area = FindDmemArea(search_start); @@ -199,7 +202,6 @@ PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, u64 size, if (dmem_area == dmem_map.end()) { // There are no suitable mappings in this range LOG_ERROR(Kernel_Vmm, "Unable to find free direct memory area: size = {:#x}", size); - mutex.unlock(); return -1; } @@ -211,13 +213,12 @@ PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, u64 size, // Track how much dmem was allocated for pools. pool_budget += size; - mutex.unlock(); return mapping_start; } PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, u64 size, u64 alignment, s32 memory_type) { - mutex.lock(); + std::scoped_lock lk{mutex}; alignment = alignment > 0 ? alignment : 16_KB; auto dmem_area = FindDmemArea(search_start); @@ -242,7 +243,6 @@ PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, u64 size, u6 if (dmem_area == dmem_map.end()) { // There are no suitable mappings in this range LOG_ERROR(Kernel_Vmm, "Unable to find free direct memory area: size = {:#x}", size); - mutex.unlock(); return -1; } @@ -252,12 +252,52 @@ PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, u64 size, u6 area.dma_type = PhysicalMemoryType::Allocated; MergeAdjacent(dmem_map, dmem_area); - mutex.unlock(); return mapping_start; } -void MemoryManager::Free(PAddr phys_addr, u64 size) { - mutex.lock(); +s32 MemoryManager::Free(PAddr phys_addr, u64 size, bool is_checked) { + // Basic bounds checking + if (phys_addr > total_direct_size || (is_checked && phys_addr + size > total_direct_size)) { + LOG_ERROR(Kernel_Vmm, "phys_addr {:#x}, size {:#x} goes outside dmem map", phys_addr, size); + if (is_checked) { + return ORBIS_KERNEL_ERROR_ENOENT; + } + return ORBIS_OK; + } + + // Lock mutex + std::scoped_lock lk{mutex}; + + // If this is a checked free, then all direct memory in range must be allocated. + std::vector> free_list; + u64 remaining_size = size; + auto phys_handle = FindDmemArea(phys_addr); + for (; phys_handle != dmem_map.end(); phys_handle++) { + if (remaining_size == 0) { + // Done searching + break; + } + auto& dmem_area = phys_handle->second; + if (dmem_area.dma_type == PhysicalMemoryType::Free) { + if (is_checked) { + // Checked frees will error if anything in the area isn't allocated. + // Unchecked frees will just ignore free areas. + LOG_ERROR(Kernel_Vmm, "Attempting to release a free dmem area"); + return ORBIS_KERNEL_ERROR_ENOENT; + } + continue; + } + + // Store physical address and size to release + const PAddr current_phys_addr = std::max(phys_addr, phys_handle->first); + const u64 start_in_dma = current_phys_addr - phys_handle->first; + const u64 size_in_dma = + std::min(remaining_size, phys_handle->second.size - start_in_dma); + free_list.emplace_back(current_phys_addr, size_in_dma); + + // Track remaining size to free + remaining_size -= size_in_dma; + } // Release any dmem mappings that reference this physical block. std::vector> remove_list; @@ -284,36 +324,24 @@ void MemoryManager::Free(PAddr phys_addr, u64 size) { } // Unmap all dmem areas within this area. - auto phys_addr_to_search = phys_addr; - auto remaining_size = size; - auto dmem_area = FindDmemArea(phys_addr); - while (dmem_area != dmem_map.end() && remaining_size > 0) { + for (auto& [phys_addr, size] : free_list) { // Carve a free dmem area in place of this one. - const auto start_phys_addr = std::max(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); - const auto dmem_handle = CarvePhysArea(dmem_map, start_phys_addr, size_in_dma); + const auto dmem_handle = CarvePhysArea(dmem_map, phys_addr, size); auto& new_dmem_area = dmem_handle->second; new_dmem_area.dma_type = PhysicalMemoryType::Free; new_dmem_area.memory_type = 0; // 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; - remaining_size -= size_in_dma; - dmem_area = FindDmemArea(phys_addr_to_search); } - mutex.unlock(); + return ORBIS_OK; } s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 mtype) { + std::scoped_lock lk{mutex}; ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); - mutex.lock(); // Input addresses to PoolCommit are treated as fixed, and have a constant alignment. const u64 alignment = 64_KB; @@ -323,7 +351,6 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 if (vma.type != VMAType::PoolReserved) { // If we're attempting to commit non-pooled memory, return EINVAL LOG_ERROR(Kernel_Vmm, "Attempting to commit non-pooled memory at {:#x}", mapped_addr); - mutex.unlock(); return ORBIS_KERNEL_ERROR_EINVAL; } @@ -332,14 +359,12 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 LOG_ERROR(Kernel_Vmm, "Pooled region {:#x} to {:#x} is not large enough to commit from {:#x} to {:#x}", vma.base, vma.base + vma.size, mapped_addr, mapped_addr + size); - mutex.unlock(); return ORBIS_KERNEL_ERROR_EINVAL; } if (pool_budget <= size) { // If there isn't enough pooled memory to perform the mapping, return ENOMEM LOG_ERROR(Kernel_Vmm, "Not enough pooled memory to perform mapping"); - mutex.unlock(); return ORBIS_KERNEL_ERROR_ENOMEM; } else { // Track how much pooled memory this commit will take @@ -386,7 +411,8 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 // Perform an address space mapping for each physical area void* out_addr = impl.Map(current_addr, size_to_map, new_dmem_area.base); - TRACK_ALLOC(out_addr, size_to_map, "VMEM"); + // Tracy memory tracking breaks from merging memory areas. Disabled for now. + // TRACK_ALLOC(out_addr, size_to_map, "VMEM"); handle = MergeAdjacent(dmem_map, new_dmem_handle); current_addr += size_to_map; @@ -398,7 +424,6 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 // Merge this VMA with similar nearby areas MergeAdjacent(vma_map, new_vma_handle); - mutex.unlock(); if (IsValidGpuMapping(mapped_addr, size)) { rasterizer->MapMemory(mapped_addr, size); } @@ -406,54 +431,9 @@ s32 MemoryManager::PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 return ORBIS_OK; } -s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, MemoryProt prot, - MemoryMapFlags flags, VMAType type, std::string_view name, - bool validate_dmem, PAddr phys_addr, u64 alignment) { - // Certain games perform flexible mappings on loop to determine - // the available flexible memory size. Questionable but we need to handle this. - if (type == VMAType::Flexible && flexible_usage + size > total_flexible_size) { - LOG_ERROR(Kernel_Vmm, - "Out of flexible memory, available flexible memory = {:#x}" - " requested size = {:#x}", - total_flexible_size - flexible_usage, size); - return ORBIS_KERNEL_ERROR_EINVAL; - } - - mutex.lock(); - - PhysHandle dmem_area; - // Validate the requested physical address range - if (phys_addr != -1) { - if (total_direct_size < phys_addr + size) { - LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at physical address {:#x}", size, - phys_addr); - mutex.unlock(); - return ORBIS_KERNEL_ERROR_ENOMEM; - } - - // Validate direct memory areas involved in this call. - auto dmem_area = FindDmemArea(phys_addr); - while (dmem_area != dmem_map.end() && dmem_area->second.base < phys_addr + size) { - // If any requested dmem area is not allocated, return an error. - if (dmem_area->second.dma_type != PhysicalMemoryType::Allocated && - dmem_area->second.dma_type != PhysicalMemoryType::Mapped) { - LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at physical address {:#x}", size, - phys_addr); - mutex.unlock(); - return ORBIS_KERNEL_ERROR_ENOMEM; - } - - // If we need to perform extra validation, then check for Mapped dmem areas too. - if (validate_dmem && dmem_area->second.dma_type == PhysicalMemoryType::Mapped) { - LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at physical address {:#x}", size, - phys_addr); - mutex.unlock(); - return ORBIS_KERNEL_ERROR_EBUSY; - } - - dmem_area++; - } - } +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; @@ -483,8 +463,7 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo 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); - mutex.unlock(); - return ORBIS_KERNEL_ERROR_ENOMEM; + return {ORBIS_KERNEL_ERROR_ENOMEM, vma_map.end()}; } } else { // When MemoryMapFlags::Fixed is not specified, and mapped_addr is 0, @@ -494,8 +473,7 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo mapped_addr = SearchFree(mapped_addr, size, alignment); if (mapped_addr == -1) { // No suitable memory areas to map to - mutex.unlock(); - return ORBIS_KERNEL_ERROR_ENOMEM; + return {ORBIS_KERNEL_ERROR_ENOMEM, vma_map.end()}; } } @@ -513,6 +491,64 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo new_vma.name = name; new_vma.type = type; new_vma.phys_areas.clear(); + return {ORBIS_OK, new_vma_handle}; +} + +s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, MemoryProt prot, + MemoryMapFlags flags, VMAType type, std::string_view name, + bool validate_dmem, PAddr phys_addr, u64 alignment) { + // Certain games perform flexible mappings on loop to determine + // the available flexible memory size. Questionable but we need to handle this. + if (type == VMAType::Flexible && flexible_usage + size > total_flexible_size) { + LOG_ERROR(Kernel_Vmm, + "Out of flexible memory, available flexible memory = {:#x}" + " requested size = {:#x}", + total_flexible_size - flexible_usage, size); + return ORBIS_KERNEL_ERROR_EINVAL; + } + + std::scoped_lock lk{mutex}; + + PhysHandle dmem_area; + // Validate the requested physical address range + if (phys_addr != -1) { + if (total_direct_size < phys_addr + size) { + LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at physical address {:#x}", size, + phys_addr); + return ORBIS_KERNEL_ERROR_ENOMEM; + } + + // Validate direct memory areas involved in this call. + auto dmem_area = FindDmemArea(phys_addr); + while (dmem_area != dmem_map.end() && dmem_area->second.base < phys_addr + size) { + // If any requested dmem area is not allocated, return an error. + if (dmem_area->second.dma_type != PhysicalMemoryType::Allocated && + dmem_area->second.dma_type != PhysicalMemoryType::Mapped) { + LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at physical address {:#x}", size, + phys_addr); + return ORBIS_KERNEL_ERROR_ENOMEM; + } + + // If we need to perform extra validation, then check for Mapped dmem areas too. + if (validate_dmem && dmem_area->second.dma_type == PhysicalMemoryType::Mapped) { + LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at physical address {:#x}", size, + phys_addr); + return ORBIS_KERNEL_ERROR_EBUSY; + } + + dmem_area++; + } + } + + auto [result, new_vma_handle] = + CreateArea(virtual_addr, size, prot, flags, type, name, alignment); + if (result != ORBIS_OK) { + return result; + } + + auto& new_vma = new_vma_handle->second; + auto mapped_addr = new_vma.base; + bool is_exec = True(prot & MemoryProt::CpuExec); // If type is Flexible, we need to track how much flexible memory is used here. // We also need to determine a reasonable physical base to perform this mapping at. @@ -542,7 +578,8 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo // Perform an address space mapping for each physical area void* out_addr = impl.Map(current_addr, size_to_map, new_fmem_area.base, is_exec); - TRACK_ALLOC(out_addr, size_to_map, "VMEM"); + // Tracy memory tracking breaks from merging memory areas. Disabled for now. + // TRACK_ALLOC(out_addr, size_to_map, "VMEM"); handle = MergeAdjacent(fmem_map, new_fmem_handle); current_addr += size_to_map; @@ -594,60 +631,32 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo // Flexible address space mappings were performed while finding direct memory areas. if (type != VMAType::Flexible) { impl.Map(mapped_addr, size, phys_addr, is_exec); + // Tracy memory tracking breaks from merging memory areas. Disabled for now. + // TRACK_ALLOC(mapped_addr, size, "VMEM"); } - TRACK_ALLOC(*out_addr, size, "VMEM"); - - mutex.unlock(); // If this is not a reservation, then map to GPU and address space if (IsValidGpuMapping(mapped_addr, size)) { rasterizer->MapMemory(mapped_addr, size); } - } else { - mutex.unlock(); } - return ORBIS_OK; } s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, MemoryProt prot, MemoryMapFlags flags, s32 fd, s64 phys_addr) { - VAddr mapped_addr = (virtual_addr == 0) ? impl.SystemManagedVirtualBase() : virtual_addr; - ASSERT_MSG(IsValidMapping(mapped_addr, size), "Attempted to access invalid address {:#x}", - mapped_addr); - - mutex.lock(); - - // Find first free area to map the file. - if (False(flags & MemoryMapFlags::Fixed)) { - mapped_addr = SearchFree(mapped_addr, size, 1); - if (mapped_addr == -1) { - // No suitable memory areas to map to - mutex.unlock(); - return ORBIS_KERNEL_ERROR_ENOMEM; - } - } - - if (True(flags & MemoryMapFlags::Fixed)) { - const auto& vma = FindVMA(mapped_addr)->second; - const u64 remaining_size = vma.base + vma.size - virtual_addr; - ASSERT_MSG(!vma.IsMapped() && remaining_size >= size, - "Memory region {:#x} to {:#x} isn't free enough to map region {:#x} to {:#x}", - vma.base, vma.base + vma.size, virtual_addr, virtual_addr + size); - } - + std::scoped_lock lk{mutex}; // Get the file to map + auto* h = Common::Singleton::Instance(); auto file = h->GetFile(fd); if (file == nullptr) { LOG_WARNING(Kernel_Vmm, "Invalid file for mmap, fd {}", fd); - mutex.unlock(); return ORBIS_KERNEL_ERROR_EBADF; } if (file->type != Core::FileSys::FileType::Regular) { LOG_WARNING(Kernel_Vmm, "Unsupported file type for mmap, fd {}", fd); - mutex.unlock(); return ORBIS_KERNEL_ERROR_EBADF; } @@ -665,35 +674,36 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory prot &= ~MemoryProt::CpuWrite; } - impl.MapFile(mapped_addr, size, phys_addr, std::bit_cast(prot), handle); - if (prot >= MemoryProt::GpuRead) { // On real hardware, GPU file mmaps cause a full system crash due to an internal error. ASSERT_MSG(false, "Files cannot be mapped to GPU memory"); } + if (True(prot & MemoryProt::CpuExec)) { // On real hardware, execute permissions are silently removed. prot &= ~MemoryProt::CpuExec; } - // Add virtual memory area - auto& new_vma = CarveVMA(mapped_addr, size)->second; - new_vma.disallow_merge = True(flags & MemoryMapFlags::NoCoalesce); - new_vma.prot = prot; - new_vma.name = "File"; - new_vma.fd = fd; - new_vma.type = VMAType::File; + auto [result, new_vma_handle] = + CreateArea(virtual_addr, size, prot, flags, VMAType::File, "anon", 0); + if (result != ORBIS_OK) { + return result; + } - mutex.unlock(); + auto& new_vma = new_vma_handle->second; + auto mapped_addr = new_vma.base; + bool is_exec = True(prot & MemoryProt::CpuExec); + + impl.MapFile(mapped_addr, size, phys_addr, std::bit_cast(prot), handle); *out_addr = std::bit_cast(mapped_addr); return ORBIS_OK; } s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { + mutex.lock(); ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); - mutex.lock(); // Do an initial search to ensure this decommit is valid. auto it = FindVMA(virtual_addr); @@ -768,7 +778,8 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { // Unmap from address space impl.Unmap(virtual_addr, size, true); - TRACK_FREE(virtual_addr, "VMEM"); + // Tracy memory tracking breaks from merging memory areas. Disabled for now. + // TRACK_FREE(virtual_addr, "VMEM"); mutex.unlock(); return ORBIS_OK; @@ -857,7 +868,8 @@ 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); - TRACK_FREE(virtual_addr, "VMEM"); + // 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)) { @@ -884,14 +896,13 @@ s32 MemoryManager::UnmapMemoryImpl(VAddr virtual_addr, u64 size) { } s32 MemoryManager::QueryProtection(VAddr addr, void** start, void** end, u32* prot) { + std::shared_lock lk{mutex}; ASSERT_MSG(IsValidMapping(addr), "Attempted to access invalid address {:#x}", addr); - mutex.lock_shared(); const auto it = FindVMA(addr); const auto& vma = it->second; if (vma.IsFree()) { LOG_ERROR(Kernel_Vmm, "Address {:#x} is not mapped", addr); - mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_EACCES; } @@ -905,7 +916,6 @@ s32 MemoryManager::QueryProtection(VAddr addr, void** start, void** end, u32* pr *prot = static_cast(vma.prot); } - mutex.unlock_shared(); return ORBIS_OK; } @@ -913,6 +923,8 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea& vma_base, u64 siz MemoryProt prot) { const auto start_in_vma = addr - vma_base.base; const auto adjusted_size = std::min(vma_base.size - start_in_vma, size); + const MemoryProt old_prot = vma_base.prot; + const MemoryProt new_prot = prot; if (vma_base.type == VMAType::Free || vma_base.type == VMAType::PoolReserved) { // On PS4, protecting freed memory does nothing. @@ -953,8 +965,11 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea& vma_base, u64 siz prot &= ~MemoryProt::CpuExec; } - // Change protection - vma_base.prot = prot; + // Split VMAs and apply protection change. + const auto new_it = CarveVMA(addr, adjusted_size); + auto& new_vma = new_it->second; + new_vma.prot = prot; + MergeAdjacent(vma_map, new_it); if (vma_base.type == VMAType::Reserved) { // On PS4, protections change vma_map, but don't apply. @@ -962,7 +977,10 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea& vma_base, u64 siz return adjusted_size; } - impl.Protect(addr, size, perms); + // Perform address-space memory protections if needed. + if (new_prot != old_prot) { + impl.Protect(addr, adjusted_size, perms); + } return adjusted_size; } @@ -974,6 +992,7 @@ s32 MemoryManager::Protect(VAddr addr, u64 size, MemoryProt prot) { } // Ensure the range to modify is valid + std::scoped_lock lk{mutex}; ASSERT_MSG(IsValidMapping(addr, size), "Attempted to access invalid address {:#x}", addr); // Appropriately restrict flags. @@ -981,7 +1000,6 @@ s32 MemoryManager::Protect(VAddr addr, u64 size, MemoryProt prot) { MemoryProt::CpuReadWrite | MemoryProt::CpuExec | MemoryProt::GpuReadWrite; MemoryProt valid_flags = prot & flag_mask; - mutex.lock(); // Protect all VMAs between addr and addr + size. s64 protected_bytes = 0; while (protected_bytes < size) { @@ -994,13 +1012,11 @@ s32 MemoryManager::Protect(VAddr addr, u64 size, MemoryProt prot) { auto result = ProtectBytes(addr + protected_bytes, vma_base, size - protected_bytes, prot); if (result < 0) { // ProtectBytes returned an error, return it - mutex.unlock(); return result; } protected_bytes += result; } - mutex.unlock(); return ORBIS_OK; } @@ -1014,7 +1030,7 @@ s32 MemoryManager::VirtualQuery(VAddr addr, s32 flags, return ORBIS_KERNEL_ERROR_EACCES; } - mutex.lock_shared(); + std::shared_lock lk{mutex}; auto it = FindVMA(query_addr); while (it != vma_map.end() && it->second.type == VMAType::Free && flags == 1) { @@ -1022,7 +1038,6 @@ s32 MemoryManager::VirtualQuery(VAddr addr, s32 flags, } if (it == vma_map.end() || it->second.type == VMAType::Free) { LOG_WARNING(Kernel_Vmm, "VirtualQuery on free memory region"); - mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_EACCES; } @@ -1050,7 +1065,6 @@ s32 MemoryManager::VirtualQuery(VAddr addr, s32 flags, strncpy(info->name, vma.name.data(), ::Libraries::Kernel::ORBIS_KERNEL_MAXIMUM_NAME_LENGTH); - mutex.unlock_shared(); return ORBIS_OK; } @@ -1061,7 +1075,7 @@ s32 MemoryManager::DirectMemoryQuery(PAddr addr, bool find_next, return ORBIS_KERNEL_ERROR_EACCES; } - mutex.lock_shared(); + std::shared_lock lk{mutex}; auto dmem_area = FindDmemArea(addr); while (dmem_area != dmem_map.end() && dmem_area->second.dma_type == PhysicalMemoryType::Free && find_next) { @@ -1070,7 +1084,6 @@ s32 MemoryManager::DirectMemoryQuery(PAddr addr, bool find_next, if (dmem_area == dmem_map.end() || dmem_area->second.dma_type == PhysicalMemoryType::Free) { LOG_WARNING(Kernel_Vmm, "Unable to find allocated direct memory region to query!"); - mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_EACCES; } @@ -1086,13 +1099,12 @@ s32 MemoryManager::DirectMemoryQuery(PAddr addr, bool find_next, dmem_area++; } - mutex.unlock_shared(); return ORBIS_OK; } s32 MemoryManager::DirectQueryAvailable(PAddr search_start, PAddr search_end, u64 alignment, PAddr* phys_addr_out, u64* size_out) { - mutex.lock_shared(); + std::shared_lock lk{mutex}; auto dmem_area = FindDmemArea(search_start); PAddr paddr{}; @@ -1132,91 +1144,90 @@ s32 MemoryManager::DirectQueryAvailable(PAddr search_start, PAddr search_end, u6 dmem_area++; } - mutex.unlock_shared(); *phys_addr_out = paddr; *size_out = max_size; return ORBIS_OK; } s32 MemoryManager::SetDirectMemoryType(VAddr addr, u64 size, s32 memory_type) { - mutex.lock(); + std::scoped_lock lk{mutex}; ASSERT_MSG(IsValidMapping(addr, size), "Attempted to access invalid address {:#x}", addr); // Search through all VMAs covered by the provided range. // We aren't modifying these VMAs, so it's safe to iterate through them. VAddr current_addr = addr; - auto remaining_size = size; + u64 remaining_size = size; auto vma_handle = FindVMA(addr); - while (vma_handle != vma_map.end() && vma_handle->second.base < addr + size) { + while (vma_handle != vma_map.end() && remaining_size > 0) { + // Calculate position in vma + const VAddr start_in_vma = current_addr - vma_handle->second.base; + const u64 size_in_vma = + std::min(remaining_size, vma_handle->second.size - start_in_vma); + // Direct and Pooled mappings are the only ones with a memory type. if (vma_handle->second.type == VMAType::Direct || vma_handle->second.type == VMAType::Pooled) { - // Calculate position in vma - const auto start_in_vma = current_addr - vma_handle->second.base; - const auto size_in_vma = vma_handle->second.size - start_in_vma; - const auto base_phys_addr = vma_handle->second.phys_areas.begin()->second.base; - auto size_to_modify = std::min(remaining_size, size_in_vma); - for (auto& phys_handle : vma_handle->second.phys_areas) { - if (size_to_modify == 0) { - break; - } + // Split area to modify into a new VMA. + vma_handle = CarveVMA(current_addr, size_in_vma); + auto phys_handle = vma_handle->second.phys_areas.begin(); + while (phys_handle != vma_handle->second.phys_areas.end()) { + // Update internal physical areas + phys_handle->second.memory_type = memory_type; - const auto current_phys_addr = - std::max(base_phys_addr, phys_handle.second.base); - if (current_phys_addr >= phys_handle.second.base + phys_handle.second.size) { - continue; - } - const auto start_in_dma = current_phys_addr - phys_handle.second.base; - const auto size_in_dma = phys_handle.second.size - start_in_dma; - - phys_handle.second.memory_type = memory_type; - - auto dmem_handle = CarvePhysArea(dmem_map, current_phys_addr, size_in_dma); + // Carve a new dmem area in dmem_map, update memory type there + auto dmem_handle = + CarvePhysArea(dmem_map, phys_handle->second.base, phys_handle->second.size); auto& dmem_area = dmem_handle->second; dmem_area.memory_type = memory_type; - size_to_modify -= dmem_area.size; - MergeAdjacent(dmem_map, dmem_handle); + + // Increment phys_handle + phys_handle++; } + + // Check if VMA can be merged with adjacent areas after physical area modifications. + vma_handle = MergeAdjacent(vma_map, vma_handle); } - remaining_size -= vma_handle->second.size; + current_addr += size_in_vma; + remaining_size -= size_in_vma; vma_handle++; } - mutex.unlock(); return ORBIS_OK; } void MemoryManager::NameVirtualRange(VAddr virtual_addr, u64 size, std::string_view name) { - mutex.lock(); + std::scoped_lock lk{mutex}; // Sizes are aligned up to the nearest 16_KB - auto aligned_size = Common::AlignUp(size, 16_KB); + u64 aligned_size = Common::AlignUp(size, 16_KB); // Addresses are aligned down to the nearest 16_KB - auto aligned_addr = Common::AlignDown(virtual_addr, 16_KB); + VAddr aligned_addr = Common::AlignDown(virtual_addr, 16_KB); ASSERT_MSG(IsValidMapping(aligned_addr, aligned_size), "Attempted to access invalid address {:#x}", aligned_addr); auto it = FindVMA(aligned_addr); - s64 remaining_size = aligned_size; - auto current_addr = aligned_addr; - while (remaining_size > 0) { + u64 remaining_size = aligned_size; + VAddr current_addr = aligned_addr; + while (remaining_size > 0 && it != vma_map.end()) { + const u64 start_in_vma = current_addr - it->second.base; + const u64 size_in_vma = std::min(remaining_size, it->second.size - start_in_vma); // Nothing needs to be done to free VMAs if (!it->second.IsFree()) { - if (remaining_size < it->second.size) { - // We should split VMAs here, but this could cause trouble for Windows. - // Instead log a warning and name the whole VMA. - LOG_WARNING(Kernel_Vmm, "Trying to partially name a range"); + if (size_in_vma < it->second.size) { + it = CarveVMA(current_addr, size_in_vma); + auto& new_vma = it->second; + new_vma.name = name; + } else { + auto& vma = it->second; + vma.name = name; } - auto& vma = it->second; - vma.name = name; } - remaining_size -= it->second.size; - current_addr += it->second.size; - it = FindVMA(current_addr); + it = MergeAdjacent(vma_map, it); + remaining_size -= size_in_vma; + current_addr += size_in_vma; + it++; } - - mutex.unlock(); } s32 MemoryManager::GetDirectMemoryType(PAddr addr, s32* directMemoryTypeOut, @@ -1226,24 +1237,22 @@ s32 MemoryManager::GetDirectMemoryType(PAddr addr, s32* directMemoryTypeOut, return ORBIS_KERNEL_ERROR_ENOENT; } - mutex.lock_shared(); + std::shared_lock lk{mutex}; const auto& dmem_area = FindDmemArea(addr)->second; if (dmem_area.dma_type == PhysicalMemoryType::Free) { LOG_ERROR(Kernel_Vmm, "Unable to find allocated direct memory region to check type!"); - mutex.unlock_shared(); return ORBIS_KERNEL_ERROR_ENOENT; } *directMemoryStartOut = reinterpret_cast(dmem_area.base); *directMemoryEndOut = reinterpret_cast(dmem_area.GetEnd()); *directMemoryTypeOut = dmem_area.memory_type; - mutex.unlock_shared(); return ORBIS_OK; } s32 MemoryManager::IsStack(VAddr addr, void** start, void** end) { + std::shared_lock lk{mutex}; ASSERT_MSG(IsValidMapping(addr), "Attempted to access invalid address {:#x}", addr); - mutex.lock_shared(); const auto& vma = FindVMA(addr)->second; if (vma.IsFree()) { mutex.unlock_shared(); @@ -1264,13 +1273,11 @@ s32 MemoryManager::IsStack(VAddr addr, void** start, void** end) { if (end != nullptr) { *end = reinterpret_cast(stack_end); } - - mutex.unlock_shared(); return ORBIS_OK; } s32 MemoryManager::GetMemoryPoolStats(::Libraries::Kernel::OrbisKernelMemoryPoolBlockStats* stats) { - mutex.lock_shared(); + std::shared_lock lk{mutex}; // Run through dmem_map, determine how much physical memory is currently committed constexpr u64 block_size = 64_KB; @@ -1290,7 +1297,6 @@ s32 MemoryManager::GetMemoryPoolStats(::Libraries::Kernel::OrbisKernelMemoryPool stats->allocated_cached_blocks = 0; stats->available_cached_blocks = 0; - mutex.unlock_shared(); return ORBIS_OK; } diff --git a/src/core/memory.h b/src/core/memory.h index 0664ed592..92a1016bf 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -114,6 +114,10 @@ struct VirtualMemoryArea { return addr >= base && (addr + size) <= (base + this->size); } + bool Overlaps(VAddr addr, u64 size) const { + return addr <= (base + this->size) && (addr + size) >= base; + } + bool IsFree() const noexcept { return type == VMAType::Free; } @@ -140,6 +144,9 @@ struct VirtualMemoryArea { if (prot != next.prot || type != next.type) { return false; } + if (name.compare(next.name) != 0) { + return false; + } return true; } @@ -237,7 +244,7 @@ public: PAddr Allocate(PAddr search_start, PAddr search_end, u64 size, u64 alignment, s32 memory_type); - void Free(PAddr phys_addr, u64 size); + s32 Free(PAddr phys_addr, u64 size, bool is_checked); s32 PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 mtype); @@ -297,6 +304,11 @@ 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); + VAddr SearchFree(VAddr virtual_addr, u64 size, u32 alignment); VMAHandle MergeAdjacent(VMAMap& map, VMAHandle iter); From c8b45e5ebc5b257e68d2413fd59276269564714e Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:05:56 -0600 Subject: [PATCH 50/83] Core: More memory hotfixes (#3954) * Update memory.cpp * Fix CoalesceFreeRegions to account for address space gaps Fixes a regression in Saint's Row games. --- src/core/address_space.cpp | 10 ++++++---- src/core/memory.cpp | 13 ++----------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/core/address_space.cpp b/src/core/address_space.cpp index 965dfdc31..f4a6b640e 100644 --- a/src/core/address_space.cpp +++ b/src/core/address_space.cpp @@ -399,10 +399,11 @@ struct AddressSpace::Impl { auto it = std::prev(regions.upper_bound(virtual_addr)); ASSERT_MSG(!it->second.is_mapped, "Cannot coalesce mapped regions"); - // Check if there are free placeholders before this area. + // Check if there are adjacent free placeholders before this area. bool can_coalesce = false; auto it_prev = it != regions.begin() ? std::prev(it) : regions.end(); - while (it_prev != regions.end() && !it_prev->second.is_mapped) { + while (it_prev != regions.end() && !it_prev->second.is_mapped && + it_prev->first + it_prev->second.size == it->first) { // If there is an earlier region, move our iterator to that and increase size. it_prev->second.size = it_prev->second.size + it->second.size; regions.erase(it); @@ -415,9 +416,10 @@ struct AddressSpace::Impl { it_prev = it != regions.begin() ? std::prev(it) : regions.end(); } - // Check if there are free placeholders after this area. + // Check if there are adjacent free placeholders after this area. auto it_next = std::next(it); - while (it_next != regions.end() && !it_next->second.is_mapped) { + while (it_next != regions.end() && !it_next->second.is_mapped && + it->first + it->second.size == it_next->first) { // If there is a later region, increase our current region's size it->second.size = it->second.size + it_next->second.size; regions.erase(it_next); diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 0726e8711..32518907a 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -117,7 +117,6 @@ 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); @@ -138,7 +137,6 @@ 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); @@ -701,7 +699,7 @@ s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, Memory } s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { - mutex.lock(); + std::scoped_lock lk{mutex}; ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}", virtual_addr); @@ -710,7 +708,6 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { while (it != vma_map.end() && it->second.base + it->second.size <= virtual_addr + size) { if (it->second.type != VMAType::PoolReserved && it->second.type != VMAType::Pooled) { LOG_ERROR(Kernel_Vmm, "Attempting to decommit non-pooled memory!"); - mutex.unlock(); return ORBIS_KERNEL_ERROR_EINVAL; } it++; @@ -728,9 +725,7 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { if (vma_base.type == VMAType::Pooled) { // We always map PoolCommitted memory to GPU, so unmap when decomitting. if (IsValidGpuMapping(current_addr, size_in_vma)) { - mutex.unlock(); rasterizer->UnmapMemory(current_addr, size_in_vma); - mutex.lock(); } // Track how much pooled memory is decommitted @@ -781,7 +776,6 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, u64 size) { // Tracy memory tracking breaks from merging memory areas. Disabled for now. // TRACK_FREE(virtual_addr, "VMEM"); - mutex.unlock(); return ORBIS_OK; } @@ -789,13 +783,12 @@ s32 MemoryManager::UnmapMemory(VAddr virtual_addr, u64 size) { if (size == 0) { return ORBIS_OK; } - mutex.lock(); + std::scoped_lock lk{mutex}; 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); - mutex.unlock(); return bytes_unmapped; } @@ -873,9 +866,7 @@ u64 MemoryManager::UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma // If this mapping has GPU access, unmap from GPU. if (IsValidGpuMapping(virtual_addr, size)) { - mutex.unlock(); rasterizer->UnmapMemory(virtual_addr, size); - mutex.lock(); } } return size_in_vma; From fa497f6bfdce7dad711de0c46cca81222abd2ea5 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 24 Jan 2026 14:57:24 +0200 Subject: [PATCH 51/83] added new cli parser using CLI11 (#3950) * added new cli parser using CLI11 * pff * fixed repo * fix game autodetection * clear unessecary comments * added a check * fixed? * parse extras * one more try * readded -g * fixed ignore_game_patches flag * some rewrite improvements --- .gitmodules | 4 + CMakeLists.txt | 2 +- externals/CMakeLists.txt | 8 +- externals/ext-CLI11 | 1 + src/main.cpp | 378 +++++++++++++++------------------------ 5 files changed, 158 insertions(+), 235 deletions(-) create mode 160000 externals/ext-CLI11 diff --git a/.gitmodules b/.gitmodules index c0ba5e79d..82c40f4f9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -123,3 +123,7 @@ [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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 929e0ebc7..5fe8ecb10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1097,7 +1097,7 @@ create_target_directory_groups(shadps4) target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG) target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 SDL3_mixer::SDL3_mixer pugixml::pugixml) -target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz fdk-aac) +target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz fdk-aac CLI11::CLI11) target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 8e96f9bec..f20310a91 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -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 set(BUILD_SHARED_LIBS OFF) @@ -268,3 +268,9 @@ add_subdirectory(json) # miniz add_subdirectory(miniz) + +# cli11 +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 diff --git a/externals/ext-CLI11 b/externals/ext-CLI11 new file mode 160000 index 000000000..1cce14833 --- /dev/null +++ b/externals/ext-CLI11 @@ -0,0 +1 @@ +Subproject commit 1cce1483345e60997b87720948c37d6a34db2658 diff --git a/src/main.cpp b/src/main.cpp index b09ea7f4d..9b263e250 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,16 +1,17 @@ // SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include +#include +#include #include -#include "functional" -#include "iostream" -#include "string" -#include "system_error" -#include "unordered_map" #include -#include #include "common/config.h" +#include "common/key_manager.h" #include "common/logging/backend.h" #include "common/memory_patcher.h" #include "common/path_util.h" @@ -22,265 +23,176 @@ #ifdef _WIN32 #include #endif -#include int main(int argc, char* argv[]) { #ifdef _WIN32 SetConsoleOutputCP(CP_UTF8); #endif + IPC::Instance().Init(); - // Init emulator state - std::shared_ptr m_emu_state = std::make_shared(); - EmulatorState::SetInstance(m_emu_state); - // Load configurations + + auto emu_state = std::make_shared(); + EmulatorState::SetInstance(emu_state); + const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); Config::load(user_dir / "config.toml"); - // temp copy the trophy key from old config to key manager if exists + + // ---- Trophy key migration ---- auto key_manager = KeyManager::GetInstance(); - if (key_manager->GetAllKeys().TrophyKeySet.ReleaseTrophyKey.empty()) { - if (!Config::getTrophyKey().empty()) { - - key_manager->SetAllKeys( - {.TrophyKeySet = {.ReleaseTrophyKey = - KeyManager::HexStringToBytes(Config::getTrophyKey())}}); - key_manager->SaveToFile(); - } + if (key_manager->GetAllKeys().TrophyKeySet.ReleaseTrophyKey.empty() && + !Config::getTrophyKey().empty()) { + key_manager->SetAllKeys({.TrophyKeySet = {.ReleaseTrophyKey = KeyManager::HexStringToBytes( + Config::getTrophyKey())}}); + key_manager->SaveToFile(); } - bool has_game_argument = false; - std::string game_path; - std::vector game_args{}; - std::optional game_folder; - bool waitForDebugger = false; + CLI::App app{"shadPS4 Emulator CLI"}; + + // ---- CLI state ---- + std::optional gamePath; + std::vector gameArgs; + std::optional overrideRoot; std::optional waitPid; + bool waitForDebugger = false; - // Map of argument strings to lambda functions - std::unordered_map> arg_map = { - {"-h", - [&](int&) { - std::cout - << "Usage: shadps4 [options] \n" - "Options:\n" - " -g, --game Specify game path to launch\n" - " -- ... Parameters passed to the game ELF. " - "Needs to be at the end of the line, and everything after \"--\" is a " - "game argument.\n" - " -p, --patch Apply specified patch file\n" - " -i, --ignore-game-patch Disable automatic loading of game patch\n" - " -f, --fullscreen Specify window initial fullscreen " - "state. Does not overwrite the config file.\n" - " --add-game-folder Adds a new game folder to the config.\n" - " --set-addon-folder Sets the addon folder to the config.\n" - " --log-append Append log output to file instead of " - "overwriting it.\n" - " --override-root Override the game root folder. Default is the " - "parent of game path\n" - " --wait-for-debugger Wait for debugger to attach\n" - " --wait-for-pid Wait for process with specified PID to stop\n" - " --config-clean Run the emulator with the default config " - "values, ignores the config file(s) entirely.\n" - " --config-global Run the emulator with the base config file " - "only, ignores game specific configs.\n" - " --show-fps Enable FPS counter display at startup\n" - " -h, --help Display this help message\n"; - exit(0); - }}, - {"--help", [&](int& i) { arg_map["-h"](i); }}, + std::optional fullscreenStr; + bool ignoreGamePatch = false; + bool showFps = false; + bool configClean = false; + bool configGlobal = false; + bool logAppend = false; - {"-g", - [&](int& i) { - if (i + 1 < argc) { - game_path = argv[++i]; - has_game_argument = true; - } else { - std::cerr << "Error: Missing argument for -g/--game\n"; - exit(1); - } - }}, - {"--game", [&](int& i) { arg_map["-g"](i); }}, + std::optional addGameFolder; + std::optional setAddonFolder; + std::optional patchFile; - {"-p", - [&](int& i) { - if (i + 1 < argc) { - MemoryPatcher::patch_file = argv[++i]; - } else { - std::cerr << "Error: Missing argument for -p/--patch\n"; - exit(1); - } - }}, - {"--patch", [&](int& i) { arg_map["-p"](i); }}, + // ---- 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"); - {"-i", [&](int&) { Core::FileSys::MntPoints::ignore_game_patches = true; }}, - {"--ignore-game-patch", [&](int& i) { arg_map["-i"](i); }}, - {"-f", - [&](int& i) { - if (++i >= argc) { - std::cerr << "Error: Missing argument for -f/--fullscreen\n"; - exit(1); - } - std::string f_param(argv[i]); - bool is_fullscreen; - if (f_param == "true") { - is_fullscreen = true; - } else if (f_param == "false") { - is_fullscreen = false; - } else { - std::cerr - << "Error: Invalid argument for -f/--fullscreen. Use 'true' or 'false'.\n"; - exit(1); - } - // Set fullscreen mode without saving it to config file - Config::setIsFullscreen(is_fullscreen); - }}, - {"--fullscreen", [&](int& i) { arg_map["-f"](i); }}, - {"--add-game-folder", - [&](int& i) { - if (++i >= argc) { - std::cerr << "Error: Missing argument for --add-game-folder\n"; - exit(1); - } - std::string config_dir(argv[i]); - std::filesystem::path config_path = std::filesystem::path(config_dir); - std::error_code discard; - if (!std::filesystem::exists(config_path, discard)) { - std::cerr << "Error: File does not exist: " << config_path << "\n"; - exit(1); - } + // FULLSCREEN: behavior-identical + app.add_option("-f,--fullscreen", fullscreenStr, "Fullscreen mode (true|false)"); - Config::addGameInstallDir(config_path); - Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml"); - std::cout << "Game folder successfully saved.\n"; - exit(0); - }}, - {"--set-addon-folder", - [&](int& i) { - if (++i >= argc) { - std::cerr << "Error: Missing argument for --add-addon-folder\n"; - exit(1); - } - std::string config_dir(argv[i]); - std::filesystem::path config_path = std::filesystem::path(config_dir); - std::error_code discard; - if (!std::filesystem::exists(config_path, discard)) { - std::cerr << "Error: File does not exist: " << config_path << "\n"; - exit(1); - } + app.add_option("--override-root", overrideRoot)->check(CLI::ExistingDirectory); - Config::setAddonInstallDir(config_path); - Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml"); - std::cout << "Addon folder successfully saved.\n"; - exit(0); - }}, - {"--log-append", [&](int& i) { Common::Log::SetAppend(); }}, - {"--config-clean", [&](int& i) { Config::setConfigMode(Config::ConfigMode::Clean); }}, - {"--config-global", [&](int& i) { Config::setConfigMode(Config::ConfigMode::Global); }}, - {"--override-root", - [&](int& i) { - if (++i >= argc) { - std::cerr << "Error: Missing argument for --override-root\n"; - exit(1); - } - std::string folder_str{argv[i]}; - std::filesystem::path folder{folder_str}; - if (!std::filesystem::exists(folder) || !std::filesystem::is_directory(folder)) { - std::cerr << "Error: Folder does not exist: " << folder_str << "\n"; - exit(1); - } - game_folder = folder; - }}, - {"--wait-for-debugger", [&](int& i) { waitForDebugger = true; }}, - {"--wait-for-pid", - [&](int& i) { - if (++i >= argc) { - std::cerr << "Error: Missing argument for --wait-for-pid\n"; - exit(1); - } - waitPid = std::stoi(argv[i]); - }}, - {"--show-fps", [&](int& i) { Config::setShowFpsCounter(true); }}}; + 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) { - if (!SDL_ShowSimpleMessageBox( - SDL_MESSAGEBOX_INFORMATION, "shadPS4", - "This is a CLI application. Please use the QTLauncher for a GUI: " - "https://github.com/shadps4-emu/shadps4-qtlauncher/releases", - nullptr)) - std::cerr << "Could not display SDL message box! Error: " << SDL_GetError() << "\n"; - int dummy = 0; // one does not simply pass 0 directly - arg_map.at("-h")(dummy); + 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; } - // Parse command-line arguments using the map - for (int i = 1; i < argc; ++i) { - std::string cur_arg = argv[i]; - auto it = arg_map.find(cur_arg); - if (it != arg_map.end()) { - it->second(i); // Call the associated lambda function - } else if (i == argc - 1 && !has_game_argument) { - // Assume the last argument is the game file if not specified via -g/--game - game_path = argv[i]; - has_game_argument = true; - } else if (std::string(argv[i]) == "--") { - if (i + 1 == argc) { - std::cerr << "Warning: -- is set, but no game arguments are added!\n"; - break; - } - for (int j = i + 1; j < argc; j++) { - game_args.push_back(argv[j]); - } - break; - } else if (i + 1 < argc && std::string(argv[i + 1]) == "--") { - if (!has_game_argument) { - game_path = argv[i]; - has_game_argument = true; - } + 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 << "Unknown argument: " << cur_arg << ", see --help for info.\n"; - } - } - - // If no game directory is set and no command line argument, prompt for it - if (Config::getGameInstallDirs().empty()) { - std::cerr << "Warning: No game folder set, please set it by calling shadps4" - " with the --add-game-folder argument\n"; - } - - if (!has_game_argument) { - std::cerr << "Error: Please provide a game path or ID.\n"; - exit(1); - } - - // Check if the game path or ID exists - std::filesystem::path eboot_path(game_path); - - // Check if the provided path is a valid file - if (!std::filesystem::exists(eboot_path)) { - // If not a file, treat it as a game ID and search in install directories recursively - bool game_found = false; - const int max_depth = 5; - for (const auto& install_dir : Config::getGameInstallDirs()) { - if (auto found_path = Common::FS::FindGameByID(install_dir, game_path, max_depth)) { - eboot_path = *found_path; - game_found = true; - break; - } - } - if (!game_found) { - std::cerr << "Error: Game ID or file path not found: " << game_path << std::endl; + std::cerr << "Error: Please provide a game path or ID.\n"; return 1; } } - if (waitPid.has_value()) { - Core::Debugger::WaitForPid(waitPid.value()); + // ---- 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; + } } - // Run the emulator with the resolved eboot path - Core::Emulator* emulator = Common::Singleton::Instance(); + 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]; emulator->waitForDebuggerBeforeRun = waitForDebugger; - emulator->Run(eboot_path, game_args, game_folder); + emulator->Run(ebootPath, gameArgs, overrideRoot); return 0; } From 1e99c4b5066f9737612d89b3a5c1df1f59fc79f2 Mon Sep 17 00:00:00 2001 From: psucien <168137814+psucien@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:17:51 +0100 Subject: [PATCH 52/83] shader_recompiler: VS clip distance emulation for NVIDIA GPUs (#3958) --- CMakeLists.txt | 1 + externals/CMakeLists.txt | 1 + .../backend/spirv/spirv_emit_context.cpp | 51 +++++++++++++++---- src/shader_recompiler/ir/attribute.cpp | 2 +- .../inject_clip_distance_attributes.cpp | 41 +++++++++++++++ src/shader_recompiler/ir/passes/ir_passes.h | 3 +- src/shader_recompiler/profile.h | 2 +- src/shader_recompiler/recompiler.cpp | 17 ++++--- src/shader_recompiler/runtime_info.h | 7 ++- .../renderer_vulkan/vk_pipeline_cache.cpp | 15 +++++- 10 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 src/shader_recompiler/ir/passes/inject_clip_distance_attributes.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5fe8ecb10..8c81c7550 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -916,6 +916,7 @@ set(SHADER_RECOMPILER src/shader_recompiler/profile.h src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp src/shader_recompiler/ir/passes/hull_shader_transform.cpp src/shader_recompiler/ir/passes/identity_removal_pass.cpp + src/shader_recompiler/ir/passes/inject_clip_distance_attributes.cpp src/shader_recompiler/ir/passes/ir_passes.h src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp src/shader_recompiler/ir/passes/lower_fp64_to_fp32.cpp diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index f20310a91..e243f63db 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -204,6 +204,7 @@ add_subdirectory(tracy) # pugixml if (NOT TARGET pugixml::pugixml) + option(PUGIXML_NO_EXCEPTIONS "" ON) add_subdirectory(pugixml) endif() diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index cc6d19075..4600d30af 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -364,7 +364,7 @@ void EmitContext::DefineInputs() { } break; } - case LogicalStage::Fragment: + case LogicalStage::Fragment: { if (info.loads.GetAny(IR::Attribute::FragCoord)) { frag_coord = DefineVariable(F32[4], spv::BuiltIn::FragCoord, spv::StorageClass::Input); } @@ -418,7 +418,13 @@ void EmitContext::DefineInputs() { spv::StorageClass::Input); } } - for (s32 i = 0; i < runtime_info.fs_info.num_inputs; i++) { + + const bool has_clip_distance_inputs = runtime_info.fs_info.clip_distance_emulation; + // Clip distances attribute vector is the last in inputs array + const auto num_inputs = + runtime_info.fs_info.num_inputs - (has_clip_distance_inputs ? 1 : 0); + + for (s32 i = 0; i < num_inputs; i++) { const auto& input = runtime_info.fs_info.inputs[i]; if (input.IsDefault()) { continue; @@ -428,12 +434,13 @@ void EmitContext::DefineInputs() { const auto [primary, auxiliary] = info.fs_interpolation[i]; const Id type = F32[num_components]; const Id attr_id = [&] { + const auto bind_location = input.param_index + (has_clip_distance_inputs ? 1 : 0); if (primary == Qualifier::PerVertex && profile.supports_fragment_shader_barycentric) { - return Name(DefineInput(TypeArray(type, ConstU32(3U)), input.param_index), + return Name(DefineInput(TypeArray(type, ConstU32(3U)), bind_location), fmt::format("fs_in_attr{}_p", i)); } - return Name(DefineInput(type, input.param_index), fmt::format("fs_in_attr{}", i)); + return Name(DefineInput(type, bind_location), fmt::format("fs_in_attr{}", i)); }(); if (primary == Qualifier::PerVertex) { Decorate(attr_id, profile.supports_amd_shader_explicit_vertex_parameter @@ -450,7 +457,15 @@ void EmitContext::DefineInputs() { input_params[i] = GetAttributeInfo(AmdGpu::NumberFormat::Float, attr_id, num_components, false, false, primary == Qualifier::PerVertex); } + + if (has_clip_distance_inputs) { + const auto type = F32[MaxEmulatedClipDistances]; + const auto attr_id = Name(DefineInput(type, 0), fmt::format("cldist_attr{}", 0)); + input_params[num_inputs] = GetAttributeInfo(AmdGpu::NumberFormat::Float, attr_id, + MaxEmulatedClipDistances, false); + } break; + } case LogicalStage::Compute: if (info.loads.GetAny(IR::Attribute::WorkgroupIndex) || info.loads.GetAny(IR::Attribute::WorkgroupId)) { @@ -546,11 +561,16 @@ void EmitContext::DefineVertexBlock() { const std::array zero{f32_zero_value, f32_zero_value, f32_zero_value, f32_zero_value, f32_zero_value, f32_zero_value, f32_zero_value, f32_zero_value}; output_position = DefineVariable(F32[4], spv::BuiltIn::Position, spv::StorageClass::Output); - 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 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); + } } if (info.stores.GetAny(IR::Attribute::CullDistance)) { const Id type{TypeArray(F32[1], ConstU32(8U))}; @@ -583,16 +603,27 @@ void EmitContext::DefineOutputs() { Name(output_attr_array, "out_attrs"); } } else { + const auto has_clip_distance_outputs = 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}; if (!info.stores.GetAny(param)) { continue; } const u32 num_components = info.stores.NumComponents(param); - const Id id{DefineOutput(F32[num_components], i)}; + const Id id{ + DefineOutput(F32[num_components], i + (has_clip_distance_outputs ? 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) { + clip_distances = Id{DefineOutput(F32[MaxEmulatedClipDistances], 0)}; + output_params[num_attrs] = GetAttributeInfo( + AmdGpu::NumberFormat::Float, clip_distances, MaxEmulatedClipDistances, true); + Name(clip_distances, fmt::format("cldist_attr{}", 0)); } } break; diff --git a/src/shader_recompiler/ir/attribute.cpp b/src/shader_recompiler/ir/attribute.cpp index 84a9fafeb..e74b62817 100644 --- a/src/shader_recompiler/ir/attribute.cpp +++ b/src/shader_recompiler/ir/attribute.cpp @@ -101,7 +101,7 @@ std::string NameOf(Attribute attribute) { case Attribute::Param31: return "Param31"; case Attribute::ClipDistance: - return "ClipDistanace"; + return "ClipDistance"; case Attribute::CullDistance: return "CullDistance"; case Attribute::RenderTargetIndex: diff --git a/src/shader_recompiler/ir/passes/inject_clip_distance_attributes.cpp b/src/shader_recompiler/ir/passes/inject_clip_distance_attributes.cpp new file mode 100644 index 000000000..cf93142a1 --- /dev/null +++ b/src/shader_recompiler/ir/passes/inject_clip_distance_attributes.cpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "shader_recompiler/info.h" +#include "shader_recompiler/ir/basic_block.h" +#include "shader_recompiler/ir/ir_emitter.h" +#include "shader_recompiler/ir/program.h" + +namespace Shader { + +void InjectClipDistanceAttributes(IR::Program& program, RuntimeInfo& runtime_info) { + auto& info = runtime_info.fs_info; + + if (!info.clip_distance_emulation || program.info.l_stage != LogicalStage::Fragment) { + return; + } + + auto* first_block = *program.blocks.begin(); + auto it = std::ranges::find_if(first_block->Instructions(), [](const IR::Inst& inst) { + return inst.GetOpcode() == IR::Opcode::Prologue; + }); + ASSERT(it != first_block->end()); + ++it; + ASSERT(it != first_block->end()); + ++it; + + IR::IREmitter ir{*first_block, it}; + + // We don't know how many clip distances are exported by VS as it is not processed at this point + // yet. Here is an assumption that we will have not more than 4 of them (while max is 8) to save + // one attributes export slot. + const auto attrib = IR::Attribute::Param0 + info.num_inputs; + for (u32 comp = 0; comp < MaxEmulatedClipDistances; ++comp) { + const auto attr_read = ir.GetAttribute(attrib, comp); + const auto cond_id = ir.FPLessThan(attr_read, ir.Imm32(0.0f)); + ir.Discard(cond_id); + } + ++info.num_inputs; +} + +} // namespace Shader diff --git a/src/shader_recompiler/ir/passes/ir_passes.h b/src/shader_recompiler/ir/passes/ir_passes.h index 5bf362284..f103b6736 100644 --- a/src/shader_recompiler/ir/passes/ir_passes.h +++ b/src/shader_recompiler/ir/passes/ir_passes.h @@ -8,7 +8,8 @@ namespace Shader { struct Profile; -} +void InjectClipDistanceAttributes(IR::Program& program, RuntimeInfo& runtime_info); +} // namespace Shader namespace Shader::Optimization { diff --git a/src/shader_recompiler/profile.h b/src/shader_recompiler/profile.h index 52e37bbf0..038a80733 100644 --- a/src/shader_recompiler/profile.h +++ b/src/shader_recompiler/profile.h @@ -41,7 +41,7 @@ struct Profile { bool needs_lds_barriers{}; bool needs_buffer_offsets{}; bool needs_unorm_fixup{}; - bool _pad0{}; + bool needs_clip_distance_emulation{}; }; } // namespace Shader diff --git a/src/shader_recompiler/recompiler.cpp b/src/shader_recompiler/recompiler.cpp index 4764ddbec..f4fa45afc 100644 --- a/src/shader_recompiler/recompiler.cpp +++ b/src/shader_recompiler/recompiler.cpp @@ -13,17 +13,16 @@ namespace Shader { IR::BlockList GenerateBlocks(const IR::AbstractSyntaxList& syntax_list) { size_t num_syntax_blocks{}; - for (const auto& node : syntax_list) { - if (node.type == IR::AbstractSyntaxNode::Type::Block) { + for (const auto& [_, type] : syntax_list) { + if (type == IR::AbstractSyntaxNode::Type::Block) { ++num_syntax_blocks; } } - IR::BlockList blocks; + IR::BlockList blocks{}; blocks.reserve(num_syntax_blocks); - u32 order_index{}; - for (const auto& node : syntax_list) { - if (node.type == IR::AbstractSyntaxNode::Type::Block) { - blocks.push_back(node.data.block); + for (const auto& [data, type] : syntax_list) { + if (type == IR::AbstractSyntaxNode::Type::Block) { + blocks.push_back(data.block); } } return blocks; @@ -60,6 +59,10 @@ IR::Program TranslateProgram(const std::span& code, Pools& pools, Inf program.blocks = GenerateBlocks(program.syntax_list); program.post_order_blocks = Shader::IR::PostOrder(program.syntax_list.front()); + // On NVIDIA GPUs HW interpolation of clip distance values seems broken, and we need to emulate + // it with expensive discard in PS. + Shader::InjectClipDistanceAttributes(program, runtime_info); + // Run optimization passes if (!profile.support_float64) { Shader::Optimization::LowerFp64ToFp32(program); diff --git a/src/shader_recompiler/runtime_info.h b/src/shader_recompiler/runtime_info.h index 8620ab970..04e176765 100644 --- a/src/shader_recompiler/runtime_info.h +++ b/src/shader_recompiler/runtime_info.h @@ -34,6 +34,7 @@ enum class LogicalStage : u32 { }; constexpr u32 MaxStageTypes = static_cast(LogicalStage::NumLogicalStages); +constexpr auto MaxEmulatedClipDistances = 4u; constexpr Stage StageFromIndex(size_t index) noexcept { return static_cast(index); @@ -201,14 +202,16 @@ struct FragmentRuntimeInfo { std::array inputs; std::array color_buffers; AmdGpu::ShaderExportFormat z_export_format; - u8 mrtz_mask; - bool dual_source_blending; + u8 mrtz_mask{}; + bool dual_source_blending{false}; + bool clip_distance_emulation{false}; bool operator==(const FragmentRuntimeInfo& other) const noexcept { return std::ranges::equal(color_buffers, other.color_buffers) && en_flags == other.en_flags && addr_flags == other.addr_flags && num_inputs == other.num_inputs && z_export_format == other.z_export_format && mrtz_mask == other.mrtz_mask && dual_source_blending == other.dual_source_blending && + clip_distance_emulation == other.clip_distance_emulation && std::ranges::equal(inputs.begin(), inputs.begin() + num_inputs, other.inputs.begin(), other.inputs.begin() + num_inputs); } diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index a0ea58817..1b0af1d17 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -101,7 +101,7 @@ const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(Stage stage, LogicalS switch (stage) { case Stage::Local: { BuildCommon(regs.ls_program); - Shader::TessellationDataConstantBuffer tess_constants; + Shader::TessellationDataConstantBuffer tess_constants{}; const auto* hull_info = infos[u32(Shader::LogicalStage::TessellationControl)]; hull_info->ReadTessConstantBuffer(tess_constants); info.ls_info.ls_stride = tess_constants.ls_stride; @@ -199,6 +199,10 @@ const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(Stage stage, LogicalS for (u32 i = 0; i < Shader::MaxColorBuffers; i++) { info.fs_info.color_buffers[i] = graphics_key.color_buffers[i]; } + info.fs_info.clip_distance_emulation = + regs.vs_output_control.clip_distance_enable && + !regs.stage_enable.IsStageEnabled(static_cast(Stage::Local)) && + profile.needs_clip_distance_emulation; break; } case Stage::Compute: { @@ -266,6 +270,7 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, instance.GetDriverID() == vk::DriverId::eMoltenvk, .needs_buffer_offsets = instance.StorageMinAlignment() > 4, .needs_unorm_fixup = instance.GetDriverID() == vk::DriverId::eMoltenvk, + .needs_clip_distance_emulation = instance.GetDriverID() == vk::DriverId::eNvidiaProprietary, }; WarmUp(); @@ -460,7 +465,13 @@ bool PipelineCache::RefreshGraphicsStages() { infos.fill(nullptr); modules.fill(nullptr); - bind_stage(Stage::Fragment, LogicalStage::Fragment); + const auto result = bind_stage(Stage::Fragment, LogicalStage::Fragment); + if (!result && regs.vs_output_control.clip_distance_enable && + profile.needs_clip_distance_emulation) { + // TODO: need to implement a discard only fallback shader + LOG_WARNING(Render_Vulkan, + "Clip distance emulation is ineffective due to absense of fragment shader"); + } const auto* fs_info = infos[static_cast(LogicalStage::Fragment)]; key.mrt_mask = fs_info ? fs_info->mrt_mask : 0u; From 514e3634722c35bf4b1ae0044a65d23752c18962 Mon Sep 17 00:00:00 2001 From: Berk Date: Tue, 27 Jan 2026 11:09:32 +0300 Subject: [PATCH 53/83] Add Docker build support and documentation (#3960) * Docker builder support * update licenses * oops I forgot change this description --- README.md | 7 +- .../.devcontainer/devcontainer.json | 45 +++++++++ documents/Docker Builder/.docker/Dockerfile | 38 ++++++++ documents/Docker Builder/docker-compose.yml | 10 ++ documents/building-docker.md | 91 +++++++++++++++++++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 documents/Docker Builder/.devcontainer/devcontainer.json create mode 100644 documents/Docker Builder/.docker/Dockerfile create mode 100644 documents/Docker Builder/docker-compose.yml create mode 100644 documents/building-docker.md diff --git a/README.md b/README.md index e43a2408d..0fb5c26ed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ @@ -58,6 +58,11 @@ This project began for fun. Given our limited free time, it may take some time b # Building +## Docker + +For building shadPS4 in a containerized environment using Docker and VSCode, check the instructions here: +[**Docker Build Instructions**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-docker.md) + ## Windows Check the build instructions for [**Windows**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-windows.md). diff --git a/documents/Docker Builder/.devcontainer/devcontainer.json b/documents/Docker Builder/.devcontainer/devcontainer.json new file mode 100644 index 000000000..32e301bd9 --- /dev/null +++ b/documents/Docker Builder/.devcontainer/devcontainer.json @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +{ + "name": "shadPS4-dev", + "dockerComposeFile": [ + "../docker-compose.yml" + ], + "containerEnv": { + "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}", + "GITHUB_USER": "${localEnv:GITHUB_USER}" + }, + "service": "shadps4", + "workspaceFolder": "/workspaces/shadPS4", + "remoteUser": "root", + "shutdownAction": "none", + "customizations": { + "vscode": { + "extensions": [ + "llvm-vs-code-extensions.vscode-clangd" + ], + "settings": { + "C_Cpp.intelliSenseEngine": "disabled", + "clangd.arguments": [ + "--background-index", + "--clang-tidy", + "--completion-style=detailed", + "--header-insertion=never" + ] + } + } + }, + "settings": { + "cmake.configureOnOpen": false, + "cmake.generator": "Unix Makefiles", + "cmake.environment": { + "CC": "clang", + "CXX": "clang++" + }, + "cmake.configureSettings": { + "CMAKE_CXX_STANDARD": "23", + "CMAKE_CXX_STANDARD_REQUIRED": "ON" + } + } +} \ No newline at end of file diff --git a/documents/Docker Builder/.docker/Dockerfile b/documents/Docker Builder/.docker/Dockerfile new file mode 100644 index 000000000..285144374 --- /dev/null +++ b/documents/Docker Builder/.docker/Dockerfile @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + build-essential \ + clang \ + 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/*/* + +WORKDIR /workspaces/shadPS4 \ No newline at end of file diff --git a/documents/Docker Builder/docker-compose.yml b/documents/Docker Builder/docker-compose.yml new file mode 100644 index 000000000..39efefa72 --- /dev/null +++ b/documents/Docker Builder/docker-compose.yml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +services: + shadps4: + build: + context: ./.docker + volumes: + - ./emu:/workspaces/shadPS4:cached + tty: true diff --git a/documents/building-docker.md b/documents/building-docker.md new file mode 100644 index 000000000..95be00044 --- /dev/null +++ b/documents/building-docker.md @@ -0,0 +1,91 @@ + + +# Building shadPS4 with Docker and VSCode Support + +This guide explains how to build **shadPS4** using Docker while keeping full compatibility with **VSCode** development. + +--- + +## Prerequisites + +Before starting, ensure you have: + +- **Docker Engine** or **Docker Desktop** installed + [Installation Guide](https://docs.docker.com/engine/install/) + +- **Git** installed on your system. + +--- + +## Step 1: Prepare the Docker Environment + +Inside the container (or on your host if mounting volumes): + +1. Navigate to the repository folder containing the Docker Builder folder: + +```bash +cd +``` + +2. Start the Docker container: + +```bash +docker compose up -d +``` + +This will spin up a container with all the necessary build dependencies, including Clang, CMake, SDL2, Vulkan, and more. + +## Step 2: Clone shadPS4 Source + +```bash +mkdir emu +cd emu +git clone --recursive https://github.com/shadps4-emu/shadPS4.git . + +or your fork link. +``` + +3. Initialize submodules: + +```bash +git submodule update --init --recursive +``` + +## Step 3: Build with CMake + +Generate the build directory and configure the project using Clang: + +```bash +cmake -S . -B build/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ +``` + +Then build the project: + +```bash +cmake --build ./build --parallel $(nproc) +``` + +* Tip: To enable debug builds, add -DCMAKE_BUILD_TYPE=Debug to the CMake command. + +--- + +After a successful build, the executable is located at: + +```bash +./build/shadps4 +``` + +## Step 4: VSCode Integration + +1. Open the repository in VSCode. +2. The CMake Tools extension should automatically detect the build directory inside the container or on your host. +3. You can configure build options, build, and debug directly from the VSCode interface without extra manual setup. + +# Notes + +* The Docker environment contains all dependencies, so you don’t need to install anything manually. +* Using Clang inside Docker ensures consistent builds across Linux and macOS runners. +* GitHub Actions are recommended for cross-platform builds, including Windows .exe output, which is not trivial to produce locally without Visual Studio or clang-cl. \ No newline at end of file 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 54/83] 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 55/83] 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 56/83] 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 57/83] 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 58/83] 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 59/83] 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 60/83] 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 61/83] 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 62/83] [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 63/83] 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 64/83] 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 65/83] 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 66/83] 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 67/83] 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 68/83] 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 69/83] 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 70/83] [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 zcmcG$Wm_C;(=`ksK#&Bt;K5ykTW|{mcXxLkV2}U_5ZqmYyIX+Z?(Xgk?)o%)@9RF^ zKk$A4X6ESbIs2?tRjXE2^YfdW#Cyb#h)_^a@1-O~6``PDG=SIb@8E#n{7as`z#nKQ zMG0Z3vJrxP;LTgJFJHevK~+W|JsQ9Q?-A@JHJqTJ(4Aj@pjS+Z-JzfYXQf2HD7))I zmOZ?buNrU9Mw1u@jrIKAO3rm)#|yok`thT|m!wBJ4vAt;`fA}o%sOsh`k*^o*Dpi0 z`5V%uaIN82EG@ThLJFbc?AhNb5F#T{tEghUcOIV;lZI#D{7d|QXI_3nsQTx$@2J^0Qjrw@y>5~8r~mhI-}l7+f4uZSB&mop zM40sZ9-T#TeI@JkQG*%jVN!0%a*hT0{m9T~>t@ct88Ul)GMQP(B>U?Ph{{VwQgnG> zau{+T*)=A|Ms@yp&tW*#Tw-mTH}4p4S5rGjCwTK8$zw z@jb(r+(9@v1YJ0h6k~p%Oukiz5-01aaF15RvA;>qh|1G|e4XpUZ`5tBAS(<_%6eb% zuO;gH@w~vfY+3k`h|g#XpEvwXP4DGrE`ocpW6<*6fEH(gZ@hnCiCNgP3zLYNhifC zXQ)M> zJyvpZfTl>s{%7m64}q^S!!$yb)~CM>&Z6eQ6d_>(+9yvqs4Z5d&kmc0bcGG%F1G1|qj@+clM(LnjGg~)X@;eejuh}2*9{tsSN;(m^%2>l1J%G&3rZ~yqB^RKY} z12gCK91_PjA>7zdwR=j|SQ5D9#bz4nPv$ES*nM{{=h$E*y8h?(t z7=V=c6vFQt>-J3lG1qJ-#7L8&y>niM@VWfmX8G7p_wqN#g5{bw-un4cm@QPN*W->L z#)Q)6`ij2w5m7uMgo6Grg=Y&0i$)Hs`imga(woTvn9++1 zmuB`w_830S0Ag*yz;T$41zL6vEo$zl&U`z*-Wl)K2ZUzEfU6`VN#~X6@VJWs*~cTT zz6RPr1BB$8;uT>jgAW<4)p2UBSnl3!HA4>7joT8s1J7D1m9bcn)h}P`TAM=O=;2?S z&z}iU(uLI!KZOM*P@)mr_Uu(H4qwHka-YMN+j*qxQ0Wkpz532?s^Sb5I1wRsXXc^8Gh;k9|ZAIT-Fm)hL~q{OAf48j--6IJGJagn|~7qMe)_Q7#`DT z3(lbFL&xd0jA+>&dM75fHF+=VB`rb{!_COVJ9!!NDv(;MXG*qo| zkqPph20{Z>Ewt*^)#LfyFyDR5Jb7>9tGlzYkSVHHII^Xv>4Be%sCcf==9_UMS$hT_ zu-4{An=?!Bm9`LOaJQfH`R-2OI zkd!3D98fBt!k`VhGggi;A~ zZMW7Y?RfTnoouuux`I(%nVx2*OAnJ!jokQ%rD9sS-&WtGc8Xt{=<;&%ArHm*M6CKG z)Jo)?OiRFf6lc@;N?i`xfU8l<>q^2NKZ~WO#m@q7O37K%@5$V>4{}*^IiXp8FF$F` zxUZ@|h?|_wdC$h|d8TGTxSG|T|H&{o=*Cy@;Xi}pGPzDs-1PEqL{yLqd{tnva4n|T z<_db(RunIl0y>xFon;VR8<$V`VgJPGjJeM8CDYkn_(VSi&`H$i!eaZh)9E#qpZv&W zPDvYG?VQl^=|g$xbZFjoFjuQ%tD`QpD)S{t3+(dLYy1xlMDdku2xw z$s+@P#?*4#HgI?^*Qwa&uJ6}7*AN>=r?Gb5Iwsri*LE>Anh>3yE_Xixa?d6WT$-6fYn@1I1P zQnT^lts^j6!dQv-Fs#@8tVF}bZbZej5wZfsLpnI5MT{|Ys4^^l&#{egXWq(Pztsx1 zN4koGe`toxc9?lJz8wo@@mQ;MsjJ8ZOtolYMtj3QdH^Xi>M%_7xoA8bW?gkfnp~`6 z(V>;{S>RC}P`st#&Cu&k1KXD~S`dLhhXyw=st++7(B2 zO)rneY}Zv9mmMqT_H$Hbm1QKpxqcp1tzw7OJbHM>1TUPlYQLi0)iHjBcrOf90I~ab z|0p!`t>21h&ez;2%kH9u!&qdN1H4TtZ;hu6w3utHne4{Z6ndtro5i}Z4lc{42LxW! z7vjmq5PSCB=$2}v&CYAP?K`y=oSF){HLLm%B-5cZJk-y$0llif8^Y2oDNlY;X&kNu z!X`FSeD0?}rPDvrtLbJwqPIJu=*>0j<#V>|#lisvx)L)OzZm6=N=L`*Sb1#I&8ne} z#(LQJMR|;4CS=p--|c+nXoch?2Akkmxit`H-2UXO`nlND&CIubJ z_QR;3(NJ-bJOv?|m}!F4*9bpLi$XT2-O;SZ*gA|!B=beY&92Pi)IVyUq#zIK-b1p;VZOK1s>ywo5lDx0T z9VyqkLVnyjTRSGk=3!U1z0P2pQ$bKhMKiK(tMI|@?g(#5>`UX|a-w@)!xDry*}G(P zJCt()9J4ww;389vOw7}A^)gG<7$iBoMKrhnxxo7B8fU91TqZOm-^PmYVI%g?+QKwC z!EOg7!kC#?&Y$7+9;?awcvg{3VUDxp%R2aBjUXYN%L(Y~iGm;+TkV&UqKu}u8# zrH+GpsI;a%SV~AFduR~chRpW6?tlJ?2jZt0;RT4&M6#tB>29aoG^Vow<=u|n`O%In z>q||##EIt6l~aBU-=}e1h+GT=(bo!=kIB&1%Xgk}9~=kIadwI%;5iu7g?`M#Zn0*a zK0HJ|*=zH~UuMxstU1E$d~V-yws;vf)uGXf9hUnUN8Y;Pum`aZ)VW=z)=bTH=s0WM z(O5guw{jT*8Xy%nq-0z>UbQ82x~P6U=a?mu0Pa!PebYmgX@oEhx?2#Lf*eid%z?6K z@5}gjw(zg7-8-zs8U=C&(QQ#eY5sB>B|e9zcsEmJi3XFP$ku5*jX&;xO=4AySvhsh zUQVu@Vix5h2dg}kcm-9OhlYIC>iO=8Cx6bSn{aYYYU>EMSMr@pHz~evm-X^L?4jR! zeo#7r#uhSc*u|}W&f|SBw{Sh|w~YCbXDdymZeC}R;Nl114_p~lAMU}8XmBRmX7WCz zzHB$#LT7~qSo;P@#J}W1k_9g7ZI3?~HzYxq9t!!H+7pAJU`o>_+=srIcP=~SU55qLk-vM!*eTojVV1!n1 zA!@GUD%yTJ4SrS$i6g6pMA(usDn0mD|k^RUI4YMqeJo zd9#u&kh9!7!i?|j6_mErj^xdEa>pl_WqLFw zzz$w0mzVODd(afjaQ<*sIL!)|J_ThLrWT3Lh)OVFqI=$#-+x}5{F3KBa-AXUynJcM z0byLZ&h+ElT}o=1n-P6ZgRLLZcediugPyl@Ysw=rA8h5^J-umGJA1^pnLclV>}l3{ z?My71g_&J@M?|fcZr!V49Yf2`QR8w;d zKLJFuD^LtDPd6?y^f)y%P!}8%Y?dY~7t|D*SNn$j#TA69gd_A}F z=)2-OU7fvIpTkCC^6_dYg=f%bM=HmgSmhO4Y(0TdUWG7 zst^w-X`!$dFAzLt3qtXVXz8%Vb@negg?H05$ZY#RCuAb0^-OFY=`t*fY=ghH1_%nY z1rdkYoy5)qERt9K<;6!T!OX+)@CzF{_WVafhuck@=ER9S8ZkTe>?96+s0#+S=3#?r z4{qzHlds3HI30@WXM%XiL~YjB1Wq=weOE*_BlXt0{f-a{_86~8X(s5LG>_|#wn=+c zRoubyt~G>dM`~^MDZN8{KBRQsc441Gqjmf%OdSP0a%FmXY}Vhbo|w(+I?B`{1p=HU zi{i^nV;)fai6Df)fbH@Nb@#p&+(T(9WA(CY5Q+0pQpYD%-xquHU3JOb=tBMFVh-$% zurXRBPTwAXI9UtN;IeDm8qZHRxfpzk2yN-G;z<$s3qjk$;N2~`Uw7j{>8 zF(QXLdVPc(59hsbX$EJ{f)}-SgAYCpGR|18kQsHnAyVgGEZwEF>eY?lh~R&r#;e}^ z%+F`K(L1^We^YLBak2bP5tO-BUJiO2=4N*fV`ZBnXC4WEaJ)X%t2EbVc>~ z_#;-DSJW&@gH2d$Gv`I{bZF9wrYe6}7q?r*^Ifx)!#LgqyI*7x&$dHv#pYvn$k@dkWBW?w6*}UEKa9 zs4WB&K8Jm(L@O1cZ|WTwTz3-PqnTRL4|jR&CZ&kbT9Wo5((oIZucSGSaFxBc4; zfAg;~_Rce34(Gq^&iPIAr3^Ht&V$fqTV%;{aZ5?_k z>@{V6Ke}mh)3o+5p(`goL(U9?wz2r(VaL~RRp!L*A~F&mJij&LWJK=p3}YL2J!`QYlDu;qqC_D^~J^H-0qT?CRw)ji9~*tCd};>P7zXu^p!cUMybTK9 z1=;O>d5^R8MUC2Ss~=-x*si6Mm>ncxu~^4g@J_m*?~5negEKnZvC38}LRM@**@)?t z(5`e>c_A&24bEBc^Hw#9W5vKE{$uu94wsiY>yq{tLmjG~NuK;wPbn=;2Tl z?tvn(ijYZ*snHd-bht_7#T}^O={Vng&_wv4%-!T7!~IZ*h+=w}y&0}j^}&o4G^;wZ z(il4pzG5+`d97-`1tH^Y9H#1Aawy?8^;J?UZ1Sp{c1V>n2r)bb{Y}=keg9tnyF$c# z?@?zU1IJ3K&DJ*~q_3@T_WwzA_hFaDX>>d=Az)l-$xlTSwcMqcV!_$f5&cEgh3@^4 zmRLnfiCcE$-}2YUK4APKO$c9-0?voljr$!P7wOr6T6%WH73-Sy}>Q`my-H8NTOvjJ;k&e5FQ!u$b^xW7n6FdPG;D z0;!vEiKc*h&X=}W+6f+(!8t@2pho}JtN)3JBNPZ#_tDufIe7l?i!$|Y2_wGQoCZoN zmNp!kHHY4~`-z$ArX&B#lN(D>B*jcLp0SyvsHoGm9+fr7(E5mQy962#H{;rne@mpH ze>WwS&hdIV7&8r8M-)ATOz40{MWLXs&<11BwF5OLoyBJ&;a@?stMStpRbmBXXjIH# zOGFpU(mie!mBPxx8fRuoAjN+feHY_Ll2#Vri1+Vxf2@$a9{V3=48JVV2Bi?kFG=FVATf z+j>n#z;36J0ZaMz#^{@WuqDn-vgve?N^=Fw{|vaYuYn=_18F#w+o>k>`yTckJpw5Y zaIuZGvq zIqY}*2pF@$Eo1=6q3o9!aUE2PRQ246W*8|OJCI&CIubZm`W6Sn_>5=D_KD`xQFnvh zYP5)r1N#8o#yoc8H;9qHH>#VA*&O*AcC$7Kmz2Tlb zrU)7FW&DKDjai~IO{bg&9Y)#`9mLgDU>25fY4Q_kXpESZdEgA3M%3~~ao*(vclwuXrpqgSXHf`q><#>9Yc!tz;^HDa=s z|53Jghu18`@>b{@K@kmRY%78;X6y`i{eY(R)Lau6^`O}QAQzr~6^bw(#Xe&_OwPs@ zGgN9NIazjGv4>tCP|~ErbZ4B?6w~nHa3w z7WBETYBhc3(k#6dOhi^|6KF1rzAnt8B&go%*s4WAwjYtv$SEcY-fa<=qeE+n|xCXqr_8Q(cRNr`-{NRI;#-L z$@XWNj8@C9xI)vuRjGS#t=i&c<5%hY%~ng)ND%`RUdgDo(2(CRdYyXt#XlJ`M#D34 zzhm7Vo4h9&NKT`9aZ_32*PtNM#Ma-`ZV>_>(({YrmPV!K;#?U1(h=sjln&;n&zX0X^oYpd{sZVPI+djudb}Pl0g)o*8T$ z#&zJTdD?)$LZWqDURL#tef~)2V=}v%mb|n(Q^n)uo#*C*cNNl_ix6@qgD+HIr;+k; z)4)a%XTBs${fAG2K0N)4#Abmqw%40i_{e-oMMh!`!pRYbrK|Oj+(5b#+~VE-LF7Lh zCZNE^@RbrdTNDzb0J6VLc68Pn+h@x%^72$qN%ZtUskwY*m&PAz^hFdKUuR)~DgM~V zCg_Q`&ak%LF0nyfGIClZ1k5EpMuHgtjtEt(kuj@?xKVUwFC1GMKJb_O!V`!lPRQa( z*rJyw;$Ed+$PwdQctSi?BNM}{rd83vusl#SL%eg6QhDog{#bH%6zp}eBuD#s@N1UQ zXBp2j%U*kdHdIihD6x#$`8%$rhk7sdd>wcf;^+8b@PjW|{K#9XUDbtyd2&^=%EBu* zQ(~)nV*>;!gLTJQ*Tud|ytf|GTKtQ-vxWR4HHNdgwDR-GD;1?T5OS28)#)&NCFb1| zXN&qc6}v4Luc$-5`@1EmDq`fVxD5EyEkp3m2nUNpV!FPbnYk%{XHQBZ?_x&A3%l>? zGLGam6U>}IN9+AVUCRT`Lj6s12Hfcdtk2L_HkO6n>vW&$sPd75+ituh{?1Vo(DS1l zuddb3M^h^E0IE{<8G8Bl)V9G=iO)tN68tI;>djOrP6xvG;9I7q!9j^{&VlH-0qA_n z%?2*lFnm%!L-G@i@|(M?P{Kd7%TsB5cEuVj5Zyk_^(*>$Ix z#My=UOBPPi8;Z%$7YeKYxAIycgKA=FeQu_JvE*Ef#*+KOcxu8NXLrMyD%|0&Mgg5Us zQU>>t#Yr>!t8*?04V`f>ZPNX9oQwWS^pm_7zv?u>bQM9D|$>os0#o>-93eZRx%UJolRuecRhpLDJ` zr{*vTysPG5b*!LVFWn;yk#AsfL4dHiZ{7V3N(TubH4#^mqGKrJ zr!6|2JDhzX+w8#{6|(lIdOGn4&e$zA-fhw+#b(g#I^Q1Ds4_+nylqEOuh56S*cskT zwkoq|(E&kfhf+8rQf!-kzW#KF;#52DE7~^gQ%1+cuzX}X@9O5anvX;I$yU2bKp?mS zubZQk&%`!cG0crROF{LgtM+S`bsdEn?@W1>o!gx!>vE6I9I?cKbix@TV>hjr^`Za~ zS&z+7Zx2kQqaEcTA@tVFL#oHYNb%%;Nz-cjXe-;Qi}p{WRi48mUaz})ltRll)@Iys zcY5q{&%9DGO)Sv2Yg&msw4tN3D7zO|ZR^S|?*i;@qbrNcy3aT4AI@tp&BhTx`mucb^b%UcoT)kaL2MJB zxhxn|*B5$BCIJY!PM70qUr8U{kIuFU->qpa__t?McpZ9a*BS}FM0R9db@mIWFV~g0 ztnvji^2nW&dmT&cWp5HaBzdF4FT; zulRZvD3e00&NtyyQ&gX^*29Va`&n@DuTi~o-A-mpeQ#>h#P&weu2@J7G>gzDq=jb5 zjdm^lH@%~-OYqy%=U4QSeMV+fFjck}Jjat-OzC9$NBM}wB}=!x$MA^1sPHxJvg0@j zaefg>l{jAOX787tY30Ol1X4R>5ryZvhRF9l*khB6N8G?a*7)61HqhUXheir*AKMc zzkf4(-#hP&qz!I#hrR;xVt3T+V5;zYaB$9~&L85z(sEF^lm4BsBF{aG-S*JSj55nY zb-YU1W}UF~+_tNKOiyys&+q|pM#y!EN#UY9ti(e2p;RakWw;NLLv$Zz7)kqGr%kiZ}E?e6Qb}uznF~>aD5;p zwLfjF6Us412+Sw3pQFzRBo)S478b3oTUZO&s~Z*dPZ}T+?&SRR`T7mQ z2Nhm|FA~W)RP?M1IqG85;dQ;=^M*-Ir|7(n@4fgWk3%!Q;jjj9+4xQp0z7kT%`&3s zd+7s44Fx6~tK+l0#xbtegaF7+{;)gZ@|Up+Rh7_W=pVaf#RQK1-)&K5C_{fz*Ic9} zxpWR+`t_e4MbB6EHeh2J2lby?7S7ZN_eQCzu_ZFcej;0_{c601YTqmmwl3FkID@NW z;#f~u{X0OYTgScjgSEMt2NsSM+Zi=61_K$9s2EEZ>IzMC$Ga zaV#ca@=K4Qo+81t%uY57mxCAWgIvL3 zTcTsg$yfb{aav48boTg->wWk)ANv}Asa51plaI_QA32&cNqAei5`8CUnH=0~+UkRsRteo^Lpq5g^VGJ_ z*cZ~UYIbwaAHovvZ5p=A4F&<$K+*8;r8zmMS`(~&ewxS3Ck6|(k5+XHpm|=xTj$^$HGxW=&|f!&8N%Ll`*5KEOfynajY(0EYKZHDez(a(qtk!QzC+&g)4Rw{(AsCOYEp9B3TlHNA!{@(lc9y)VUDPQ<2Q!dgQ(S6Jye3z~R zmpz>jeNjtSi3|smBN1WR@f`sfmsx0W zYh!)FY0G8I8q@@uE)I=Y^vt+X&asUc-;0+ocw9tp^|-+pD6TzRm{q@LxC86;SB`?f ztcILou*qwM)z7j4W;pS7;VdfGXEKaNbxlu?I^$=p9I-BEP&$~19(TV2ls_bPd%v{& zkcv9CQUV$k{w@Sdk7PtXY#Exp(F+(SRo5d_cef=*er@yBbN!7~`M&5RroJ4qPH!m} zkT?Xmce@;kQFe5pdYzCp)PMrqI&E(5um)QoKnnEHIXDjQA;6C(yJpY}VRn3-9IJ}I ze1)mf#Spae#+CP>f$jv{9Fb956Z58^QF37)DRV=Z{EI41p*WlL-jCJ{oI!Xovx?C; z6Qx4C@&iqryM4@8sq2bOU#MH3nb~v*7n@%F?=Q}kzT5;|X<^|vrcS+r@WaEy%Brf0 z%F3b&3TStCcd2|H(H$MaBdOdXVq&lyDK}{-A3m5)6)1!eE8Z$)h-gW!B))DaZuo;T&^{Wy`Ko0W<>R9W-#2xEqZ zw?P(H5$bhPoW`H45QDxqrcksVSaRG+HGeuA!2Z7ZCww~SXM%3k>I_35j9F@R9W3kq zwTtY7@n`uKuQ%sx9>a?c{@2X+06~|kZ;*Ta6zjho|FC?{eByn5h zf$Y@Hlw!0jdczvS#4H$mCj%TbM?S$MH&!jVOZ5hQ@Y%diY8=<$Vqn|D#V*s?ldCi1 zJUW2V&B!$>GjcJAic7w(+ikdA(|IWLzb2v^mF~Dc$x6WNV$9tNLHc*49UvMnI11S(rY(F(kSKWbEJ4<|2DU< z$jZwL%Fh0Th=fE#Prudm6Ju*<$MNyjA)J7_V?0NK+vkz1v9WP$ch~7)ie_M7z})&$ zR_G(E@jIiQuyMxLf+MZ^&_Ae7yi@|Mb5t|z`DM=KxExOJ4;M>Gb{-rScc=`xEBaX| z=>x88VVodfSW(vJ?hG<{=M~phdG*iR zA=St{jAd|N$V<>>EG8CG;qJH~tEBYkjBXRey>sjq!HbprdT;RnIf zZp>2iqnzrCDT5VZy=_m32@rcER`;9X~ zw=nRh2pz^29z1;Z7O}zEYS!r%Pm4o#ln*2Rd~}-itE&3FW(X_*b1Tqv<4TVn=K^3|d6kCW z>f0ryEfB)|?1kPysf~2q6Q~(6V%ofs%ifU?V#>U2k0C((-Al}`K z5HvkmX(8aU_ciJbSJ2i5Sfg1%VPRHPRdlu4sG_=h9{`B?YBLl)?<=g_+;kCTvQM9I zxyE*xR>eAeuGAj_7HPCpYvXk7zV=IHXXg~x=n^Lg|KzyJbNNs#%gRQ$y`&lQ&mMr&JVf}JccCRDBVzYP z@D~4T_ZQ+1t=8qH4Ra5FR9ywGz2nhBxt--q6#coJW)fFC5pC^^|%wI~Bp&oJ3OUjIt~XFS;!aes2M? z_Y|bQ{Q|U_a@6fp)%?7O6Wt*_J3Hg))VQ}veYu{RG0JTdID6UQLp|ameq~Abpj*7z zw90vwLCo&3AH?%~o)q^nLGW22nO%ipKHU_gS0FlyxHfu|{G+b6zQy~fzta8sbfU3x z{FOOx`X$z#WY@RVuC#`3WM?DevHo^z7GYH7A(NhZ8(Sbn-P1~Jc^qdcRvW8Ncb{0$ zQ!RVN$;#Pq6M~QLFM>e87WG3+ker>7D5kA#)Ip7bfq{V6H7tfoUQthPwMkY*B_pD8 z-kKd4K^YtKEh@UbyJNAKAn%DFKADkIQ4zS`Bl9^6)oWnDd9#dsNlIEUeX^G9P{hV0 zG|H09;H|wUtUb0fl++12Y01}F3Kp;00~2lKp$X9Daco_b_o`JtCAx|cCpQM!+yqq* zl5z2>9XXu^o>)*pPTq{EW#rbJQKtWqU0-XZOn*|{uBe0dm0Hgll-1@R>UjWL?VPy= z4GdiW**m)aN5z%eGS8#m|2#Mw!%gg(UVoCkXN7L;5{-@-fgTrz9&JBDNqn8TkXiQj za6jUwIalWt6b9#|Vq`ALyHD;*qHw=g{XOQGodpoJrlzMA6cvSid<5RVe?K)lYgDV* zQfC8%$JW;Lg#}GZD=Q0g^UT~_*y3r#zXb|8i7ZBN06~}$h>Z}q|M76rrq`_gj_D3#Q4r7<_D6Kvb{~&@6$sR_d7RUF8lCi-APqDBiNJ?z0ubxcC8O7e^lZA z*aHZO=9*>YFs|}#-J7n}vz8oawD;5yo0W>3#tR`@_EmmI=(n+Jwdb~~q=L^rZOB`j z^5cH81>1OE_W8T4BxfZgnkpkke9X=^Pj3+D%2pYG*EG%?@^|45I-FdEm|s=oA& zYQL9#Sh8!uF`tJi!K|8d_tUdRh49Mju;QSK1#b|;L9$wasxVKBn0Vl` z+b{&$S`yCU@9=^)a5m7qQb9oaqYe=+EmAaq80;g7ec9cWzbq+6NK5q}+2Y2loVLtu zYD$(De2N7yKQT78reHE=T0K5DC#j)c!Ih4TAJI+dW^h4yWyG2vBW>!%OB5Qi9de4w}J?-6NF;@WaOFGJ=*Y6!2}*s zNfoy!`4na3~j#fq@Z_kwN_R>sNscy?jxgMzpeIS$uoU6UPojPRVqqW72#(xWyl5POyX1+xc$N5Yo zj9yDON<0l`iBvcFpit~6^)0!9r7I&#oZrSkvDeuL`g`sO$(i7Z81dSP>R4ye8+QN@ zMa9MZo|OQKsDTCDo^1kA>H*mtP>9FJ#{td8{%Trn^W=ovuezuR*y@XUvy8J2Bqsa& z%l#Q58o=e%)p3>b;vbn7y!3M%;0GBB3=v)|HC7_j{@14$|Q$30=aaVB74g2+)hTzE(~ zdpwk7tt~A#R$rbuf$b#w`-Ym1E~lcRqEpE9uP^lPygXK*@Ot!;wO*Q=>w7i%-F5=E z$N6uq0X;**PlEU35e;`cDR?~21+xy?azHtId+RP9P9P;B0s~YL(E`-}xZOw!qe+st z9<^U3D(ZXB(deDsz#?I9yt)l}1-giAj#>Yri7@Hf>7Ze^lFc~qlD>bmIOu&vDlGk< z!s6nb>}>yog9BHXk`e62+g-tz{uH~`eKh@F0L)(X)j_7V)V@ARS<0AR4sDtc|F$+k zk>C#|fRZMg&Zn^``kzI`{plIRBG3s?o}->}<^pTcYIiU#@9Oss5pfCd2c}}BfZY5R zE6`Ns>RtRo4%^)(r#{KEgTsOfU^yjq^}T9geGd=rxVSh0&`-dOFz_G9KYxxxz@WTM zg)atTl-r6&@XZP+0tgjAoEmHLdAvQ-_P(0dcAYm76%+H2^;{wXgTX!FgoA)L`S%-7 zl>Pg_%0}uQyn?7;_TIP0P7)Hh?k(??0w-}52(&81Vk)HbmJ>UY%N@$X!b00)n?C5n z*S~;66lJ<*{b(j-f1_l~9z-(g(f0A5Oe491PTBklb5DBLu~J9VgxKbQQCq5IoCR?q#LpdJ)` z0J}pOf*AlJ!o*>|7D+d%KopxY^^SzX*3lNaOI2VF4-WVgq0`zGbac`rpn}MBnE3co zV`DK*hm*H!;bl}*EG;ej1_m|)adb%lUj{w`v=j`C&eyie>oIJ90WZsGXh;MYume!= z0Pk9^o5Vy#?e}vNEOA~ZxrN_3$Vo1_LDv%+^w_`>m_Pm^+ROklo!Mx5Ze``TO`{SJ z>~h3^Z2()?Gdc?NqX@(p1s+dD>g-mJRmD*C_r|h}jg6UsMnOks=Q7c*vWiM~rBN@J z`zZ;Pd^$N5RWOjsm2`E>Y@}bW1TYiE|6u{x)i3``96USSJn{!4`n(`i>9!vE*{-AF zGUMYV48?TVGLWX>N9Qhn4U2)YSKG!?WaccztHv zckkYvYHoGxk7bE?J>TsLglfNn@+~|(h%i7!RaG3|QGtPhKz*TKt7&0ty8{prOe`#m z6eHl&v-6Mt&aiOdj2N+xM^N)H2W5VLFEXAoGTPNy^qxMyGHzGDfLF}Z0An7F|uy6-D>)6pr_k(H4_s;#XBm@DWv8jnWl z%*;<;Zv-SHqqV<&qM|jqw64lq9YCg0P-y>;aJ06rF#NCrcwJrH41|SiE!+%Ga zMi3CHpn%CBR9NWB-K^})5K9~JT1*4FhYxK%lgJ^6J5_^>Mk{~taRt1%prN4`;Ik(8 zEB2k0oh!dLv#?_ZBuIJVm$gk+s|?BNgy~S6&80Z*~q2&-Xn+D8wUJ zYTE5UZW-}~hQZ}_G;S7SEK#MKo}NzSa`2zbmj|WQ7w6`7TQzzJ3JL-`eSr@Bzr=&v zp^PlVNAk;AgeVRH3eQs`q+3y?fLGnA7#w= z?rr|{o~$?~(_)QgsV*oR_^PX`D_t0>{JcEM^YioD*|ujf2L~1)$1YH=?KNsP*poW# z3>g3|0|<5_Pz|oXN(9^69+&#ui@-fJ#es4DnMcYNj3kjU6G8~{6B>6RD9K?eoMu?qV=PCgCZtui*E2&1umo7@xA7pLJ1(3R;bhK z!Z53ipr69R!g`&y!xQj0_gQ6{060ZN>{wl0HA2Qt{)DqK(7kBQK}$ymh$KKH%w8tq z7{{Mbul^h}^d4^kYq=U297$SKXH#m3oY048a1g9V2iOSo~)s}4@NqD*q2#2 zurW3*lW1)e{&BJWKxhmMv#q(GM)0{ARVw38Wr&sbeJK=>Q%04|@Ie3yZqua(2LX^< zO_(^SDf6pJp_+>3J@wo-mFdF})kjJpVFh=GoYfAk=$Al}mJz#O8yrZI&7W&5}z${h7#b5iy329 z+ah3K<==!p6++@q-Mbn@ypxks zZq22J#dS%qeN70+^-XjoY0S_N>}k-BH;57`w9Y{i5owXTJqQ^)&lB4V&6~{Ts!k5J zwZ&A1!$Zihs+@%USCW)z!HB^q5v1w4X9e_!l_vNX#Os)8j!0EPA98j-F*%=9ZUeX% zPT2}~aVzS67%D4rH`7}l$>Bq8EbHNKdLf=1NVyxl9RXoLyxU%KR^kC6T5lAY1P^`C`1~gvAVy*0;5fbVGIn7g ze-G)>Qq0WEyozoRLkDOnV44aR;rg%hH1_ZSzm8VWf%=p-$fJLWfP#nzDlafRLTM48SOS3*1!IYpQ7OnmK;lg{y3qaaRQ z6)k^S?%~q)Z+KR9upJ`7oTO*a&K0!QiPpe^5AjEh0=zQFHTxMOK0@N!=Ucv!slefcZX6%dX(32_lU2_Xm8R7x28F zP-kO98}uWP=HnIU7Bd98Ly?kI2}HxMGdD)0W^LCstuIpes4%hL?9t%x@$j)D)ziB3 zbdeT-r;c9kb>TEZSWqe4sf$=pJ!!;7=%(R@Yv-h^6$aZVzNwAgIv6!V>bk>-Gi2@U zD`q!P(hNA@(Xg@M!i5HB`kmW%4lGM_-=J2{AAQyzKfFpDHZ5WHa)^r|vHlaJ#B4;lt@|(sJIjK;Q2Bo0 z45QRSES#NC4|3X6y1qd9VWCoSrKjFG?^pgmgG6R$nrfb$*Jd%nNxx7i}TGP#h;(S8Xz2^o$H@0ws?iGfX#2@z<3$G+qm{M^6pr z^}F+BkVY-N2jUlfd03gxB9e%7Y;3?N)g}w&;OwD=8NQsBF4>?}L>`$6Gg!@v>1REQ z22pwaU~h*>Smt0UTLz5U!}YE{Y1BN?=yFdfVXYPM zVd*bbxPFAl`rc~zL8^!wrH79M>}@tlTC;0U2{O&AE4tOF!k6I!Va)e5Vkmjcu@wf? zo=U63p}Cq0DV7y+M8c(f!ijop7(Ef|_lM% z6?OaY(9F<_W=tsHrQY0-u`p0%R#nmPi)P@pxk?Yr+gT0@%$RcZ8UX`hJp!)nNv$zJ z0EsK4u-R-$@AsdIOQ+&PKFWgA{A7~$i>mkHj?vhW*^s9sK(b0g`oUc> zZbC2i2}xUC{ZEY)7zTSt-gz}1z8As4oAx1 z&@7vVIy$y{E4UO$8!i+yq-BL+*D(-SGz}Xg*_A#r2cHM3IpCiic>Gz_xb|e!!De&9 zr<>b4*IkS#-#wHx6fxin-iYnGw*Xaq8 zW!>$qdES{{m6vQ(yJ*pDO&-ve@x_KNun$N6Kye>XTXsH!qi`X=YtH#&!KE-?cCI) zznNs1Oh;D}ihztv&c%hRqOu&Ke97MD=!o5Ud)kRI-v$<3^}2YU4;1VFuerZ)s`7io ze{ou*RS=|GknT?D6r^L*A>G{|AdMg)ol19iiAZ;cwA7}vBov9VQJSgG==Om|P1)N)kEZ7Y~>VbRrL z7H)9M_mn%-eDICi_SPu~h5;U^Sv8y5sEt@eZX zUhPtb`@Td2#oP%_d9c@mdhI3nKh!>03Jd5OjCQY6;f;N%!T(v8A*9Y4d*F6i|Fd0GV2(9^V$-ZnXT@GoUq5h z#TMT{>JDS~Eym_Y)GhoPz=XZH=b3Irm}NsIB}JX6-$uWwa{2q=BjtwJ@R z7rAu1T~&p%?b>H?pD5eK38i0l_&4`L zPEl>>vc}s(msdO2+6_8U8Q06t#lu%F?$D~w@96@^=>ju_$lD~#_&#}ElFPX=gH`AD zIIMx)dPW_C5ciwy0u}fQ2J!AIQH!P~s?xs53)rjltnuovgrMzJGy9J1!3O3Abl@r8kG*g6mQwSOw@s2z&B` zN;ti^08}E)pA6Ib^u_kW#~AMA4bofJ>6uym_bi8Ii$cn!3v>O?%c$x5xjxD){>rp# z5?!=L6(u}*R&HXlV8>ARM~)Ea6t;@{gG1-+%+OYc)=XwP`pb)>Ozf|sl`o2d&KIg2 zqOO{FKBdp#@{DG#F)!9675s^T^lmNBvqWe5h=hHdy#Llr(uAkQKVLHYXEkr+NcrE; zOukr2Jwq0K8W9{|32Q7}$zOPvM)YnFl>7oA?Y|j&o})PLmo>22E^vY(y;&dBWo?jH zrEk*e+1s5X3wCL(; zcA$vzd0#SuYHez2>K)kAiirhE$D?ZkNZ=8i0uAa|2x|b?b)QXZ8hpSvfCKZoAaXHN z%6m9ixK7S&~SW}#OfN-PrQAYbPm zp|Q$X=lx0v*L$V@Yr;cb2I+Tm{M=IBlPG3{F(+Jq{s;`9%Nm(k>!j2_scPXjC>U9x z+VT*>XPrp%hi-}_zVpfR#I*9V2{9GDzt#&qD5TuO{zk{l&1p^ftG@ek%Dk2fei zkh;40^Pl}UD<612H>JC?qdhp)Fgv!j!j1(R$)~^Kxi4z7k z>swpEE~Tm}equuP@y+x+r3S5v{Os%i&|a@FsdR`Ul2=eL1q1-ZyGkibco?XQ^mNA> zlI8xuQ4o|EC+KFzSciQI?Zw*%AQMTZi)+%TEuR;{!kjYiqKL}rGA&ipi#8iw$bOf) zcmIH8w~YpMY8A0Z*&TU*uu$30G~fLsFU;D5B6Hzwi#<+hcw9xVENQ}o{*%hH8;GXn zTGG($2%Fob%#EK=hJ2dk z17o^huBcueq^}MmAHEF9X`3z&rqvzB|#e`d)9knZ-1X2bXLEA4_{w5mXnwFMbQGy z((m8t3KdetO-=J>2huwJ?lEAI2_JA_RvAnVq1Rmq!@iv3m2bRl%~0SCer7vemujuG zI$cErX)uH8J1h5p5Kx(jF&LOz^C^wx9-Lk57jXTl64h14)75u+aj41W-1@R6wu-Qf z_>X)!^urGn7y+Sy&XbiEB%P5UBJ^5%^TF8$Z}$01zxjD@{q)!4BeguUFThr%N6dNM zDr;zdkpS2B*eZ*7#&zkdvrl&Yp3Z(st&-k_J(Ri$%D38r>n-+qR$ygyh4sCUyIDvp(lBA%?&Y$tDt$Sg5{odzP;DR+*F8{=IA5-T_^CJnyRR zz!mV#xJC&qR{v5_neZk4dgpwoWBNl|eX32h_%2s{(KoANey|FtTq_gq&qONho|pLm4!rXZ77lUc6S?t65U4^}f44l8t@fmk$SLFZV5W}{2kp2|pFV-c@%%=c>tz%P zn2yOb@LWz+^;w;Xto@pS5W2wNTc_TC>b5>s&sgKCMCk45?)}O*`EZ3u$xZr?J?Ui? zcaQeSVpADjr5Tm>f?5$%$$}hriy-u%hPh2jSEDJ`abSp=gD>rzhhIS4jG+W@I)OV~nUm>Ln6(v~;TU;-##~%8*Z6wLOawnJr6a**_E&h{yLPy)b<_jy|UNs0FDQQ9J3KX6+us40`?t}4Ah?_CS6{?TY@tL%>3(DsIx5{?Qyd2KV@>n9#5%x zM`R~EDvTIE1M4OPu_!6&B@iklRaC}3|Io0fDm>OcL+N}80JZas>a()3*?iPHbn9(? zw?Ny^{t|{X@`Cn{GkoiBn(PUgM}L$opN$Lp_s7oNIKCIf`tB;olLCTa_7(ICoV+b) zACL5F+|p~IJq=eyCBm2L921TBrBnk8*0))?wYWe3rg$)aMo~~uP+RL#-|00=fN|*} zmZ21;uCBs$@_u*fm4v=46U9b<9!Dw=OiQL02n~Vk=ylj-WoK8|+PbvkqrTHUS7jor zq;#os&^?dd;Jl-RhK5Gn`LFrD19HG(?iI#^>2YbOyVzJXu(>qEx7ent-;OyDo*5z< z4g3yKj3W1OLV_PKXEbspW*Ob834YjayIP?F4mBWEq?ML3%E`4R(?FT?#$~ipb{bh` zDe);b650w;xw*JhbacQqadB~ZBC&$zVq$VK=zx4+d>PrVRn~6KN%%^@jq!b!(rZ)J{5#e&hGMQ7gebb$wFKiZ8gNi@y(-PLVIVUG>-gx-9r8X{)WZuHb{ErpG%e z-JFdt>G`gww~&jWVY0J9lN`)80{b{t31mXis(+r}@{pXQs9Qj@3Hn`g-` zCwP!SB_dO@bmA>!6~S#==jcR$-S$T)iH$StMtNZbN-6yIL+V9d%4C5S z!RvRxJ3l`U`gVwboD=$$=sh_#1=a#&ZEXWqp^N9L6-`NjFJ2q5rV0J;>EnxZv;Tt~zG)W^6%2L?P(4Ibh`Y59S zty0LbqV*Kb`|7BmwN>x`h>mA_lnVH2q81XbI1%-CD~}X~m(-$lya{#HN#&MhZ%yt{ zWzlFnJWvZ!MQr!WRn%>Uxd{geEg5a|2Tm&LCge=tA)N%HaCjUWK5h*-EpZ$j9g&cb zEE^DF*kl3yur;YH@_T`y!gPIe00~AAtPa$Yla2mGCMTeqh=JQN+EfjOj;x#MTycXuaOSFafvqd=Y|B_(Be zLb1d@SC#DU_LcxVFdXJXhM+M|fDt-#5gi>35ZA|ta3oVm-qw~0Om9Gq zYO?J&We1&%r)X%D{QS)qFNui{W=4o2`?>+e!t1aG=MP(4{a#v{2=_0L^IRTZJFXy` z;8q1i>FrY`xIFE}&W0Jdect+N`!NPCq#wx7h1t-Y#c?8X(l(l#vkMAe4G!p7IhA~1 zEYB-giIBB0-i^!2S5SIf-vHUX4u!@Jm;&)4E1R5yHerCuAvZZfk1hfz#Ky;KU5`#8 z08_LeYxydwtAjF<&*z#I+F{RfIzBe$Fw!6K&;C8GjjtJ3z!Bh&^fzR`)9^^AD>Bq+ zYNDb_eJf{@>p4Gn;<2pCwdCKNU0?jhUBzNW$vQA)aqBP+PvL2O7lOQ%|G*&!ByL0U zPoH{HSSeEFnVUlL^QmtEd8w}_7f_0=_F;1L)}K0bnA0_6aza_4A8 z*q_h(YZ#Rf??Wy(=!&A99rL49Wf1vqXx#Nl;+fL>8PPp6d6)ZHY<6Hucsf+a_jw)r zlf6i`Yp!S9!cp1JD6S=uuNlWzzNvY4nw8n-G!>?97t<2T5q@W>H2glxarL!Hk7I`r z%qXpwO4?-_;6^SUN%Z`Km+N0P9c$#U06)YtDk)a$8OG<&pZi>y#0C|UHJam z#L3Bt*BPWz{>}N{WsUc}cvN<>JzRpBksm5!UXy94PbHyLrc0FxMof4Rn#9TN&annb z&!Owl{l%(xjG6mdT02#5$Yf8^71x<;ZuXDOEY_jd+-#vgN@Lv8emkO2QZ(bzrPt{v-3LD&J_+(2>&v)uX5p`YT!LCSs zI-v#xNU^9CKE~Ai{z4H)OXqVgNgZ8TT{*!`R&(l6pGalHvn;B@omDowWr1W%#~)o* z^s22xPek})a}uMcJ9mL9_S6)NfqYautQ6*qv~3n|Hk%loa_Vcx z)F4XZ`>C8?G}rKnB0aBBZ=YcG;*!P~Tb2ZTFDQOfY}I34*`Pd*#T2zT#ZdSg zT*bDLlA7|PckYDX0tJ^vop;16KC=164Q|^dc5(YPlI~lf2uQ3kYxXYY?j0hl6qe`? z)Yp!suIbZ{Jv1e?#FNoe!c)SpmY~ooD3C<>mu=yw(nuG?NMnN}x5*2}!r>bpt6|xx z;tfN3iHZsu7X_p)*_>qyChcv#^UlU70;HUbP z{R*H7ffVl+;naX`^*xCac3l!msnEsrI0!t{=n5TP@F&CutO>I3MMp&u_ieppF>{%c z>Arnir#RvnANdv9o@cWfrbc=lQdrZsFs<*FKk#d%nYP`|^8bfx1Wk*y*z^CNZSEzB zVck^TOnT6%y~-Z?F?Pdqc78BJ#9vFot_I--SJ#xqwGcR7OcD?YEm#DoLm)~J2D zZ;EovX?7U3!$Ur&O+1b_JXgqjTbH9lR5kt5Sm2DlcnhiU-IvNw&p;h$g35i_f)>Q< zRf!UYWj^wgPa%o~${|?^n3mgHA5hspwj<$_hRyQ;g9vE&EOjJB+7~A$CqrMkN|xbD zib|t$XYa+fzL8z%1R&#NcNzv~=Iod-vpb5t$Ia-sXlL*`)f#sd5`uuDulM72w<7)& zEGOkjZtt)J8%j_vpNf5XpGGF)7rrr7#dqR3zZ0lX32?(#Mgx);p52Le0Dk7iPlu6< z)TGj@SW5k-I_G1D=Crk@z8V6;FcR{|HL|sWes!1Bx`K~CexU3za!t*;+kV~)2oOaW z?n3%1#9|oUKAx~qfF1Ee4pV8J1^5)_ZU}ktV|GfAO1D%l z!R-P}kAfVuI_%FBADUIu5IfY8XdMd)l;_306Tct`8Q zL<(yQzrJKy+@r6_WqnrxsJzY^vcq)>cn#1eKPChRx&$ z1^_T(va+)J-Qd~<;(Lxpg*TU#Ma-2|*r3x>XO8+WR=MlXt++%CP0C59E#4a|L$qH6h7O_+#d~0HbrmC7{tm( zf{XL6B?%7jkP^gCVo!uE-utiFA6q!=8kUo?rRB@4!fOB&?9nA7>`Rz7y#0)Ti-!l` zJW7s^RikWLpywDx$QAL5!~B{!*sM;QPv1M#y=G-2LHWx0^!Vx5H-uF)bvO&ACgl8~ zgsvbX^XUwI$et_GULvXFK2zuXOQOX$`+O#K6FyzOlmgryyAFqx@Ol9h`_w|M2Ms;b9dIX3n#);X;Fo%)yJ;x~_;5I`2!mVCq^#McA< zWqwq$U}mr9*J1E)=5*ebIOW{xPK8opm?MrSW0PBIp@xc%isp8(?nP6F(6Cx;64UlZ z_2u(er2#7F|GzWIIaB>K3QBy?Vg+{xG!Yl3%Osgmut|r2!wujyjiYcStvR1(omMvA zIdV^9s@G0&y*JPbqv=R$MnE5)R;1JTB1#m+PMET0Zx|;gT?y$54}$!GaGM7gpZeLp zFwo0CGavZmyZUEOwuLzItfZu09t3&|x^n-Fh>oVAq4@}wRZz9omb5T(awY->sJgnE zdhB^F;$J}B8Cc_AT0|Jo$;~;S#z?D(*42y7Qvir=x76%f8ul!K8}%%X{5_)&qv4^Y+p`wPGSWnPNm&jH#YB}Mxc;o0Q}e9teRKZ^szxdP-FgM#*vz5s+UxZ1jsK4gkG2LHdS zw=O4?Nz0dS%bV7p7t-k597Ot<&P=Rq%`;7`BKPGXNi6on{~(>I^;0C4_nFkpZE zX$#}a^Y@S5a3!ssX4OF-XCgGIt-ZsvdLE|E7Uk z1Yq!VaQ{#-FVZ{S>(QAz-kitx5}@-n?vQ`{K_I1+`h#Y{Jhz`Yy+#|r4QS$Q*b-bw z;`&b?ci;@MM&}JUoRo3LpW;EGZG_5sKFC+O`|ZC=KK;-IUrjs{hRypIl8;x(L+9v2^GdxxO6F)UEnbPg*$1SZz)>Yy15M=#O z)}!w1gDgR9vM#=HXz$k$rLZ;ew5rVA+qPwWiHKOb)}KV0eYV4h@t3=W0 z^geSoxjc4Y@PV^2m%4EYZ;;LnQ(?B0xD?VeVYbiNzbWYc-oyH?c&-z0k~)WaIe$pS z(hjYj$;EMafab;jTj%23@E^(wnqh08to+dMv#)4x@<8OmzWvSLuORv^=v6x%Kl1xV zvh7~OZd%Fbjnptx-acsbsxPcmijBeh-^eOAmS0wI#jChMNDr# z?WKeL@m0l%lS_4aThC!02lFCl6Y>xJ%>`6ky3h4pjnW6+pzTJ{QGEJ7%=In_b2^L- zMf|!KmGYa^3)b)cVQW%t@t}fD<#56R&z$#R7cd&Asj0zx}kJ{#qRc7;Mf@gw>c& zBqJe!1`OA~NZZRN&USJ^d8ujhN9p+Ob^>V}vEmboj7>COcDDtwwzXtZ&fX`*j@c-? z419E?E#UxVq^LYo{lwevN$-&)h(5J*CRLt1+YJ^B4!<`y-K2Y5V4kv>u@F6?viOt@WzNYP1h(}SIYJi_>am(;@3TCI38bF zwrg0nzt0%9wzp79uNfD$TRwJo>o_5HuVvmZhRwIT^N`sB-%Rdw@S^+K_zIKYcl~j6 z6i9tIjI=#Vpq#W=e~aFWy?^}VSv6RCDi@nJMnPyzu zydUiL6W`Euy|wZX5=864lAY>x%*IIeQNwMl~FvpdXW+(J#RocHT$nvCoS2M}wS{>rglJ?tnX zMPr42PH1gXPthELi8GYKfYMaa?22RJ z^3G$^dd0Nc0UJlEebl-7Uo3xNht6bT5GYnKPR1KqYCas3k>d0DAs??f7p#$twt14 z2H)2Hb?q5^NOUZ@gA7h6xk(3BCG#dFew)}hJ)IN@=<*G{4MJ5yqw-OuepbYmQYfLy zK^@l$M`FO)P+3v_$=!;GZ)|L6z?|Wp|D)_ZN)!ar(^vYpcSG>+HTd}XCW%pR#Mxj* zyw~*QgAMaBjWT0P%wPp%xNE#!_^>O-d}3Bi(;pMwT8A`a?BJgHUM&w`<^>1P zo?;(tCFIq8)wt7#l9U7nQ=*>wwdLF6k*z1R8K_Qe?yBF;-$4+|0&9*oyRw*!vsl}ebGy=_agB@cFMW!jg0$T~}Z84h9Jbh4H1Mf#a1>0D#52&o-{32GioHu_T5 zU0$3XxgBtkH8FPhyWqa-(e!Rvkla`_n6an1xh%Wg)Ia=+F1@FmwRL~ac6`CDvHGxM zhpZJR<|Ix!uX|bbx?g--YLJSg*MDaYrLg7>7LSd}ga_sO(>BVbJ*4YCtiq(eP-d)Lh9}Pyz-TuH$#3s@H2-gsg=KO ztgxLjkI(PS#cQ84w95x41jk8clQ3|=Oa6Cem{};y!e+w7@5%!uOW^7u_EqbBf@4`0 zCafO6%5gl}XU+1gsbY5gZIQ?w=7aYtX6-cURK(UVnBVB$%?YRPrNzCw?%t_!S`D8 z3}R0~l2TF+ZGZjbDjmfZBw5rNgjDVH^Ip<}AF=to^SarV;&8$t`EF4b(v8sHSOLg9 z8H){g^i3$W&?DA-$%jz-g?h`z#b2>4PF3sb5R3Hw#tn<@GlRX0-}PjZn;Zd63)!~x zpnq>qi#%$Y3keV3Oias(oRm}ep@@G9NxvR9ZSz48YPE|WKf)8_=iKxN@4u7POA#^E z4c}cBTC0X*$3S@`nIHXkWsDnH25rNER9Rq?w^=p1b822!o2{en{l-WRcRU%B+{K3O zUgaK_{!P{t&zY1W6;$V|6JVm*M;ai98y<5KQ|F~1u|4=1CySO(a$%oFACoU z#77Zg7#w&cNt3M(Wp>jBNwI7mJ<(U{d6Mtwe82|rvg3FV)RTX=ZliHKG1!^WC@FZh zMp-B^RcSG~VdHV3OTZe97rQ)vnDF=3{k}6)v0@(aTR`BDo{vaLvI4E~rj4K5s(!uz zLTPT>;{5fHTRU&bvbaBc;;S0Mo3Rt&uP6#8C!^-pM0IFi9k||hERKX8W~rEB8c%gE z-n?Snv2c$U<`1uq-fEC9?Q` zY(UfVE9ZM_(6XH8E)$M9=*kL?TdYC{ec&0i#d);w1vl+0omE2kOf^|#i0@6i&P820eb_XA@@2Q zE;|Hl&+|8)tK13YYwis(hEWD2uoaO``};4_%v}mCN5%CCu315;Zp*}WM0P>Nsw=C| zYFZ<>X{hOE9~jn3$DNmkJ|W6$?2|NntYg2AnQWQ{T~K~DH#*Zbb-|m25)Y>>N;{(R zZ)^_)vCwa>9bqcJ+ZmCdG+7AEuXYx9>P`xI(fXc3fDxY%$ZYz94i( zWo{EFZ!6#p2@F|eENHMM7>^zw2d6cj7x9xeyAqoq?0bNp6g=KzLpwQBP;s^4VJiOE zljS#H4-p#Dtn&)h=g(MWa#qyGpt3Fga&_+(Fmy|Mn;RPEXicU@jEZV;t=@2!_M{D2 zr)ueZRoQt|y-LleHWl`;LzX_y{il}GoHlh}aK);l#R8je+Wa%~;Rk}m#~3gzEF0s) z%=!XdD0p-=gL$(UO>MR1_ti!LlV0Mw+9vd%q0tsU``0qScVDBaY;+_n1^%%0atmFS zB2saGfi`93ua^e)kLpkN-Dgp)WYyD)-}y8Y4?ivV0TTkJ?_bVxne=g^_{gOty@<$2 z0nm4O^nzi=P~wC3?UK*gFz88kS_dFIoNmgx{imlwbLvMXzR?FQ=PanHm0;F^^o#Pk z?{Bzb*mQDD6lt=mey@xY5Im_2HUI&S%|~rx1Fd&80}_}nH}MsBpA`p5e`mo}e3kLA zc)Ag$NR?6F4_|rhRT$D8uL<4BIh5>VMp2v_E~ENZ6(w>U;=QkAwf-~B-!OQkH<)3Y z{f!Gb2M4L~6v7L{C%g!yu?;mjt53AxwtT8w@rYKCgh_rSCYy6O9Ge;@*XQUT!EV=2 z4sqcN9R+UYHv^;ekocGtt(0wc#?4v?2s9;BjGj6B-QtW%3=6b)U(fd<|8hjemhOht?dF~h zkMsJcLqw!~OMN^}19OOso|o1Te$`*q6u&V#Fe-c5hqQiazRpV_5D4eJO(7@={524U0XYrMh0OX1yhpzZp!ZICFX0Nx==jMtAVe{cBn5;TB7$ z=G%1-3a*sK@c{X;4c|qF#bP524xO95EP?A$o&zQWerFQ0CE*a+qN3yStB?$%r>r8LdAJ1q)v`v{>KU6nv5SkX5{zWGrl<*p(ZR7;O~Sw4P8~lR zBEcGyIFk1EbT~6Y3tTkEHc6?9@m+-M@Q}end-9_-&5Y9M$2l3a)^hbQ@XLQ1u0NcpF31dc(}I*e^AC;S zaNGSuq2&?AL&pQXRmqD|((qq=E@LMh?zRr?2FqjoAFO%olPtLvUU)t%3`ai+`h=J~ zsIJbduS?!dBGctI5&NrH54$I~77 zaAnO6dk9@*>Bulx)f}wg@!3VqH*WglpSj(iFr2<{S2}oV_viuw(4?&+^IU&sI$l2Y zFm<%AExbZ9=4(MDxrjPlUM0y3gzhkS<}S^v#CL1;(Z0Be71U1`JpGhk5wUB>jk&E( zjI36JU_+eGKYtA=Xf&5q;fU0PReb2BB-QnOLB;ngW;SE>Okw3_o!!rC+T?b%V3^=8 zT8-2r)t`5U)a&kTj1r{x^Qv&DXU4htMaa&)uSkCJn|gDindX+ZZB;PCM^$TY~)9T8Rb@MQ%i*xzC%GKiF(8=vkn_rH!vt{fCuJ7p%wykZV3j!drv?9awW7bZ@0rjzCo8DS)Z zWdWqMS-H9!hk0hw%eS*LE?v4p%~Q>JpQ!8w2SY_V2I6%(amQa8`Zfn+j@s-bxZTrb zU?X0{xcAdApj**7$E2{>Y$2p#rsIx54O;7Nm@2m54WE->o@viXGA$*`ppAQY^nFB zB5RTDJ5?c-go8GjkI>}Lk(owRuUXw)2)ZofJX^C-jPn|8m&n(H*IIf;5_T^!D4B8T zSX_~`AZqFjF^I6z@x!+hIpapO*`XUTaOeTCd*4xR5fJrWHrL?i^9~i_ubPi58yIgr zIr?I3y_fz>j(S{7bQe?~g;e*Zs48$?zPdD>GirTATHDnbOR?PXw{!5qC0uMa3rKdZ zYHY+LMhFP9Ez37^B{JxBS=x;cNiN5?Y*dZ@X8iUdFcaa24YX<&Y-{h`RpA|$Fl$!K zSN(Y%tdR^}`Sxu?cpRd>Y69ts?E!J_FD`Dnis`nP zDX}NlBPN?O$j%qTvsD`(G2Ha_=XqRo)yT&r#4-m{Dn1hy69tHhEc}Ii<%PvN`^txP zP%XQM`U{^haQmx?9jIR&??1>PtKOVUj-GA|&^oPZ_3H~^Rt7x!tP*}ABsY^butZ1$IJi`pLh1OYDqI zC&lV-PmS&!e>JtnU}r)47RnyhgN4YBE6S|3>$6 zA!*|yBn4VEZ+# z87KN;)#q4~<#koo$x_=xm%6;h$dj)ph>jtp2IX_2ob`oU7?t1H=F5r zO2_M^A8E;rHiek!;}M%|=R~v9b1Rm0u41}pJzVAym<>)iQ^TyD61K(RiDZ`AB6n6# zcdX6)K`feY$#8vuph z!L9vs8!;ke)xde$jVvUEeyZ=NuDG5o&#sII8X zU}%hwB87{I?z`(QJG)kk7m?g#>m~dzOw0?MVhbg1xSs2CP4WAFHh=uMPrEOSj*)SS``Mk^&#^F%%^dJCeqrN( z7Q4ho5q>%fb^%#JZyEhgh${qL1^#SMS@P@3Czza!vG%6~7t&ejWW`YsU!Y7Xz}yP2w{74&87@(F zhAove-bR!(Znd8g8^YguvdpV@{`_b&-=5h|=9Oqs{}6v3KbCk!@9!-Q3&?D_C}Og? z?|Z(DVVhs7z+s<(NB&u*qcKC8v^62HD@Hh|P$xX|$MWQ;q;k^RyQeFQ#~kHpu6W6# zI-MD#2b3L<#a+GTAd=CQAD3TZ*xtG?|GbTKZgyOg$=5dus<&NSq!!qcYm)#tzI=Kj&OyX~lV?Iea zyxe@ls!HLqHi|*Z(|};;$wUrqxZGw=7RK*_163>3&Ck~TY)r(k>&n6R+JYLe>&q+= z<}sC@#Z6DUpVU|d4NCqp<2k+jrDjL;Oy<3($v%`g3qN78)Iz}u%^Bq{ydQ>gYNzu1 zZl5RV{sP19+yor_Xd=4w4}Rk(`unH#w8t$EvOD9RR5G>Gj`dG48_A26GN`|uQZ$SxL+b#W4|3&LxxOu9XVXNUK|&do^9=AR6OkbzO3xK-ZA223^6 ziyP0i*Hj%SZ70A6SV&BKhdhWziK_7kZ-NMyApJZ^p^YeQ2;d1!{9+c`%jQ=>2vMyk_9pZpuo0E!;CXZ{DOjmxvJph7I*mo*T7E% zJZXHhpnZ+E5ne3v%X{=T5&7!ycp?%xk@Itn0FV1u1NYDN2K{2Ky~ zzC*#&nkFPNk>B$PuBROBsO?_`j`X{u;NP8ZVQ~vy7S!YU+;et#0L$16*uFbq#|pY& z52{iYF}Y>^JYKI3_6A)4>(|z7eTwGWbsrQy1)uh`cpPaepM9+NWT7hw5_7iSi4P-P zj~NVFj_Eg8xsS>%B$paC=uVHPh28xnmsQ5 zimI-slTNo+SJt2P9|w=N7iZ(@MTq+hYf5U|-k#%T@-FFA>swFuUj`AHn1e`39HG?F z_F?;)Hy_ZV&5l|JuRV2myM~wL4<_u=Vpq0B3s{ zd)!y6ZTIQYpP$CtKyP?_eLi( z!&$O}CCkGN56s)`qIJ;Y^mTHTu?smDwCdNy`?SNX_Y+m55B{i zPiBti7s<{7+$?nmzLhTBa%HK%O7Zw(>gRgKTZlXj&iM7#y5Sk_OLRWMc<@>8&tGj! zG%&m56suGO?S9^3G_S{`{}H~L%PKedmoKI1&xVG8b>sCB4R!tL0zCSsGkv)Nkb?RB zdNwJdF(4zqVbR&NVy~e_@Z*;<`X~lj`m4)EK{xZ)E%|&&l^l6xI6vB5<$_L^nP% zR`~P(z}v=>!3`Tl^1O-Js-qLvQh72-!yaukn$A5}a6B;xt3o7TGe!zVX&P}idM(NP z$k;gwdk$v)!St=ucVxaz8*S&6lXWI=3|>dIX8yhVjqD__K6RH3%pBxwZ1MkjL!yBI z4g7;L@&82!rSpJ13t)N&CpO7|1hN0ZE%4i!N@P0b-XnKGy6>&BX8R4>Ad;#+4{zYH`5pzCMFG#x2dGP zzO|R);B(OXj3bKN{ZVcLMX1mOvT~;20I<%tLjJ^4;;$;J+T=i8>f(a_2gINmp#CnZ z`N#riY+~~03ji@dr1-uqARZ2o6_0j%i;;7Pq&Nk$#^YO~@4no}HLKUt8EE@HX}!@Y zF(KjY`}g}%q-(pL;QK);+6(YU16GO4xEB@VDoKiq2aQI248tN>2T5~AS4Yc0rk4qr zj-`)?xu*EE(3hL+vX#GV$jR<7h66${uw1u1C_u&w#FggfbMbVt&&+dAFi?VL&xI{= z(CM8{eDhQ-PEv~G&Vxe#mYcEdM8K6_v&Bx0LWKHHkPxf;k)B;EIDbkh{kuLe2R(ybLEUvV z^!0~9LIijzE6mRH|0l^*Tbo#>+i*HAP3Fmz@{d;y;e7xHn?HHn3qN8vk?`T+;f4#f zAJ*vH0!>cW!^2Tsn(G^uM@M7-hYcmf#jSC5cB?3u?9Y_>T>lPJQ**hqs{98auP=>- zg@)eiw7hds0*E`{xTvYAad2@r>y;{IH+FWUfguf;T0wSC?(9ArVKn&91c-J58D~_| zF`MAL2?}B1_GL=^hrhEOb0F*$3{rNKf1rA}pF&V6DF_V~19lnUfC2V6{)}=ing4=y zl)>wQXAL-|08-wiLO?}L9RkK1AgjeyRaINyXFwuozciU|FCfSSe>nf|jIqCu2cdva zuo&=jz{@2LLh8KQ+Fdg^#?Y{_`z<+*>vY)ChC%WMhs*A}AJIfRt{6!mt&E%7>3mmZ z#pg`%k-?^S`u9fmB8!9aHVAkinLBQs*xA{E)8G{KYx863lYzk~h(z@G{Sp6qJIx;a z#u@JabA;Y|FioHXGY0*eH@SfS1OOPw*s4?HF9V-E=0vXE9l3+Fns){UEc8L(p#u|v zo+vmQ4Y{~@VrgjzKouY#4MY>9fYTOiYp_U!yb|44eUl$eIjyaN;Du@aXR=yH0Ef1* zsHlGdL27__1!+oRD!3qE65Is{x=tgAz*#S;XkQ1ODwEZAUl7FXm9Ys@MIR@g@dq#m z41(_fq%$XAjaF$f+ATgF z=lb~PdgM3%&ygX1PMP5r_;09*haV-7|L^rFyyB6x|9v;=cqBD=Z2$Yoa0vDPvYJ3P z2<2m@<$qt##g~oymoo_RA!GoJ_wVbwlFv>5H!vhvKL3B@rE?#!$s`>W&g!EV;J`mw 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 71/83] .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 72/83] 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 73/83] 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 74/83] 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 75/83] 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 76/83] 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 77/83] 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 78/83] 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 79/83] 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 80/83] 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 81/83] 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 82/83] 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 83/83] 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}")