From a5172fd2c070d3b46d3502071f64e9ef05a7fdf0 Mon Sep 17 00:00:00 2001 From: ParantezTech Date: Sun, 28 Jun 2026 23:45:05 +0300 Subject: [PATCH] [kernel] Guest-thread blocking for pthread_mutex_lock is currently disabled --- .../Kernel/KernelAprCompatExports.cs | 89 +++- .../Kernel/KernelEventFlagCompatExports.cs | 77 ++- .../Kernel/KernelEventQueueCompatExports.cs | 293 ++++++++++- src/SharpEmu.Libs/Kernel/KernelExports.cs | 26 + .../Kernel/KernelMemoryCompatExports.cs | 454 +++++++++++++++++- .../Kernel/KernelPthreadCompatExports.cs | 249 +++++++--- .../KernelPthreadExtendedCompatExports.cs | 78 ++- .../Kernel/KernelPthreadState.cs | 14 +- .../Kernel/KernelRuntimeCompatExports.cs | 71 ++- .../Kernel/KernelSemaphoreCompatExports.cs | 2 +- 10 files changed, 1148 insertions(+), 205 deletions(-) diff --git a/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs index 379f919..a4bd50d 100644 --- a/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelAprCompatExports.cs @@ -11,8 +11,13 @@ namespace SharpEmu.Libs.Kernel; public static class KernelAprCompatExports { - private static readonly ConcurrentDictionary _submittedCommandBuffers = new(); + private static readonly ConcurrentDictionary _submittedCommandBuffers = new(); private static int _nextSubmissionId; + private static int _aprWaitTraceCount; + private static readonly bool _traceApr = + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal); + + private readonly record struct AprSubmission(ulong CommandBuffer, ulong Priority, ulong ResultAddress); [SysAbiExport( Nid = "ASoW5WE-UPo", @@ -37,7 +42,7 @@ public static class KernelAprCompatExports submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); } - _submittedCommandBuffers[submissionId] = commandBuffer; + _submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress); var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer); if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) @@ -67,20 +72,23 @@ public static class KernelAprCompatExports public static int KernelAprWaitCommandBuffer(CpuContext ctx) { var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]); - var priority = ctx[CpuRegister.Rsi]; - var resultAddress = ctx[CpuRegister.Rdx]; + var waitArg1 = ctx[CpuRegister.Rsi]; + var waitArg2 = ctx[CpuRegister.Rdx]; - if (!_submittedCommandBuffers.TryRemove(submissionId, out var commandBuffer)) + if (!_submittedCommandBuffers.TryRemove(submissionId, out var submission)) { + TraceAprWaitFailure(ctx, "wait_missing", submissionId, commandBuffer: 0, waitArg1, waitArg2); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } + var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress); if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress)) { + TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - TraceApr(ctx, "wait", submissionId, commandBuffer, priority, resultAddress); + TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -98,7 +106,7 @@ public static class KernelAprCompatExports } var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); - _submittedCommandBuffers[submissionId] = commandBuffer; + _submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0); var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer); if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) @@ -125,7 +133,7 @@ public static class KernelAprCompatExports } var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); - _submittedCommandBuffers[submissionId] = commandBuffer; + _submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0); var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer); if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) @@ -149,6 +157,27 @@ public static class KernelAprCompatExports return ctx.Memory.TryWrite(resultAddress, result); } + private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress) + { + if (waitArg2 == 0) + { + return submittedResultAddress; + } + + if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF) + { + return submittedResultAddress; + } + + return waitArg2; + } + + private static bool IsAmprCompletionToken(ulong value) + { + var tag = value >> 56; + return tag is 0x0C or 0x10; + } + private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value) { Span buffer = stackalloc byte[sizeof(uint)]; @@ -164,7 +193,7 @@ public static class KernelAprCompatExports ulong priority, ulong aux) { - if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal)) + if (!_traceApr) { return; } @@ -181,4 +210,46 @@ public static class KernelAprCompatExports $"[LOADER][TRACE] apr.{operation}.result: addr=0x{aux:X16} q0=0x{result0:X16} q1=0x{result1:X16}"); } } + + private static void TraceAprWaitFailure( + CpuContext ctx, + string operation, + uint submissionId, + ulong commandBuffer, + ulong priority, + ulong resultAddress) + { + if (!_traceApr) + { + return; + } + + var traceCount = Interlocked.Increment(ref _aprWaitTraceCount); + if (traceCount > 32 && (traceCount & 0x3FF) != 0) + { + return; + } + + var returnRip = 0UL; + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip); + Console.Error.WriteLine( + $"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} " + + $"rsi=0x{priority:X16} rdx=0x{resultAddress:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16} " + + $"r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16} ret=0x{returnRip:X16}"); + TraceReadableQword(ctx, operation, "rsi", priority); + TraceReadableQword(ctx, operation, "rdx", resultAddress); + TraceReadableQword(ctx, operation, "rcx", ctx[CpuRegister.Rcx]); + TraceReadableQword(ctx, operation, "r8", ctx[CpuRegister.R8]); + } + + private static void TraceReadableQword(CpuContext ctx, string operation, string name, ulong address) + { + if (address == 0 || !ctx.TryReadUInt64(address, out var value)) + { + return; + } + + Console.Error.WriteLine( + $"[LOADER][TRACE] apr.{operation}.{name}: addr=0x{address:X16} q0=0x{value:X16}"); + } } diff --git a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs index 877a819..f1455e9 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs @@ -127,6 +127,7 @@ public static class KernelEventFlagCompatExports TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}"); } + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle)); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } @@ -226,7 +227,7 @@ public static class KernelEventFlagCompatExports Monitor.Enter(state.Gate); try { - if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, out var immediateWaitResult)) + if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult)) { return SetReturn(ctx, immediateWaitResult); } @@ -242,7 +243,28 @@ public static class KernelEventFlagCompatExports var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle; var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx); var managedThread = Environment.CurrentManagedThreadId; - var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEventFlag"); + var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK; + var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock( + ctx, + "sceKernelWaitEventFlag", + GetEventFlagWakeKey(handle), + () => (int)blockedWaitResult, + () => + { + if (!TryPrepareBlockedWait( + ctx, + state, + pattern, + waitMode, + resultAddress, + out var preparedResult)) + { + return false; + } + + blockedWaitResult = preparedResult; + return true; + }); TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}"); TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}"); if (!requestedBlock) @@ -270,7 +292,7 @@ public static class KernelEventFlagCompatExports Monitor.Enter(state.Gate); } - if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, out var pumpedWaitResult)) + if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var pumpedWaitResult)) { state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1); releaseWaiter = false; @@ -369,19 +391,21 @@ public static class KernelEventFlagCompatExports } private static bool TryCompleteSatisfiedWait( - CpuContext ctx, - EventFlagState state, - ulong pattern, - uint waitMode, - out OrbisGen2Result result) + CpuContext ctx, + EventFlagState state, + ulong pattern, + uint waitMode, + ulong resultAddress, + out OrbisGen2Result result) { result = OrbisGen2Result.ORBIS_GEN2_OK; + if (!IsSatisfied(state.Bits, pattern, waitMode)) { return false; } - if (!TryWriteResultPattern(ctx, ctx[CpuRegister.Rcx], state.Bits)) + if (!TryWriteResultPattern(ctx, resultAddress, state.Bits)) { result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return true; @@ -391,6 +415,41 @@ public static class KernelEventFlagCompatExports return true; } + private static bool TryPrepareBlockedWait( + CpuContext ctx, + EventFlagState state, + ulong pattern, + uint waitMode, + ulong resultAddress, + out OrbisGen2Result result) + { + lock (state.Gate) + { + result = OrbisGen2Result.ORBIS_GEN2_OK; + if (!IsSatisfied(state.Bits, pattern, waitMode)) + { + return false; + } + + if (!TryWriteResultPattern(ctx, resultAddress, state.Bits)) + { + result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + else + { + ApplyClearMode(state, pattern, waitMode); + } + + state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1); + TraceEventFlag( + $"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}"); + return true; + } + } + + private static string GetEventFlagWakeKey(ulong handle) => + $"event_flag:0x{handle:X16}"; + private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) => address == 0 || ctx.TryWriteUInt64(address, bits); diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index a6e21b8..4d41e6d 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -10,10 +10,14 @@ namespace SharpEmu.Libs.Kernel; public static class KernelEventQueueCompatExports { private const int KernelEventSize = 0x20; + public const short KernelEventFilterGraphics = -14; + public const short KernelEventFilterAmpr = -16; + public const short KernelEventFilterAmprSystem = -17; private static readonly object _eventQueueGate = new(); private static readonly HashSet _eventQueues = new(); private static readonly Dictionary> _pendingEvents = new(); + private static readonly Dictionary> _registeredEvents = new(); private static long _nextEventQueueHandle = 1; public readonly record struct KernelQueuedEvent( @@ -24,6 +28,11 @@ public static class KernelEventQueueCompatExports ulong Data, ulong UserData); + private readonly record struct KernelEventRegistration( + ulong Ident, + short Filter, + ulong UserData); + [SysAbiExport( Nid = "D0OdFMjp46I", ExportName = "sceKernelCreateEqueue", @@ -42,6 +51,7 @@ public static class KernelEventQueueCompatExports { _eventQueues.Add(handle); _pendingEvents[handle] = new LinkedList(); + _registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>(); } if (!ctx.TryWriteUInt64(outAddress, handle)) @@ -65,6 +75,7 @@ public static class KernelEventQueueCompatExports { _eventQueues.Remove(handle); _pendingEvents.Remove(handle); + _registeredEvents.Remove(handle); } TraceEventQueue(ctx, "delete", handle); @@ -122,8 +133,16 @@ public static class KernelEventQueueCompatExports LibraryName = "libKernel")] public static int KernelAddAmprEvent(CpuContext ctx) { - TraceEventQueue(ctx, "add_ampr", ctx[CpuRegister.Rdi]); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + var handle = ctx[CpuRegister.Rdi]; + var registered = RegisterEvent( + handle, + unchecked((uint)ctx[CpuRegister.Rsi]), + KernelEventFilterAmpr, + ctx[CpuRegister.Rdx]); + TraceEventQueue(ctx, "add_ampr", handle); + return registered + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } [SysAbiExport( @@ -133,8 +152,16 @@ public static class KernelEventQueueCompatExports LibraryName = "libKernel")] public static int KernelAddAmprSystemEvent(CpuContext ctx) { - TraceEventQueue(ctx, "add_ampr_system", ctx[CpuRegister.Rdi]); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + var handle = ctx[CpuRegister.Rdi]; + var registered = RegisterEvent( + handle, + unchecked((uint)ctx[CpuRegister.Rsi]), + KernelEventFilterAmprSystem, + ctx[CpuRegister.Rdx]); + TraceEventQueue(ctx, "add_ampr_system", handle); + return registered + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } [SysAbiExport( @@ -144,8 +171,15 @@ public static class KernelEventQueueCompatExports LibraryName = "libKernel")] public static int KernelDeleteAmprEvent(CpuContext ctx) { - TraceEventQueue(ctx, "delete_ampr", ctx[CpuRegister.Rdi]); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + var handle = ctx[CpuRegister.Rdi]; + var deleted = DeleteRegisteredEvent( + handle, + unchecked((uint)ctx[CpuRegister.Rsi]), + KernelEventFilterAmpr); + TraceEventQueue(ctx, "delete_ampr", handle); + return deleted + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } [SysAbiExport( @@ -155,8 +189,15 @@ public static class KernelEventQueueCompatExports LibraryName = "libKernel")] public static int KernelDeleteAmprSystemEvent(CpuContext ctx) { - TraceEventQueue(ctx, "delete_ampr_system", ctx[CpuRegister.Rdi]); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + var handle = ctx[CpuRegister.Rdi]; + var deleted = DeleteRegisteredEvent( + handle, + unchecked((uint)ctx[CpuRegister.Rsi]), + KernelEventFilterAmprSystem); + TraceEventQueue(ctx, "delete_ampr_system", handle); + return deleted + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } [SysAbiExport( @@ -171,6 +212,57 @@ public static class KernelEventQueueCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "vz+pg2zdopI", + ExportName = "sceKernelGetEventUserData", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelGetEventUserData(CpuContext ctx) + { + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x18, out var userData); + ctx[CpuRegister.Rax] = userData; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "mJ7aghmgvfc", + ExportName = "sceKernelGetEventId", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelGetEventId(CpuContext ctx) + { + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi], out var ident); + ctx[CpuRegister.Rax] = ident; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "23CPPI1tyBY", + ExportName = "sceKernelGetEventFilter", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelGetEventFilter(CpuContext ctx) + { + Span filterBytes = stackalloc byte[sizeof(short)]; + var filter = ctx.Memory.TryRead(ctx[CpuRegister.Rdi] + 0x08, filterBytes) + ? BinaryPrimitives.ReadInt16LittleEndian(filterBytes) + : (short)0; + ctx[CpuRegister.Rax] = unchecked((uint)filter); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "kwGyyjohI50", + ExportName = "sceKernelGetEventData", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelGetEventData(CpuContext ctx) + { + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x10, out var data); + ctx[CpuRegister.Rax] = data; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "fzyMKs9kim0", ExportName = "sceKernelWaitEqueue", @@ -196,7 +288,13 @@ public static class KernelEventQueueCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue")) + if (timeoutAddress == 0 && + GuestThreadExecution.RequestCurrentThreadBlock( + ctx, + "sceKernelWaitEqueue", + GetEventQueueWakeKey(handle), + () => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress), + () => HasPendingEvents(handle))) { TraceEventQueue(ctx, "wait-block", handle); return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -216,6 +314,7 @@ public static class KernelEventQueueCompatExports public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent) { + var queued = false; lock (_eventQueueGate) { if (!_eventQueues.Contains(handle)) @@ -230,10 +329,100 @@ public static class KernelEventQueueCompatExports } queue.AddLast(queuedEvent); + queued = true; + } + + if (queued) + { + WakeEventQueue(handle); + } + + return queued; + } + + public static bool RegisterEvent( + ulong handle, + ulong ident, + short filter, + ulong userData) + { + lock (_eventQueueGate) + { + if (!_eventQueues.Contains(handle)) + { + return false; + } + + if (!_registeredEvents.TryGetValue(handle, out var events)) + { + events = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>(); + _registeredEvents[handle] = events; + } + + events[(ident, filter)] = new KernelEventRegistration(ident, filter, userData); return true; } } + public static bool DeleteRegisteredEvent( + ulong handle, + ulong ident, + short filter) + { + lock (_eventQueueGate) + { + return _registeredEvents.TryGetValue(handle, out var events) && + events.Remove((ident, filter)); + } + } + + public static int TriggerRegisteredEvents( + ulong ident, + short filter, + ulong data) + { + List? wakeHandles = null; + var triggeredCount = 0; + lock (_eventQueueGate) + { + foreach (var (handle, registrations) in _registeredEvents) + { + if (!registrations.TryGetValue((ident, filter), out var registration)) + { + continue; + } + + if (!_pendingEvents.TryGetValue(handle, out var queue)) + { + queue = new LinkedList(); + _pendingEvents[handle] = queue; + } + + QueueOrUpdateEvent( + queue, + new KernelQueuedEvent( + registration.Ident, + registration.Filter, + 0, + 1, + data, + registration.UserData)); + (wakeHandles ??= new List()).Add(handle); + triggeredCount++; + } + } + + if (wakeHandles is not null) + { + foreach (var handle in wakeHandles) + { + WakeEventQueue(handle); + } + } + + return triggeredCount; + } + public static bool TriggerDisplayEvent( ulong handle, ulong ident, @@ -241,6 +430,7 @@ public static class KernelEventQueueCompatExports ulong eventHint, ulong userData) { + var triggered = false; lock (_eventQueueGate) { if (!_eventQueues.Contains(handle)) @@ -254,17 +444,8 @@ public static class KernelEventQueueCompatExports _pendingEvents[handle] = events; } - LinkedListNode? pendingNode = null; - for (var node = events.First; node is not null; node = node.Next) - { - if (node.Value.Ident == ident && node.Value.Filter == filter) - { - pendingNode = node; - break; - } - } - var count = 1UL; + var pendingNode = FindPendingEvent(events, ident, filter); if (pendingNode is not null) { count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL); @@ -289,8 +470,80 @@ public static class KernelEventQueueCompatExports events.AddLast(triggeredEvent); } - return true; + triggered = true; } + + if (triggered) + { + WakeEventQueue(handle); + } + + return triggered; + } + + private static int ResumeWaitEqueue( + CpuContext ctx, + ulong handle, + ulong eventsAddress, + int eventCapacity, + ulong outCountAddress) + { + var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity); + if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static bool HasPendingEvents(ulong handle) + { + lock (_eventQueueGate) + { + return _pendingEvents.TryGetValue(handle, out var events) && events.Count != 0; + } + } + + private static void QueueOrUpdateEvent( + LinkedList queue, + KernelQueuedEvent queuedEvent) + { + var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter); + if (pendingNode is null) + { + queue.AddLast(queuedEvent); + return; + } + + pendingNode.Value = queuedEvent with + { + Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags), + }; + } + + private static LinkedListNode? FindPendingEvent( + LinkedList queue, + ulong ident, + short filter) + { + for (var node = queue.First; node is not null; node = node.Next) + { + if (node.Value.Ident == ident && node.Value.Filter == filter) + { + return node; + } + } + + return null; + } + + private static string GetEventQueueWakeKey(ulong handle) => + $"sceKernelWaitEqueue:{handle:X16}"; + + private static void WakeEventQueue(ulong handle) + { + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventQueueWakeKey(handle)); } private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity) diff --git a/src/SharpEmu.Libs/Kernel/KernelExports.cs b/src/SharpEmu.Libs/Kernel/KernelExports.cs index 663ebb0..ab66074 100644 --- a/src/SharpEmu.Libs/Kernel/KernelExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelExports.cs @@ -252,6 +252,32 @@ public static class KernelExports return PthreadCreate(ctx); } + [SysAbiExport( + Nid = "3kg7rT0NQIs", + ExportName = "scePthreadExit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadExit(CpuContext ctx) + { + var value = ctx[CpuRegister.Rdi]; + GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value); + ctx[CpuRegister.Rax] = value; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "FJrT5LuUBAU", + ExportName = "pthread_exit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePosix")] + public static int PosixPthreadExit(CpuContext ctx) + { + var value = ctx[CpuRegister.Rdi]; + GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value); + ctx[CpuRegister.Rax] = value; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + [SysAbiExport( Nid = "onNY9Byn-W8", ExportName = "scePthreadJoin", diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 9233877..8bec571 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -65,6 +65,7 @@ public static class KernelMemoryCompatExports private const uint HostPageExecuteWriteCopy = 0x80; private const uint HostPageGuard = 0x100; private const int Enomem = 12; + private const int Efault = 14; private const int Einval = 22; private const int Erange = 34; private const int Struncate = 80; @@ -96,10 +97,14 @@ public static class KernelMemoryCompatExports private static readonly object _libcAllocGate = new(); private static readonly object _memoryGate = new(); private static readonly object _tlsGate = new(); + private static readonly object _ioTraceGate = new(); + private static readonly object _statCacheGate = new(); private static readonly Dictionary _directAllocations = new(); private static readonly Dictionary _libcAllocations = new(); private static readonly Dictionary _mappedRegions = new(); private static readonly Dictionary _tlsModuleBlocks = new(); + private static readonly HashSet _tracedStatResults = new(StringComparer.Ordinal); + private static readonly HashSet _negativeStatCache = new(StringComparer.OrdinalIgnoreCase); private static long _nextFileDescriptor = 2; private static ulong _nextPhysicalAddress; private static ulong _nextVirtualAddress; @@ -114,6 +119,8 @@ public static class KernelMemoryCompatExports private static int _hostMemoryWriteFallbackCount; private static int _hostMemoryReadFallbackCount; private static int _nullWcscpyRecoveryCount; + private static string? _cachedApp0Root; + private static string? _cachedDownload0Root; [StructLayout(LayoutKind.Sequential)] private struct MemoryBasicInformation @@ -1236,6 +1243,12 @@ public static class KernelMemoryCompatExports var mode = ResolveOpenMode(flags, access); try { + if (IsMutatingOpen(flags) && IsReadOnlyGuestMutationPath(guestPath)) + { + LogOpenTrace($"_open readonly path='{guestPath}' host='{hostPath}' flags=0x{flags:X8}"); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + var wantsDirectory = (flags & O_DIRECTORY) != 0; if (wantsDirectory || Directory.Exists(hostPath)) { @@ -1280,6 +1293,11 @@ public static class KernelMemoryCompatExports _openFiles[fd] = stream; } + if (IsMutatingOpen(flags)) + { + InvalidateNegativeStatCacheForPathAndAncestors(guestPath); + } + LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}"); ctx[CpuRegister.Rax] = unchecked((ulong)fd); return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -1334,11 +1352,30 @@ public static class KernelMemoryCompatExports } var hostPath = ResolveGuestPath(guestPath); - if (!TryWriteHostPathStat(ctx, statAddress, hostPath)) + var statCacheKey = GetNegativeStatCacheKey(guestPath); + if (statCacheKey is not null && IsNegativeStatCached(statCacheKey)) { + LogUniqueStatTrace(guestPath, hostPath, found: false); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } + if (!TryWriteHostPathStat(ctx, statAddress, hostPath)) + { + if (statCacheKey is not null) + { + AddNegativeStatCache(statCacheKey); + } + + LogUniqueStatTrace(guestPath, hostPath, found: false); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + if (statCacheKey is not null) + { + RemoveNegativeStatCache(statCacheKey); + } + + LogUniqueStatTrace(guestPath, hostPath, found: true); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -1356,13 +1393,22 @@ public static class KernelMemoryCompatExports var sizesAddress = ctx[CpuRegister.Rcx]; if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024) { + KernelRuntimeCompatExports.TrySetErrno(ctx, Einval); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } for (ulong i = 0; i < count; i++) { + if (idsAddress != 0 && + !TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue)) + { + KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath)) { + KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -1370,6 +1416,7 @@ public static class KernelMemoryCompatExports if (!TryGetAprFileSize(hostPath, out var fileSize)) { LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found"); + KernelRuntimeCompatExports.TrySetErrno(ctx, 2); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } @@ -1379,11 +1426,13 @@ public static class KernelMemoryCompatExports if (idsAddress != 0 && !TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId)) { + KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize)) { + KernelRuntimeCompatExports.TrySetErrno(ctx, Efault); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } } @@ -1415,6 +1464,170 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "AUXVxWeJU-A", + ExportName = "sceKernelUnlink", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelUnlink(CpuContext ctx) + { + var pathAddress = ctx[CpuRegister.Rdi]; + if (pathAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + var hostPath = ResolveGuestPath(guestPath); + if (IsReadOnlyGuestMutationPath(guestPath)) + { + LogOpenTrace($"unlink readonly path='{guestPath}' host='{hostPath}'"); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + + try + { + if (Directory.Exists(hostPath)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + + if (!File.Exists(hostPath)) + { + AddNegativeStatCacheForGuestPath(guestPath); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + File.Delete(hostPath); + InvalidateNegativeStatCacheForPathAndAncestors(guestPath); + AddNegativeStatCacheForGuestPath(guestPath); + LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + catch (UnauthorizedAccessException) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + catch (IOException) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + } + + [SysAbiExport( + Nid = "1-LFLmRFxxM", + ExportName = "sceKernelMkdir", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelMkdir(CpuContext ctx) + { + var pathAddress = ctx[CpuRegister.Rdi]; + if (pathAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + var hostPath = ResolveGuestPath(guestPath); + if (IsReadOnlyGuestMutationPath(guestPath)) + { + LogOpenTrace($"mkdir readonly path='{guestPath}' host='{hostPath}'"); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + + try + { + if (File.Exists(hostPath) || Directory.Exists(hostPath)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS; + } + + var parentDirectory = Path.GetDirectoryName(hostPath); + if (string.IsNullOrWhiteSpace(parentDirectory) || !Directory.Exists(parentDirectory)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + Directory.CreateDirectory(hostPath); + if (!Directory.Exists(hostPath)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + InvalidateNegativeStatCacheForPathAndAncestors(guestPath); + LogOpenTrace($"mkdir path='{guestPath}' host='{hostPath}'"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + catch (UnauthorizedAccessException) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + catch (IOException) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + } + + [SysAbiExport( + Nid = "naInUjYt3so", + ExportName = "sceKernelRmdir", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int KernelRmdir(CpuContext ctx) + { + var pathAddress = ctx[CpuRegister.Rdi]; + if (pathAddress == 0) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + var hostPath = ResolveGuestPath(guestPath); + if (IsReadOnlyGuestMutationPath(guestPath)) + { + LogOpenTrace($"rmdir readonly path='{guestPath}' host='{hostPath}'"); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + + try + { + if (!Directory.Exists(hostPath)) + { + AddNegativeStatCacheForGuestPath(guestPath); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + Directory.Delete(hostPath, recursive: false); + InvalidateNegativeStatCacheForPathAndAncestors(guestPath); + AddNegativeStatCacheForGuestPath(guestPath); + LogOpenTrace($"rmdir path='{guestPath}' host='{hostPath}'"); + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + catch (UnauthorizedAccessException) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + } + catch (IOException) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + } + private static int KernelCloseCore(CpuContext ctx, int fd) { if (fd is 0 or 1 or 2) @@ -3798,57 +4011,72 @@ public static class KernelMemoryCompatExports return guestPath; } - var devlogAppRoot = ResolveDevlogAppRoot(); if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase)) { var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]); - return Path.Combine(devlogAppRoot, relative); + return Path.Combine(ResolveDevlogAppRoot(), relative); } if (guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase)) { var relative = NormalizeMountRelativePath(guestPath["devlog/app/".Length..]); - return Path.Combine(devlogAppRoot, relative); + return Path.Combine(ResolveDevlogAppRoot(), relative); } if (string.Equals(guestPath, "/devlog/app", StringComparison.OrdinalIgnoreCase) || string.Equals(guestPath, "devlog/app", StringComparison.OrdinalIgnoreCase)) { - return devlogAppRoot; + return ResolveDevlogAppRoot(); } - var temp0Root = ResolveTemp0Root(); if (guestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase)) { var relative = NormalizeMountRelativePath(guestPath["/temp0/".Length..]); - return Path.Combine(temp0Root, relative); + return Path.Combine(ResolveTemp0Root(), relative); } if (string.Equals(guestPath, "/temp0", StringComparison.OrdinalIgnoreCase)) { - return temp0Root; + return ResolveTemp0Root(); + } + + if (guestPath.StartsWith("/download0/", StringComparison.OrdinalIgnoreCase)) + { + var relative = NormalizeMountRelativePath(guestPath["/download0/".Length..]); + return Path.Combine(ResolveDownload0Root(), relative); + } + + if (guestPath.StartsWith("download0/", StringComparison.OrdinalIgnoreCase)) + { + var relative = NormalizeMountRelativePath(guestPath["download0/".Length..]); + return Path.Combine(ResolveDownload0Root(), relative); + } + + if (string.Equals(guestPath, "/download0", StringComparison.OrdinalIgnoreCase) || + string.Equals(guestPath, "download0", StringComparison.OrdinalIgnoreCase)) + { + return ResolveDownload0Root(); } - var hostappRoot = ResolveHostappRoot(); if (guestPath.StartsWith("/hostapp/", StringComparison.OrdinalIgnoreCase)) { var relative = NormalizeMountRelativePath(guestPath["/hostapp/".Length..]); - return Path.Combine(hostappRoot, relative); + return Path.Combine(ResolveHostappRoot(), relative); } if (guestPath.StartsWith("hostapp/", StringComparison.OrdinalIgnoreCase)) { var relative = NormalizeMountRelativePath(guestPath["hostapp/".Length..]); - return Path.Combine(hostappRoot, relative); + return Path.Combine(ResolveHostappRoot(), relative); } if (string.Equals(guestPath, "/hostapp", StringComparison.OrdinalIgnoreCase) || string.Equals(guestPath, "hostapp", StringComparison.OrdinalIgnoreCase)) { - return hostappRoot; + return ResolveHostappRoot(); } - var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); + var app0Root = ResolveApp0Root(); if (!string.IsNullOrWhiteSpace(app0Root)) { if (string.Equals(guestPath, "/app0", StringComparison.OrdinalIgnoreCase) || @@ -3881,6 +4109,24 @@ public static class KernelMemoryCompatExports return guestPath; } + private static string? ResolveApp0Root() + { + var cached = Volatile.Read(ref _cachedApp0Root); + if (!string.IsNullOrWhiteSpace(cached)) + { + return cached; + } + + var configured = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); + if (string.IsNullOrWhiteSpace(configured)) + { + return null; + } + + Interlocked.CompareExchange(ref _cachedApp0Root, configured, null); + return _cachedApp0Root; + } + private static string NormalizeMountRelativePath(string relativePath) { return relativePath @@ -3931,6 +4177,32 @@ public static class KernelMemoryCompatExports return root; } + private static string ResolveDownload0Root() + { + var cached = Volatile.Read(ref _cachedDownload0Root); + if (!string.IsNullOrWhiteSpace(cached)) + { + return cached; + } + + const string download0VariableName = "SHARPEMU_DOWNLOAD0_DIR"; + var configuredRoot = Environment.GetEnvironmentVariable(download0VariableName); + string root; + if (!string.IsNullOrWhiteSpace(configuredRoot)) + { + root = Path.GetFullPath(configuredRoot); + } + else + { + root = Path.Combine(GetPerAppWritableRoot(), "download0"); + Environment.SetEnvironmentVariable(download0VariableName, root); + } + + Directory.CreateDirectory(root); + Interlocked.CompareExchange(ref _cachedDownload0Root, root, null); + return _cachedDownload0Root; + } + private static string ResolveHostappRoot() { const string hostappVariableName = "SHARPEMU_HOSTAPP_DIR"; @@ -3950,6 +4222,22 @@ public static class KernelMemoryCompatExports return root; } + private static string GetPerAppWritableRoot() + { + var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); + var appName = string.IsNullOrWhiteSpace(app0Root) + ? "default" + : Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root)); + if (string.IsNullOrWhiteSpace(appName)) + { + appName = "default"; + } + + var invalidChars = Path.GetInvalidFileNameChars(); + appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()); + return Path.Combine(Path.GetTempPath(), "SharpEmu", appName); + } + private static void EnsureOpenParentDirectoryExists(string guestPath, string hostPath, int flags) { if (string.IsNullOrWhiteSpace(hostPath)) @@ -3973,6 +4261,17 @@ public static class KernelMemoryCompatExports } } + private static bool IsMutatingOpen(int flags) => + (flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0; + + private static bool IsReadOnlyGuestMutationPath(string guestPath) + { + var normalized = NormalizeGuestStatCachePath(guestPath); + return normalized is not null && + (string.Equals(normalized, "/app0", StringComparison.OrdinalIgnoreCase) || + normalized.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase)); + } + private static bool TryReadCString(CpuContext ctx, ulong address, ulong maxLength, out byte[] bytes) { return TryReadBytesUntilNull(ctx, address, maxLength, 1_048_576, out bytes); @@ -4350,7 +4649,7 @@ public static class KernelMemoryCompatExports return true; } - private static bool TryReadUInt64Compat(CpuContext ctx, ulong address, out ulong value) + internal static bool TryReadUInt64Compat(CpuContext ctx, ulong address, out ulong value) { Span bytes = stackalloc byte[sizeof(ulong)]; if (!TryReadCompat(ctx, address, bytes)) @@ -4397,7 +4696,7 @@ public static class KernelMemoryCompatExports return TryWriteCompat(ctx, address, bytes); } - private static bool TryWriteUInt64Compat(CpuContext ctx, ulong address, ulong value) + internal static bool TryWriteUInt64Compat(CpuContext ctx, ulong address, ulong value) { Span bytes = stackalloc byte[sizeof(ulong)]; BinaryPrimitives.WriteUInt64LittleEndian(bytes, value); @@ -5345,19 +5644,20 @@ public static class KernelMemoryCompatExports size = 0; try { - if (Directory.Exists(hostPath)) + var fileInfo = new FileInfo(hostPath); + if (fileInfo.Exists) { - size = 65536; + var length = fileInfo.Length; + size = length < 0 ? 0UL : unchecked((ulong)length); return true; } - if (!File.Exists(hostPath)) + if (!new DirectoryInfo(hostPath).Exists) { return false; } - var length = new FileInfo(hostPath).Length; - size = length < 0 ? 0UL : unchecked((ulong)length); + size = 65536; return true; } catch @@ -5542,6 +5842,120 @@ public static class KernelMemoryCompatExports Console.Error.WriteLine($"[LOADER][TRACE] {operation} path='{path}' {detail}"); } + private static void LogUniqueStatTrace(string guestPath, string hostPath, bool found) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IO"), "1", StringComparison.Ordinal)) + { + return; + } + + var result = found ? "found" : "not_found"; + lock (_ioTraceGate) + { + if (!_tracedStatResults.Add($"{result}\0{guestPath}")) + { + return; + } + } + + LogIoTrace("stat", guestPath, $"host='{hostPath}' result={result}"); + } + + private static string? GetNegativeStatCacheKey(string guestPath) + { + var normalized = NormalizeGuestStatCachePath(guestPath); + return IsReadOnlyGuestStatPath(normalized) ? normalized : null; + } + + private static string? NormalizeGuestStatCachePath(string guestPath) + { + var normalized = guestPath.Replace('\\', '/').TrimEnd('/'); + if (normalized.Length == 0) + { + return null; + } + + if (normalized[0] != '/') + { + normalized = "/" + normalized; + } + + return normalized; + } + + private static bool IsReadOnlyGuestStatPath(string? normalizedGuestPath) => + normalizedGuestPath is not null && + (string.Equals(normalizedGuestPath, "/app0", StringComparison.OrdinalIgnoreCase) || + normalizedGuestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedGuestPath, "/hostapp", StringComparison.OrdinalIgnoreCase) || + normalizedGuestPath.StartsWith("/hostapp/", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedGuestPath, "/devlog/app", StringComparison.OrdinalIgnoreCase) || + normalizedGuestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedGuestPath, "/temp0", StringComparison.OrdinalIgnoreCase) || + normalizedGuestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalizedGuestPath, "/download0", StringComparison.OrdinalIgnoreCase) || + normalizedGuestPath.StartsWith("/download0/", StringComparison.OrdinalIgnoreCase)); + + private static bool IsNegativeStatCached(string cacheKey) + { + lock (_statCacheGate) + { + return _negativeStatCache.Contains(cacheKey); + } + } + + private static void AddNegativeStatCache(string cacheKey) + { + lock (_statCacheGate) + { + _negativeStatCache.Add(cacheKey); + } + } + + private static void RemoveNegativeStatCache(string cacheKey) + { + lock (_statCacheGate) + { + _negativeStatCache.Remove(cacheKey); + } + } + + private static void AddNegativeStatCacheForGuestPath(string guestPath) + { + var cacheKey = GetNegativeStatCacheKey(guestPath); + if (cacheKey is not null) + { + AddNegativeStatCache(cacheKey); + } + } + + private static void InvalidateNegativeStatCacheForPathAndAncestors(string guestPath) + { + var normalized = NormalizeGuestStatCachePath(guestPath); + if (normalized is null) + { + return; + } + + lock (_statCacheGate) + { + var current = normalized; + while (true) + { + _negativeStatCache.Remove(current); + var slash = current.LastIndexOf('/'); + if (slash <= 0) + { + break; + } + + current = current[..slash]; + } + + _negativeStatCache.Remove("/"); + } + } + private static string PreviewIoBytes(byte[] buffer, int count, int maxBytes) { if (count <= 0) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 803d47f..d6a5349 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -1,6 +1,7 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.Collections.Concurrent; using SharpEmu.HLE; using System.Buffers.Binary; using System.Threading; @@ -10,7 +11,7 @@ namespace SharpEmu.Libs.Kernel; public static class KernelPthreadCompatExports { - private const int MutexTypeDefault = 1; + private const int MutexTypeDefault = 0; private const int MutexTypeErrorCheck = 1; private const int MutexTypeRecursive = 2; private const int MutexTypeNormal = 3; @@ -25,11 +26,18 @@ public static class KernelPthreadCompatExports private const int PthreadOnceDone = 2; private static readonly object _stateGate = new(); - private static readonly Dictionary _mutexStates = new(); + private static readonly ConcurrentDictionary _mutexStates = new(); private static readonly Dictionary _mutexAttrStates = new(); private static readonly Dictionary _condStates = new(); private static readonly Dictionary _onceGates = new(); private static readonly HashSet _condAttrStates = new(); + private static readonly bool _tracePthreads = + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal); + private static readonly bool _tracePthreadConds = + _tracePthreads || + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal); + private static readonly HashSet? _tracePthreadMutexFilter = ParseTraceAddressFilter( + Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER")); private sealed class PthreadMutexState { @@ -40,6 +48,12 @@ public static class KernelPthreadCompatExports public int Protocol { get; set; } } + private sealed class PthreadMutexWaiter + { + public required ulong ThreadId { get; init; } + public int Reserved; + } + private sealed class PthreadCondState { public object SyncRoot { get; } = new(); @@ -463,19 +477,13 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - lock (_stateGate) - { - _mutexStates[mutexAddress] = state; - _mutexStates[handle] = state; - } + _mutexStates[mutexAddress] = state; + _mutexStates[handle] = state; - if (!ctx.TryWriteUInt64(mutexAddress, handle)) + if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, handle)) { - lock (_stateGate) - { - _mutexStates.Remove(mutexAddress); - _mutexStates.Remove(handle); - } + _mutexStates.TryRemove(mutexAddress, out _); + _mutexStates.TryRemove(handle, out _); state.Semaphore.Dispose(); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; @@ -492,15 +500,10 @@ public static class KernelPthreadCompatExports } var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress); - PthreadMutexState? state; - lock (_stateGate) + _mutexStates.TryRemove(resolvedAddress, out var state); + if (resolvedAddress != mutexAddress) { - _mutexStates.TryGetValue(resolvedAddress, out state); - _mutexStates.Remove(resolvedAddress); - if (resolvedAddress != mutexAddress) - { - _mutexStates.Remove(mutexAddress); - } + _mutexStates.TryRemove(mutexAddress, out _); } if (state is null) @@ -508,7 +511,7 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } - _ = ctx.TryWriteUInt64(mutexAddress, 0); + _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0); state.Semaphore.Dispose(); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -538,7 +541,7 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp) + if (state.Type is MutexTypeDefault or MutexTypeNormal or MutexTypeAdaptiveNp) { if (tryOnly) { @@ -563,15 +566,33 @@ public static class KernelPthreadCompatExports } } - var acquired = true; - if (tryOnly) + var acquired = state.Semaphore.Wait(0); + if (!acquired) { - acquired = state.Semaphore.Wait(0); - } - else - { - state.Semaphore.Wait(); + // Guest-thread blocking for pthread_mutex_lock is currently disabled. + // Demon's Souls deadlocks during PS5SyncEvent initialization. + /* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId }; + if (!tryOnly && + GuestThreadExecution.IsGuestThread && + GuestThreadExecution.TryGetCurrentImportCallFrame(out _) && + GuestThreadExecution.RequestCurrentThreadBlock( + ctx, + "pthread_mutex_lock", + GetMutexWakeKey(resolvedAddress), + () => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter), + () => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter))) + { + TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } */ + + if (!tryOnly) + { + state.Semaphore.Wait(); + acquired = true; + } } + if (!acquired) { TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); @@ -630,6 +651,7 @@ public static class KernelPthreadCompatExports try { state.Semaphore.Release(); + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1); } catch (SemaphoreFullException) { @@ -666,7 +688,7 @@ public static class KernelPthreadCompatExports _mutexAttrStates[handle] = initialState; } - if (!ctx.TryWriteUInt64(attrAddress, handle)) + if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, handle)) { lock (_stateGate) { @@ -765,22 +787,16 @@ public static class KernelPthreadCompatExports return 0; } - lock (_stateGate) + if (_mutexStates.ContainsKey(mutexAddress)) { - if (_mutexStates.ContainsKey(mutexAddress)) - { - return mutexAddress; - } + return mutexAddress; } - if (ctx.TryReadUInt64(mutexAddress, out var pointedHandle) && pointedHandle != 0) + if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle) && pointedHandle != 0) { - lock (_stateGate) + if (_mutexStates.ContainsKey(pointedHandle)) { - if (_mutexStates.ContainsKey(pointedHandle)) - { - return pointedHandle; - } + return pointedHandle; } } @@ -796,16 +812,13 @@ public static class KernelPthreadCompatExports return false; } - lock (_stateGate) + if (_mutexStates.TryGetValue(mutexAddress, out state)) { - if (_mutexStates.TryGetValue(mutexAddress, out state)) - { - resolvedAddress = mutexAddress; - return true; - } + resolvedAddress = mutexAddress; + return true; } - if (!ctx.TryReadUInt64(mutexAddress, out var pointedHandle)) + if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle)) { return false; } @@ -817,14 +830,11 @@ public static class KernelPthreadCompatExports if (pointedHandle != 0) { - lock (_stateGate) + if (_mutexStates.TryGetValue(pointedHandle, out state)) { - if (_mutexStates.TryGetValue(pointedHandle, out state)) - { - _mutexStates[mutexAddress] = state; - resolvedAddress = pointedHandle; - return true; - } + _mutexStates.TryAdd(mutexAddress, state); + resolvedAddress = pointedHandle; + return true; } resolvedAddress = pointedHandle; @@ -847,7 +857,7 @@ public static class KernelPthreadCompatExports return 0; } - if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0) + if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, attrAddress, out var pointedHandle) && pointedHandle != 0) { lock (_stateGate) { @@ -900,7 +910,7 @@ public static class KernelPthreadCompatExports } } - if (ctx.TryReadUInt64(condAddress, out var pointedHandle) && pointedHandle != 0) + if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, condAddress, out var pointedHandle) && pointedHandle != 0) { lock (_stateGate) { @@ -932,7 +942,7 @@ public static class KernelPthreadCompatExports } } - if (ctx is null || !ctx.TryReadUInt64(condAddress, out var pointedHandle)) + if (ctx is null || !KernelMemoryCompatExports.TryReadUInt64Compat(ctx, condAddress, out var pointedHandle)) { return false; } @@ -971,7 +981,7 @@ public static class KernelPthreadCompatExports _condStates[handle] = createdState; } - if (!ctx.TryWriteUInt64(condAddress, handle)) + if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, handle)) { lock (_stateGate) { @@ -1035,7 +1045,7 @@ public static class KernelPthreadCompatExports _condStates[handle] = state; } - if (!ctx.TryWriteUInt64(condAddress, handle)) + if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, handle)) { lock (_stateGate) { @@ -1066,7 +1076,7 @@ public static class KernelPthreadCompatExports } } - _ = ctx.TryWriteUInt64(condAddress, 0); + _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -1202,6 +1212,63 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + private static string GetMutexWakeKey(ulong resolvedMutexAddress) => + $"pthread_mutex:0x{resolvedMutexAddress:X16}"; + + private static bool TryReserveBlockedMutexLock( + CpuContext ctx, + ulong mutexAddress, + ulong resolvedAddress, + PthreadMutexState state, + PthreadMutexWaiter waiter) + { + lock (state) + { + if (state.OwnerThreadId != 0 || state.RecursionCount != 0) + { + TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); + return false; + } + + state.OwnerThreadId = waiter.ThreadId; + state.RecursionCount = 1; + Interlocked.Exchange(ref waiter.Reserved, 1); + } + + TracePthreadMutex(ctx, "lock-reserve", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return true; + } + + private static int CompleteBlockedMutexLock( + CpuContext ctx, + ulong mutexAddress, + ulong resolvedAddress, + PthreadMutexState state, + PthreadMutexWaiter waiter) + { + var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); + if (Interlocked.Exchange(ref waiter.Reserved, 0) == 1) + { + TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (!state.Semaphore.Wait(0)) + { + TracePthreadMutex(ctx, "lock-resume-busy", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + + lock (state) + { + state.OwnerThreadId = currentThreadId; + state.RecursionCount = 1; + } + + TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + private static TimeSpan GetCondWaitTimeout(uint timeoutUsec) { if (timeoutUsec == 0) @@ -1313,13 +1380,10 @@ public static class KernelPthreadCompatExports _mutexStates[handle] = createdState; } - if (!ctx.TryWriteUInt64(mutexAddress, handle)) + if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, handle)) { - lock (_stateGate) - { - _mutexStates.Remove(mutexAddress); - _mutexStates.Remove(handle); - } + _mutexStates.TryRemove(mutexAddress, out _); + _mutexStates.TryRemove(handle, out _); resolvedAddress = 0; state = null; @@ -1357,13 +1421,13 @@ public static class KernelPthreadCompatExports private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result) { - if (!ShouldTracePthread()) + if (!ShouldTracePthreadMutex(mutexAddress, resolvedAddress)) { return; } - _ = ctx.TryReadUInt64(mutexAddress, out var guestWord0); - _ = ctx.TryReadUInt64(mutexAddress + 8, out var guestWord1); + _ = KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var guestWord0); + _ = KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestWord1); Console.Error.WriteLine( $"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " + $"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " + @@ -1373,7 +1437,7 @@ public static class KernelPthreadCompatExports private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result) { - if (!ShouldTracePthread()) + if (!_tracePthreadConds) { return; } @@ -1385,6 +1449,45 @@ public static class KernelPthreadCompatExports private static bool ShouldTracePthread() { - return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal); + return _tracePthreads; + } + + private static bool ShouldTracePthreadMutex(ulong mutexAddress, ulong resolvedAddress) + { + if (_tracePthreadMutexFilter is null || _tracePthreadMutexFilter.Count == 0) + { + return _tracePthreads; + } + + return _tracePthreadMutexFilter.Contains(mutexAddress) || + _tracePthreadMutexFilter.Contains(resolvedAddress); + } + + private static HashSet? ParseTraceAddressFilter(string? filter) + { + if (string.IsNullOrWhiteSpace(filter)) + { + return null; + } + + var addresses = new HashSet(); + foreach (var token in filter.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var normalized = token.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? token[2..] + : token; + normalized = normalized.TrimStart('0'); + + if (ulong.TryParse( + normalized.Length == 0 ? "0" : normalized, + System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, + out var address)) + { + addresses.Add(address); + } + } + + return addresses.Count == 0 ? null : addresses; } } diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index 2491c62..c3b29bb 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -3,6 +3,7 @@ using SharpEmu.HLE; using System.Buffers.Binary; +using System.Collections.Concurrent; using System.Text; using System.Threading; using System.Diagnostics.CodeAnalysis; @@ -26,12 +27,12 @@ public static class KernelPthreadExtendedCompatExports private static readonly Dictionary _threadStates = new(); private static readonly Dictionary _attrStates = new(); private static readonly Dictionary _rwlockStates = new(); - private static readonly Dictionary _tlsKeys = new(); + private static readonly ConcurrentDictionary _tlsKeys = new(); private static int _nextTlsKey = 1; private static long _nextSyntheticRwlockHandleId = 1; private static long _nextSyntheticPthreadAttrHandleId = 1; - private static readonly Dictionary> _threadLocalSpecific = new(); + private static readonly ConcurrentDictionary> _threadLocalSpecific = new(); private sealed class ThreadState { @@ -793,7 +794,7 @@ public static class KernelPthreadExtendedCompatExports _rwlockStates[syntheticHandle] = rwlock; } - _ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle); + _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -847,7 +848,7 @@ public static class KernelPthreadExtendedCompatExports } } - _ = ctx.TryWriteUInt64(rwlockAddress, 0); + _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, 0); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -959,15 +960,13 @@ public static class KernelPthreadExtendedCompatExports } int key; - lock (_stateGate) + while (true) { - while (_tlsKeys.ContainsKey(_nextTlsKey)) + key = Interlocked.Increment(ref _nextTlsKey) - 1; + if (_tlsKeys.TryAdd(key, new TlsKeyState(destructor))) { - _nextTlsKey++; + break; } - - key = _nextTlsKey++; - _tlsKeys[key] = new TlsKeyState(destructor); } if (!TryWriteInt32(ctx, outKeyAddress, key)) @@ -994,17 +993,14 @@ public static class KernelPthreadExtendedCompatExports public static int PosixPthreadKeyDelete(CpuContext ctx) { var key = unchecked((int)ctx[CpuRegister.Rdi]); - lock (_stateGate) + if (!_tlsKeys.TryRemove(key, out _)) { - if (!_tlsKeys.Remove(key)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; - } + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } - foreach (var entry in _threadLocalSpecific) - { - entry.Value.Remove(key); - } + foreach (var values in _threadLocalSpecific.Values) + { + values.TryRemove(key, out _); } ctx[CpuRegister.Rax] = 0; @@ -1028,22 +1024,15 @@ public static class KernelPthreadExtendedCompatExports var key = unchecked((int)ctx[CpuRegister.Rdi]); var value = ctx[CpuRegister.Rsi]; var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle(); - lock (_stateGate) + if (!_tlsKeys.ContainsKey(key)) { - if (!_tlsKeys.TryGetValue(key, out _)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; - } - - if (!_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values)) - { - values = new Dictionary(); - _threadLocalSpecific[currentThreadHandle] = values; - } - - values[key] = value; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } + var values = _threadLocalSpecific.GetOrAdd( + currentThreadHandle, + static _ => new ConcurrentDictionary()); + values[key] = value; ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -1065,19 +1054,16 @@ public static class KernelPthreadExtendedCompatExports var key = unchecked((int)ctx[CpuRegister.Rdi]); var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle(); ulong value = 0; - lock (_stateGate) + if (!_tlsKeys.ContainsKey(key)) { - if (!_tlsKeys.TryGetValue(key, out _)) - { - ctx[CpuRegister.Rax] = 0; - return (int)OrbisGen2Result.ORBIS_GEN2_OK; - } + ctx[CpuRegister.Rax] = 0; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } - if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) && - values.TryGetValue(key, out var storedValue)) - { - value = storedValue; - } + if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) && + values.TryGetValue(key, out var storedValue)) + { + value = storedValue; } ctx[CpuRegister.Rax] = value; @@ -1163,7 +1149,7 @@ public static class KernelPthreadExtendedCompatExports } } - if (ctx.TryReadUInt64(rwlockAddress, out var pointedHandle) && pointedHandle != 0) + if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle) && pointedHandle != 0) { lock (_stateGate) { @@ -1195,7 +1181,7 @@ public static class KernelPthreadExtendedCompatExports } } - if (!ctx.TryReadUInt64(rwlockAddress, out var pointedHandle)) + if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle)) { return false; } @@ -1230,7 +1216,7 @@ public static class KernelPthreadExtendedCompatExports _rwlockStates[syntheticHandle] = createdRwlock; } - _ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle); + _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle); resolvedAddress = syntheticHandle; rwlock = createdRwlock; return true; diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs index edfe4d4..ccdca3a 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs @@ -1,6 +1,7 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Threading; using SharpEmu.HLE; @@ -11,8 +12,7 @@ internal static class KernelPthreadState { private const int ThreadObjectSize = 0x1000; - private static readonly object Gate = new(); - private static readonly Dictionary Threads = new(); + private static readonly ConcurrentDictionary Threads = new(); private static readonly byte[] ZeroThreadObject = new byte[ThreadObjectSize]; private static long _nextUniqueThreadId = 1; @@ -56,10 +56,7 @@ internal static class KernelPthreadState internal static bool TryGetThreadIdentity(ulong threadHandle, out ThreadIdentity identity) { - lock (Gate) - { - return Threads.TryGetValue(threadHandle, out identity); - } + return Threads.TryGetValue(threadHandle, out identity); } private static void EnsureCurrentThreadRegistered() @@ -81,10 +78,7 @@ internal static class KernelPthreadState Marshal.Copy(ZeroThreadObject, 0, pointer, ThreadObjectSize); var handle = unchecked((ulong)pointer.ToInt64()); - lock (Gate) - { - Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name); - } + Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name); return handle; } diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index 215fe8b..167c6c8 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -57,6 +57,13 @@ public static class KernelRuntimeCompatExports private static readonly (ulong Base, ulong Size)[] _prtApertures = new (ulong Base, ulong Size)[3]; private static int _stackChkFailCount; private static long _usleepTraceCount; + private static readonly bool _traceUsleep = + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal); + private static readonly bool _traceGuestThreads = + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal); + + [ThreadStatic] + private static int _shortUsleepCount; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate ulong RdtscDelegate(); @@ -80,10 +87,20 @@ public static class KernelRuntimeCompatExports if (micros < 1000) { - Thread.Yield(); + // Guest worker pools use usleep(1) as a polling backoff. Periodically + // relinquish a full host time slice so spin workers cannot starve producers. + if ((++_shortUsleepCount & 31) == 0) + { + Thread.Sleep(1); + } + else + { + Thread.Yield(); + } } else { + _shortUsleepCount = 0; var sleepMilliseconds = (int)Math.Min((micros + 999UL) / 1000UL, int.MaxValue); Thread.Sleep(sleepMilliseconds); } @@ -94,7 +111,7 @@ public static class KernelRuntimeCompatExports private static void TraceUsleepSpin(CpuContext ctx, ulong micros) { - if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal)) + if (!_traceUsleep) { return; } @@ -142,6 +159,19 @@ public static class KernelRuntimeCompatExports Console.Error.WriteLine( $"[LOADER][TRACE] usleep#{count}: usec={micros} ret=0x{returnRip:X16} caller={callerReturnText} thread=0x{thread:X16} fiber=0x{fiber:X16} rbx=0x{rbx:X16} lock@+F78=0x{lockAddress:X16}:{lockText} r12=0x{r12:X16} scheduler@+8={schedulerText} r13=0x{r13:X16}:{waitValueText} r14=0x{ctx[CpuRegister.R14]:X16} r15=0x{ctx[CpuRegister.R15]:X16}"); + + if (count % 100000 == 0 && + _traceGuestThreads && + GuestThreadExecution.Scheduler is { } scheduler) + { + foreach (var snapshot in scheduler.SnapshotThreads()) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] guest_thread.snapshot handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " + + $"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " + + $"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}"); + } + } } [SysAbiExport( @@ -581,19 +611,16 @@ public static class KernelRuntimeCompatExports public static int ErrorAddress(CpuContext ctx) { var address = GetTlsScratchAddress(ctx, TlsErrnoOffset); - if (address != 0) - { - Span zero = stackalloc byte[sizeof(int)]; - if (!ctx.Memory.TryWrite(address, zero)) - { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; - } - } - ctx[CpuRegister.Rax] = address; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + internal static bool TrySetErrno(CpuContext ctx, int value) + { + var address = GetTlsScratchAddress(ctx, TlsErrnoOffset); + return address != 0 && TryWriteInt32(ctx, address, value); + } + [SysAbiExport( Nid = "bnZxYgAFeA0", ExportName = "sceKernelGetSanitizerNewReplaceExternal", @@ -1505,30 +1532,40 @@ public static class KernelRuntimeCompatExports ulong.TryParse(overrideHzText, out var overrideHz) && overrideHz >= minSane) { + TraceKernelTscFrequency("env", overrideHz); return overrideHz; } - if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane) - { - return cpuidHz; - } - if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane) { + TraceKernelTscFrequency("calibrated-rdtsc", calibratedHz); return calibratedHz; } + if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane) + { + TraceKernelTscFrequency("cpuid", cpuidHz); + return cpuidHz; + } + var hostQpc = Stopwatch.Frequency > 0 ? unchecked((ulong)Stopwatch.Frequency) : DefaultKernelTscFrequency; if (hostQpc >= minSane) { + TraceKernelTscFrequency("qpc", hostQpc); return hostQpc; } + TraceKernelTscFrequency("default", DefaultKernelTscFrequency); return DefaultKernelTscFrequency; } + private static void TraceKernelTscFrequency(string source, ulong frequencyHz) + { + Console.Error.WriteLine($"[LOADER][INFO] Kernel TSC frequency: {frequencyHz} Hz ({source})"); + } + private static bool TryResolveCpuidTscFrequency(out ulong frequencyHz) { frequencyHz = 0; @@ -1788,7 +1825,7 @@ public static class KernelRuntimeCompatExports } var invokeArgs = allocateAtHasAllowAlternativeArg - ? new object[] { desiredAddress, length, false, false } + ? new object[] { desiredAddress, length, false, allowSearch } : new object[] { desiredAddress, length, false }; var result = allocateAt.Invoke(memoryObject, invokeArgs); if (result is not ulong allocated || allocated == 0) diff --git a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs index a40984f..23de3a9 100644 --- a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs @@ -120,7 +120,7 @@ public static class KernelSemaphoreCompatExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); } - if (!GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitSema")) + if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema")) { TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);