diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 8e084c7..b081dc0 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -338,10 +338,15 @@ public sealed partial class DirectExecutionBackend { orbisGen2Result = DispatchBootstrapBridge(); } - else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal)) + else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) || + string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal)) { orbisGen2Result = DispatchKernelDynlibDlsym(); } + else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal)) + { + orbisGen2Result = DispatchIl2CppApiLookupSymbol(); + } else if (importStubEntry.Export is { } cachedExport && (cachedExport.Target & cpuContext.TargetGeneration) != 0) { @@ -1180,8 +1185,11 @@ public sealed partial class DirectExecutionBackend cpuContext[CpuRegister.Rax] = 18446744073709551615uL; return OrbisGen2Result.ORBIS_GEN2_OK; } - if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress)) + if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) && + !TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress)) { + Console.Error.WriteLine( + $"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'"); cpuContext[CpuRegister.Rax] = 18446744073709551615uL; return OrbisGen2Result.ORBIS_GEN2_OK; } @@ -1194,6 +1202,62 @@ public sealed partial class DirectExecutionBackend return OrbisGen2Result.ORBIS_GEN2_OK; } + private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address) + { + address = 0; + var alias = symbolName switch + { + "scriptingGetMem" => "malloc", + "scriptingFreeMem" => "free", + "scriptingRealloc" => "realloc", + "scriptingCalloc" => "calloc", + _ => null, + }; + + return alias != null && TryResolveRuntimeSymbolAddress(alias, out address); + } + + private OrbisGen2Result DispatchIl2CppApiLookupSymbol() + { + var cpuContext = ActiveCpuContext; + if (cpuContext == null) + { + return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var symbolNameAddress = cpuContext[CpuRegister.Rdi]; + var outputAddress = cpuContext[CpuRegister.Rsi]; + if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) || + outputAddress == 0 || + !TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress) || + !TryWriteUInt64Compat(outputAddress, resolvedAddress)) + { + Console.Error.WriteLine( + $"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}"); + if (outputAddress != 0) + { + _ = TryWriteUInt64Compat(outputAddress, 0); + } + + cpuContext[CpuRegister.Rax] = ulong.MaxValue; + return OrbisGen2Result.ORBIS_GEN2_OK; + } + + cpuContext[CpuRegister.Rax] = 0; + return OrbisGen2Result.ORBIS_GEN2_OK; + } + + private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address) + { + if (TryResolveRuntimeSymbolAddress(symbolName, out address)) + { + return true; + } + + return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) && + TryResolveRuntimeSymbolAddress(symbol.Nid, out address); + } + private OrbisGen2Result DispatchBootstrapBridge() { var cpuContext = ActiveCpuContext; diff --git a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs index 1b7601a..eab36fa 100644 --- a/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs +++ b/src/SharpEmu.Core/Runtime/SharpEmuRuntime.cs @@ -543,6 +543,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime { Path.Combine(ebootDirectory, "sce_module"), Path.Combine(ebootDirectory, "sce_modules"), + Path.Combine(ebootDirectory, "Media", "Modules"), } .Distinct(StringComparer.OrdinalIgnoreCase) .Where(Directory.Exists) @@ -554,7 +555,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime } var allModulePaths = moduleDirectories - .SelectMany(Directory.EnumerateFiles) + .SelectMany(directory => Directory + .EnumerateFiles(directory) + .OrderBy(path => path, StringComparer.OrdinalIgnoreCase)) .Where(path => { var extension = Path.GetExtension(path); @@ -562,7 +565,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase); }) .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(path => path, StringComparer.OrdinalIgnoreCase) .ToArray(); var modulePaths = allModulePaths diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 1dfc9aa..e9d24ec 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -1204,6 +1204,51 @@ public static class AgcExports return ReturnPointer(ctx, commandAddress); } + [SysAbiExport( + Nid = "q88lQ+GP5Yk", + ExportName = "sceAgcDcbDrawIndex", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbDrawIndex(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var indexCount = (uint)ctx[CpuRegister.Rsi]; + var indexAddress = ctx[CpuRegister.Rdx]; + var modifier = (uint)ctx[CpuRegister.Rcx]; + + if (commandBufferAddress == 0 || modifier != 0x4000_0000) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var baseCommand) || + !TryWriteUInt32(ctx, baseCommand, Pm4(3, ItIndexBase, 0)) || + !TryWriteUInt32(ctx, baseCommand + 4, (uint)indexAddress) || + !TryWriteUInt32(ctx, baseCommand + 8, (uint)(indexAddress >> 32)) || + !TryWriteUInt32(ctx, baseCommand + 12, Pm4(2, ItIndexBufferSize, 0)) || + !TryWriteUInt32(ctx, baseCommand + 16, indexCount)) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var drawCommand) || + !TryWriteUInt32(ctx, drawCommand, Pm4(5, ItDrawIndex2, 0)) || + !TryWriteUInt32(ctx, drawCommand + 4, indexCount) || + !TryWriteUInt32(ctx, drawCommand + 8, 0) || + !TryWriteUInt32(ctx, drawCommand + 12, 0) || + !TryWriteUInt32(ctx, drawCommand + 16, 0)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc( + $"agc.dcb_draw_index buf=0x{commandBufferAddress:X16} " + + $"base=0x{baseCommand:X16} draw=0x{drawCommand:X16} " + + $"count={indexCount} index=0x{indexAddress:X16}"); + + return ReturnPointer(ctx, drawCommand); + } + [SysAbiExport( Nid = "Yw0jKSqop+E", ExportName = "sceAgcDcbDrawIndexAuto", @@ -2006,6 +2051,88 @@ public static class AgcExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "uJziRsODk1c", + ExportName = "sceAgcDriverGetResourceRegistrationMaxNameLength", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DriverGetResourceRegistrationMaxNameLength(CpuContext ctx) + { + var outAddress = ctx[CpuRegister.Rdi]; + + if (outAddress == 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!TryWriteUInt32(ctx, outAddress, 256)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + + private const uint DefaultAgcOwner = 1; + [SysAbiExport( + Nid = "F0ZXt5q0ZTA", + ExportName = "sceAgcDriverGetDefaultOwner", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DriverGetDefaultOwner(CpuContext ctx) + { + var ownerAddress = ctx[CpuRegister.Rdi]; + + if (ownerAddress == 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!TryWriteUInt32(ctx, ownerAddress, DefaultAgcOwner)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + + [SysAbiExport( + Nid = "W5z4eZrjEas", + ExportName = "sceAgcDriverRegisterResource", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DriverRegisterResource(CpuContext ctx) + { + var resourceAddress = ctx[CpuRegister.Rdi]; + var owner = (uint)ctx[CpuRegister.Rsi]; + var nameAddress = ctx[CpuRegister.Rdx]; + var type = (uint)ctx[CpuRegister.R8]; + var flags = (uint)ctx[CpuRegister.R9]; + + TraceAgc( + $"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " + + $"name=0x{nameAddress:X16} type={type} flags={flags}"); + + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + + [SysAbiExport( + Nid = "-KRzWekV120", + ExportName = "sceAgcDriverUnknown_KRzWekV120", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DriverUnknownKRzWekV120(CpuContext ctx) + { + TraceAgc( + $"agc.driver_unknown_krz rdi=0x{ctx[CpuRegister.Rdi]:X16} " + + $"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " + + $"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}"); + + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + [SysAbiExport( Nid = "h9z6+0hEydk", ExportName = "sceAgcSuspendPoint", diff --git a/src/SharpEmu.Libs/Audio/AudioOutExports.cs b/src/SharpEmu.Libs/Audio/AudioOutExports.cs new file mode 100644 index 0000000..954cfc2 --- /dev/null +++ b/src/SharpEmu.Libs/Audio/AudioOutExports.cs @@ -0,0 +1,92 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Collections.Concurrent; +using System.Threading; + +namespace SharpEmu.Libs.Audio; + +public static class AudioOutExports +{ + private static readonly ConcurrentDictionary Ports = new(); + private static int _nextPortHandle; + + private sealed record PortState(int UserId, int Type, uint BufferLength, uint Frequency, int Format); + + [SysAbiExport( + Nid = "JfEPXVxhFqA", + ExportName = "sceAudioOutInit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAudioOut")] + public static int AudioOutInit(CpuContext ctx) => SetReturn(ctx, 0); + + [SysAbiExport( + Nid = "ekNvsT22rsY", + ExportName = "sceAudioOutOpen", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAudioOut")] + public static int AudioOutOpen(CpuContext ctx) + { + var userId = unchecked((int)ctx[CpuRegister.Rdi]); + var type = unchecked((int)ctx[CpuRegister.Rsi]); + var bufferLength = unchecked((uint)ctx[CpuRegister.Rcx]); + var frequency = unchecked((uint)ctx[CpuRegister.R8]); + var format = unchecked((int)ctx[CpuRegister.R9]); + if (bufferLength == 0 || frequency == 0) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + var handle = Interlocked.Increment(ref _nextPortHandle); + Ports[handle] = new PortState(userId, type, bufferLength, frequency, format); + return SetReturn(ctx, handle); + } + + [SysAbiExport( + Nid = "s1--uE9mBFw", + ExportName = "sceAudioOutClose", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAudioOut")] + public static int AudioOutClose(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + return SetReturn( + ctx, + Ports.TryRemove(handle, out _) + ? 0 + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + [SysAbiExport( + Nid = "QOQtbeDqsT4", + ExportName = "sceAudioOutOutput", + Target = Generation.Gen5, + LibraryName = "libSceAudioOut")] + public static int AudioOutOutput(CpuContext ctx) + { + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "b+uAV89IlxE", + ExportName = "sceAudioOutSetVolume", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAudioOut")] + public static int AudioOutSetVolume(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + return SetReturn( + ctx, + Ports.ContainsKey(handle) + ? 0 + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + private static int SetReturn(CpuContext ctx, int result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)result); + return result; + } +} diff --git a/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs b/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs new file mode 100644 index 0000000..1cfc6fa --- /dev/null +++ b/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs @@ -0,0 +1,77 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; + +namespace SharpEmu.Libs.AvPlayer; + +public static class AvPlayerExports +{ + private const int InvalidParameters = unchecked((int)0x806A0001); + private static readonly object StateGate = new(); + private static readonly HashSet Players = new(); + + [SysAbiExport( + Nid = "aS66RI0gGgo", + ExportName = "sceAvPlayerInit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAvPlayer")] + public static int AvPlayerInit(CpuContext ctx) + { + if (ctx[CpuRegister.Rdi] == 0 || + !KernelMemoryCompatExports.TryAllocateHleData(ctx, 0x40, 16, out var handle)) + { + ctx[CpuRegister.Rax] = 0; + return 0; + } + + lock (StateGate) + { + Players.Add(handle); + } + + ctx[CpuRegister.Rax] = handle; + return unchecked((int)handle); + } + + [SysAbiExport( + Nid = "HD1YKVU26-M", + ExportName = "sceAvPlayerPostInit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAvPlayer")] + public static int AvPlayerPostInit(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + var dataAddress = ctx[CpuRegister.Rsi]; + lock (StateGate) + { + return SetReturn( + ctx, + handle != 0 && dataAddress != 0 && Players.Contains(handle) + ? 0 + : InvalidParameters); + } + } + + [SysAbiExport( + Nid = "NkJwDzKmIlw", + ExportName = "sceAvPlayerClose", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceAvPlayer")] + public static int AvPlayerClose(CpuContext ctx) + { + lock (StateGate) + { + return SetReturn( + ctx, + Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters); + } + } + + private static int SetReturn(CpuContext ctx, int result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)result); + return result; + } +} diff --git a/src/SharpEmu.Libs/CommonDialog/MsgDialogExports.cs b/src/SharpEmu.Libs/CommonDialog/MsgDialogExports.cs new file mode 100644 index 0000000..a8946f5 --- /dev/null +++ b/src/SharpEmu.Libs/CommonDialog/MsgDialogExports.cs @@ -0,0 +1,27 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Threading; + +namespace SharpEmu.Libs.CommonDialog; + +public static class MsgDialogExports +{ + private const int AlreadyInitialized = unchecked((int)0x80B80002); + private static int _initialized; + + [SysAbiExport( + Nid = "lDqxaY1UbEo", + ExportName = "sceMsgDialogInitialize", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceMsgDialog")] + public static int MsgDialogInitialize(CpuContext ctx) + { + var result = Interlocked.Exchange(ref _initialized, 1) == 0 + ? 0 + : AlreadyInitialized; + ctx[CpuRegister.Rax] = unchecked((ulong)result); + return result; + } +} diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 124af76..16769f5 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -103,6 +103,7 @@ public static class KernelMemoryCompatExports private static readonly Dictionary _directAllocations = new(); private static readonly Dictionary _libcAllocations = new(); private static readonly Dictionary _mappedRegions = new(); + private static readonly Dictionary _mappedRegionNames = new(); private static readonly Dictionary _tlsModuleBlocks = new(); private static readonly Dictionary _guestMounts = new(StringComparer.OrdinalIgnoreCase); private static readonly HashSet _tracedStatResults = new(StringComparer.Ordinal); @@ -231,6 +232,25 @@ public static class KernelMemoryCompatExports return true; } + internal static void RegisterReservedVirtualRange(ulong address, ulong length) + { + if (address == 0 || length == 0) + { + return; + } + + lock (_memoryGate) + { + _mappedRegions[address] = new MappedRegion( + address, + length, + Protection: 0, + IsFlexible: false, + IsDirect: false, + DirectStart: 0); + } + } + [SysAbiExport( Nid = "8zTFvBIAIN8", ExportName = "memset", @@ -2435,6 +2455,36 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "hwVSPCmp5tM", + ExportName = "sceKernelCheckedReleaseDirectMemory", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelCheckedReleaseDirectMemory(CpuContext ctx) + { + var start = ctx[CpuRegister.Rdi]; + var length = ctx[CpuRegister.Rsi]; + + if (length == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + lock (_memoryGate) + { + if (!_directAllocations.TryGetValue(start, out var allocation) || + allocation.Length != length) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + _directAllocations.Remove(start); + _nextPhysicalAddress = GetDirectMemoryHighWaterMarkLocked(); + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "L-Q3LEjIbgA", ExportName = "sceKernelMapDirectMemory", @@ -2614,6 +2664,16 @@ public static class KernelMemoryCompatExports return KernelMapNamedFlexibleMemory(ctx); } + [SysAbiExport( + Nid = "4h6F1LLbTiw", + ExportName = "sceKernelMapFlexibleMemoryInternal", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelMapFlexibleMemoryInternal(CpuContext ctx) + { + return KernelMapNamedFlexibleMemory(ctx); + } + [SysAbiExport( Nid = "2SKEx6bSq-4", ExportName = "sceKernelBatchMap", @@ -2667,6 +2727,44 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "DGMG3JshrZU", + ExportName = "sceKernelSetVirtualRangeName", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelSetVirtualRangeName(CpuContext ctx) + { + var address = ctx[CpuRegister.Rdi]; + var length = ctx[CpuRegister.Rsi]; + var nameAddress = ctx[CpuRegister.Rdx]; + if (nameAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (!TryReadCString(ctx, nameAddress, OrbisKernelMaximumNameLength, out var nameBytes)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var name = Encoding.UTF8.GetString(nameBytes); + lock (_memoryGate) + { + if (!TryFindVirtualQueryRegionLocked(address, findNext: false, out var region) || + length > region.Length || + address < region.Address || + length > region.Address + region.Length - address) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + _mappedRegionNames[region.Address] = name; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "rVjRvHJ0X6c", ExportName = "sceKernelVirtualQuery", @@ -2724,11 +2822,16 @@ public static class KernelMemoryCompatExports BinaryPrimitives.WriteInt32LittleEndian(payload[28..32], memoryType); payload[32] = unchecked((byte)stateFlags); - var name = region.IsDirect - ? "direct" - : region.IsFlexible - ? "flexible" - : string.Empty; + string name; + lock (_memoryGate) + { + name = _mappedRegionNames.GetValueOrDefault(region.Address) + ?? (region.IsDirect + ? "direct" + : region.IsFlexible + ? "flexible" + : string.Empty); + } if (!string.IsNullOrEmpty(name)) { var nameBytes = Encoding.ASCII.GetBytes(name); diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index 9ccaaa5..a798599 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -717,6 +717,35 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "FXPWHNk8Of0", + ExportName = "scePthreadAttrGetschedparam", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadAttrGetschedparam(CpuContext ctx) + { + var attrAddress = ctx[CpuRegister.Rdi]; + var schedParamAddress = ctx[CpuRegister.Rsi]; + if (attrAddress == 0 || schedParamAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + PthreadAttrState state; + lock (_stateGate) + { + state = GetOrCreateAttrStateLocked(attrAddress); + } + + if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "DzES9hQF4f4", ExportName = "scePthreadAttrSetschedparam", diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index 167c6c8..d210bc0 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -752,6 +752,7 @@ public static class KernelRuntimeCompatExports _nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length); } + KernelMemoryCompatExports.RegisterReservedVirtualRange(mappedAddress, length); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } diff --git a/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs b/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs new file mode 100644 index 0000000..d755ab2 --- /dev/null +++ b/src/SharpEmu.Libs/Ngs2/Ngs2Exports.cs @@ -0,0 +1,322 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using System.Buffers.Binary; +using System.Threading; + +namespace SharpEmu.Libs.Ngs2; + +public static class Ngs2Exports +{ + private const int OrbisNgs2ErrorInvalidOutAddress = unchecked((int)0x804A0053); + private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230); + private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261); + private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300); + private const ulong HandleStorageSize = 0x20; + private const int RenderBufferInfoSize = 0x18; + private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024; + + private static readonly object StateGate = new(); + private static readonly Dictionary Systems = new(); + private static readonly Dictionary Racks = new(); + private static readonly Dictionary Voices = new(); + private static long _nextUid; + private static long _renderCount; + + private sealed record SystemState(uint Uid); + private sealed record RackState(ulong SystemHandle, uint RackId); + private sealed record VoiceState(ulong RackHandle, uint VoiceIndex); + + [SysAbiExport( + Nid = "mPYgU4oYpuY", + ExportName = "sceNgs2SystemCreateWithAllocator", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNgs2")] + public static int Ngs2SystemCreateWithAllocator(CpuContext ctx) + { + var outHandleAddress = ctx[CpuRegister.Rdx]; + if (outHandleAddress == 0) + { + return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress); + } + + if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) || + !ctx.TryWriteUInt64(outHandleAddress, handle)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + lock (StateGate) + { + Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid))); + } + + return SetReturn(ctx, 0); + } + + [SysAbiExport( + Nid = "u-WrYDaJA3k", + ExportName = "sceNgs2SystemDestroy", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNgs2")] + public static int Ngs2SystemDestroy(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + lock (StateGate) + { + if (!Systems.Remove(handle)) + { + return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle); + } + + var rackHandles = Racks + .Where(pair => pair.Value.SystemHandle == handle) + .Select(pair => pair.Key) + .ToArray(); + foreach (var rackHandle in rackHandles) + { + RemoveRackLocked(rackHandle); + } + } + + return SetReturn(ctx, 0); + } + + [SysAbiExport( + Nid = "U546k6orxQo", + ExportName = "sceNgs2RackCreateWithAllocator", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNgs2")] + public static int Ngs2RackCreateWithAllocator(CpuContext ctx) + { + var systemHandle = ctx[CpuRegister.Rdi]; + var rackId = unchecked((uint)ctx[CpuRegister.Rsi]); + var outHandleAddress = ctx[CpuRegister.R8]; + lock (StateGate) + { + if (!Systems.ContainsKey(systemHandle)) + { + return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle); + } + } + + if (outHandleAddress == 0) + { + return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress); + } + + if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) || + !ctx.TryWriteUInt64(outHandleAddress, handle)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + lock (StateGate) + { + Racks[handle] = new RackState(systemHandle, rackId); + } + + return SetReturn(ctx, 0); + } + + [SysAbiExport( + Nid = "lCqD7oycmIM", + ExportName = "sceNgs2RackDestroy", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNgs2")] + public static int Ngs2RackDestroy(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + lock (StateGate) + { + if (!Racks.ContainsKey(handle)) + { + return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle); + } + + RemoveRackLocked(handle); + } + + return SetReturn(ctx, 0); + } + + [SysAbiExport( + Nid = "MwmHz8pAdAo", + ExportName = "sceNgs2RackGetVoiceHandle", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNgs2")] + public static int Ngs2RackGetVoiceHandle(CpuContext ctx) + { + var rackHandle = ctx[CpuRegister.Rdi]; + var voiceIndex = unchecked((uint)ctx[CpuRegister.Rsi]); + var outHandleAddress = ctx[CpuRegister.Rdx]; + lock (StateGate) + { + if (!Racks.ContainsKey(rackHandle)) + { + return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle); + } + + var existing = Voices.FirstOrDefault( + pair => pair.Value.RackHandle == rackHandle && pair.Value.VoiceIndex == voiceIndex); + if (existing.Key != 0) + { + return ctx.TryWriteUInt64(outHandleAddress, existing.Key) + ? SetReturn(ctx, 0) + : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + } + + if (outHandleAddress == 0) + { + return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress); + } + + if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) || + !ctx.TryWriteUInt64(outHandleAddress, handle)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + lock (StateGate) + { + Voices[handle] = new VoiceState(rackHandle, voiceIndex); + } + + return SetReturn(ctx, 0); + } + + [SysAbiExport( + Nid = "uu94irFOGpA", + ExportName = "sceNgs2VoiceControl", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNgs2")] + public static int Ngs2VoiceControl(CpuContext ctx) + { + lock (StateGate) + { + return SetReturn( + ctx, + Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle); + } + } + + [SysAbiExport( + Nid = "AbYvTOZ8Pts", + ExportName = "sceNgs2VoiceRunCommands", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNgs2")] + public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx); + + [SysAbiExport( + Nid = "i0VnXM-C9fc", + ExportName = "sceNgs2SystemRender", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNgs2")] + public static int Ngs2SystemRender(CpuContext ctx) + { + var systemHandle = ctx[CpuRegister.Rdi]; + var bufferInfoAddress = ctx[CpuRegister.Rsi]; + var bufferInfoCount = unchecked((uint)ctx[CpuRegister.Rdx]); + lock (StateGate) + { + if (!Systems.ContainsKey(systemHandle)) + { + return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle); + } + } + + if (bufferInfoCount != 0 && bufferInfoAddress == 0) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + for (uint i = 0; i < bufferInfoCount; i++) + { + var entryAddress = bufferInfoAddress + (i * RenderBufferInfoSize); + if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) || + !ctx.TryReadUInt64(entryAddress + 8, out var bufferSize)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + if (bufferAddress != 0 && bufferSize != 0) + { + if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + } + } + + var count = Interlocked.Increment(ref _renderCount); + if (ShouldTrace() && (count <= 4 || count % 10_000 == 0)) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}"); + } + + return SetReturn(ctx, 0); + } + + private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle) + { + handle = 0; + if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle)) + { + return false; + } + + Span data = stackalloc byte[(int)HandleStorageSize]; + data.Clear(); + BinaryPrimitives.WriteUInt64LittleEndian(data[0..8], handle); + BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle); + BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1); + BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type); + return ctx.Memory.TryWrite(handle, data); + } + + private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length) + { + Span zeroes = stackalloc byte[4096]; + zeroes.Clear(); + for (ulong offset = 0; offset < length;) + { + var chunkSize = (int)Math.Min((ulong)zeroes.Length, length - offset); + if (!ctx.Memory.TryWrite(address + offset, zeroes[..chunkSize])) + { + return false; + } + + offset += unchecked((uint)chunkSize); + } + + return true; + } + + private static void RemoveRackLocked(ulong rackHandle) + { + Racks.Remove(rackHandle); + foreach (var voiceHandle in Voices + .Where(pair => pair.Value.RackHandle == rackHandle) + .Select(pair => pair.Key) + .ToArray()) + { + Voices.Remove(voiceHandle); + } + } + + private static bool ShouldTrace() => + string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"), + "1", + StringComparison.Ordinal); + + private static int SetReturn(CpuContext ctx, int result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)result); + return result; + } +} diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs index 30f0512..b6ac27f 100644 --- a/src/SharpEmu.Libs/Pad/PadExports.cs +++ b/src/SharpEmu.Libs/Pad/PadExports.cs @@ -160,6 +160,16 @@ public static class PadExports : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } + [SysAbiExport( + Nid = "W2G-yoyMF5U", + ExportName = "scePadSetVibrationMode", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePad")] + public static int PadSetVibrationMode(CpuContext ctx) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK); + } + private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress) { Span data = stackalloc byte[PadDataSize]; diff --git a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs index 28355a0..8b448ac 100644 --- a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs +++ b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs @@ -244,6 +244,36 @@ public static class SaveDataExports } } + private static int _nextTransactionResource; + [SysAbiExport( + Nid = "gjRZNnw0JPE", + ExportName = "sceSaveDataCreateTransactionResource", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceSaveData")] + public static int SaveDataCreateTransactionResource(CpuContext ctx) + { + var userId = unchecked((int)ctx[CpuRegister.Rdi]); + var reserved = ctx[CpuRegister.Rsi]; + var resourceAddress = ctx[CpuRegister.Rdx]; + + if (resourceAddress == 0) + { + return SetReturn(ctx, OrbisSaveDataErrorParameter); + } + + var id = (uint)Interlocked.Increment(ref _nextTransactionResource); + + if (!TryWriteUInt32(ctx, resourceAddress, id)) + { + return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + TraceSaveData( + $"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}"); + + return SetReturn(ctx, 0); + } + private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond) { cond = default;