From 90c72ebecf1a30ca1a335ab05466909e9e8524cf Mon Sep 17 00:00:00 2001 From: Spooks <62370103+Spooks4576@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:20:05 -0600 Subject: [PATCH] Fixes a Mutex Issue Preventing Some UE Titles From Booting (#451) * Optimize guest import, memory, and pthread hot paths * Fix UE adaptive mutex self-lock handling --- .../Native/DirectExecutionBackend.Imports.cs | 18 +- .../Cpu/Native/DirectExecutionBackend.cs | 52 +++- src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs | 3 + .../Memory/PhysicalVirtualMemory.cs | 152 ++++++++++ src/SharpEmu.HLE/ICpuMemory.cs | 2 + .../Kernel/KernelMemoryCompatExports.cs | 9 +- .../Kernel/KernelPthreadCompatExports.cs | 284 +++++++++++++++--- .../Memory/PhysicalVirtualMemoryTests.cs | 59 +++- .../Pthread/PthreadMutexSemanticsTests.cs | 126 +++++++- 9 files changed, 647 insertions(+), 58 deletions(-) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 79b6195f..45fa5ca0 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -530,9 +530,12 @@ public sealed partial class DirectExecutionBackend { GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame); } - DeliverPendingGuestExceptionAtSafePoint( - cpuContext, - CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7)); + if (Volatile.Read(ref _pendingGuestExceptionCount) != 0) + { + DeliverPendingGuestExceptionAtSafePoint( + cpuContext, + CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7)); + } StoreImportVectorReturn(cpuContext, argPackPtr); if (dispatchResolved && orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK && @@ -1326,9 +1329,12 @@ public sealed partial class DirectExecutionBackend GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame); } } - DeliverPendingGuestExceptionAtSafePoint( - cpuContext, - CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip)); + if (Volatile.Read(ref _pendingGuestExceptionCount) != 0) + { + DeliverPendingGuestExceptionAtSafePoint( + cpuContext, + CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip)); + } StoreImportVectorReturn(cpuContext, argPackPtr); if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index cdd7aca0..d5ba75d9 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -712,6 +712,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private readonly Dictionary _pendingGuestExceptions = new Dictionary(); + // Import dispatch is the hottest managed path in UE titles. Most imports do + // not have an exception queued, so publish the dictionary population and let + // safe points skip _guestThreadGate entirely in the common case. + private int _pendingGuestExceptionCount; + private readonly HashSet _activeGuestExceptionDeliveries = new HashSet(); private int _guestThreadPumpDepth; @@ -3948,10 +3953,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I // unwinding. Unity can begin its next stop-the-world cycle in // that window; treating the new raise as part of the old delivery // strands the collector waiting for an acknowledgement. - _pendingGuestExceptions[threadHandle] = new PendingGuestException( + QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException( handler, exceptionType, - external.ExceptionStackBase); + external.ExceptionStackBase)); return true; } @@ -3960,10 +3965,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I // managed thread corrupts the worker's control state. Queue the // request and let that exact executor consume it at its next HLE // boundary, where the original guest thread is safely paused. - _pendingGuestExceptions[threadHandle] = new PendingGuestException( + QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException( handler, exceptionType, - external.ExceptionStackBase); + external.ExceptionStackBase)); if (logGuestExceptions) { Console.Error.WriteLine( @@ -4008,17 +4013,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } if (target.ExceptionDeliveryActive) { - _pendingGuestExceptions[threadHandle] = new PendingGuestException( + QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException( handler, exceptionType, - exceptionStackBase); + exceptionStackBase)); return true; } - _pendingGuestExceptions[threadHandle] = new PendingGuestException( + QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException( handler, exceptionType, - exceptionStackBase); + exceptionStackBase)); if (logGuestExceptions) { Console.Error.WriteLine( @@ -4179,7 +4184,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I RestoreInterruptedGuestThread(); if (target.State == GuestThreadRunState.Blocked && !target.ExecutorActive && - _pendingGuestExceptions.Remove(threadHandle, out var queued)) + TryRemovePendingGuestExceptionLocked(threadHandle, out var queued)) { followUp = queued; } @@ -4265,6 +4270,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I CpuContext currentContext, GuestCpuContinuation interruptedContinuation) { + if (Volatile.Read(ref _pendingGuestExceptionCount) == 0) + { + return; + } + var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle; if (threadHandle == 0) { @@ -4278,7 +4288,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return; } - if (!_pendingGuestExceptions.Remove(threadHandle, out pending)) + if (!TryRemovePendingGuestExceptionLocked(threadHandle, out pending)) { return; } @@ -4340,6 +4350,27 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } } + private void QueuePendingGuestExceptionLocked( + ulong threadHandle, + PendingGuestException pending) + { + _pendingGuestExceptions[threadHandle] = pending; + Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count); + } + + private bool TryRemovePendingGuestExceptionLocked( + ulong threadHandle, + out PendingGuestException pending) + { + if (!_pendingGuestExceptions.Remove(threadHandle, out pending)) + { + return false; + } + + Volatile.Write(ref _pendingGuestExceptionCount, _pendingGuestExceptions.Count); + return true; + } + private static bool TryWriteGuestExceptionContext( CpuContext context, ulong address, @@ -4434,6 +4465,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I _guestThreads.Clear(); _externalGuestThreads.Clear(); _pendingGuestExceptions.Clear(); + Volatile.Write(ref _pendingGuestExceptionCount, 0); _activeGuestExceptionDeliveries.Clear(); } diff --git a/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs b/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs index 71f3de66..085073e2 100644 --- a/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs +++ b/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs @@ -40,6 +40,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo return result; } + public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => + _inner.TryCopy(destinationAddress, sourceAddress, length); + public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address) { if (_inner is IGuestMemoryAllocator allocator) diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index 54f39e45..86ad912c 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -20,6 +20,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new(); private readonly Dictionary _pageProtections = new(); private bool _disposed; + + [ThreadStatic] + private static CommittedRangeCache? _committedRangeCache; + + private long _mappingGeneration; private const ulong PageSize = 0x1000; private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000; private const ulong GuestAllocationArenaSize = 0x0100_0000; @@ -28,6 +33,77 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA private const ulong FullCommitRegionLimit = 4UL << 30; private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB + private const int CommittedRangeCacheCapacity = 4; + + private sealed class CommittedRangeCache + { + private readonly CommittedRange[] _ranges = new CommittedRange[CommittedRangeCacheCapacity]; + private PhysicalVirtualMemory? _owner; + private long _generation; + private int _count; + private int _nextReplacement; + + public bool Contains( + PhysicalVirtualMemory owner, + long generation, + ulong start, + ulong end) + { + if (!ReferenceEquals(_owner, owner) || _generation != generation) + { + return false; + } + + for (var index = 0; index < _count; index++) + { + var range = _ranges[index]; + if (start >= range.Start && end <= range.End) + { + return true; + } + } + + return false; + } + + public void Add( + PhysicalVirtualMemory owner, + long generation, + ulong start, + ulong end) + { + if (!ReferenceEquals(_owner, owner) || _generation != generation) + { + _owner = owner; + _generation = generation; + _count = 0; + _nextReplacement = 0; + } + + for (var index = 0; index < _count; index++) + { + var range = _ranges[index]; + if (start <= range.End && end >= range.Start) + { + _ranges[index] = new CommittedRange( + Math.Min(start, range.Start), + Math.Max(end, range.End)); + return; + } + } + + if (_count < _ranges.Length) + { + _ranges[_count++] = new CommittedRange(start, end); + return; + } + + _ranges[_nextReplacement] = new CommittedRange(start, end); + _nextReplacement = (_nextReplacement + 1) % _ranges.Length; + } + } + + private readonly record struct CommittedRange(ulong Start, ulong End); // Raw Windows PAGE_* values retained for the internal region/protection // bookkeeping: regions and saved old-protection values always carry the raw @@ -440,6 +516,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA _gate.ExitWriteLock(); } + Interlocked.Increment(ref _mappingGeneration); _hostMemory.Free(address); } @@ -611,6 +688,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA { _allocationSearchHints.Clear(); } + Interlocked.Increment(ref _mappingGeneration); } finally { @@ -953,6 +1031,60 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA } } + public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) + { + if (length == 0) + { + return true; + } + if (length > int.MaxValue) + { + return false; + } + + // Match TryWrite's managed-write notification before touching an + // identity-mapped guest page protected by the image tracker. + GuestImageWriteTracker.NotifyManagedWrite(destinationAddress, length); + + _gate.EnterReadLock(); + try + { + var sourceRegion = FindRegion(sourceAddress, length); + var destinationRegion = FindRegion(destinationAddress, length); + if (sourceRegion is null || destinationRegion is null || + !TryResolveRegionOffset(sourceAddress, length, sourceRegion, out var sourceOffset) || + !TryResolveRegionOffset(destinationAddress, length, destinationRegion, out var destinationOffset)) + { + return false; + } + + var sourcePointer = sourceRegion.VirtualAddress + sourceOffset; + var destinationPointer = destinationRegion.VirtualAddress + destinationOffset; + if ((sourceRegion.IsReservedOnly && + !EnsureRangeCommitted(sourcePointer, length, sourceRegion)) || + (destinationRegion.IsReservedOnly && + !EnsureRangeCommitted(destinationPointer, length, destinationRegion)) || + !CanReadWithoutProtectionChange(sourcePointer, length, sourceRegion) || + !CanWriteWithoutProtectionChange(destinationPointer, length, destinationRegion)) + { + return false; + } + + // Span.CopyTo has memmove overlap semantics, so this allocation-free + // path safely serves both libc memcpy and libc memmove. + new ReadOnlySpan((void*)sourcePointer, checked((int)length)).CopyTo( + new Span((void*)destinationPointer, checked((int)length))); + NotifyGuestWriteWatch( + destinationAddress, + new ReadOnlySpan((void*)destinationPointer, checked((int)length))); + return true; + } + finally + { + _gate.ExitReadLock(); + } + } + private bool TryReadExclusive(ulong virtualAddress, Span destination) { var region = FindRegion(virtualAddress, (ulong)destination.Length); @@ -1292,6 +1424,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA var startPage = AlignDown(address, PageSize); var endPage = AlignUp(address + size, PageSize); + var mappingGeneration = Volatile.Read(ref _mappingGeneration); + var committedRangeCache = _committedRangeCache ??= new CommittedRangeCache(); + if (committedRangeCache.Contains(this, mappingGeneration, startPage, endPage)) + { + return true; + } var commitProtection = GetCommitProtection(region); var pageAddress = startPage; @@ -1313,6 +1451,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA if (info.State == HostRegionState.Committed) { + // The host query proved this whole range is committed. Retain + // that result instead of caching only the caller's small span. + CacheCommittedRange(info.BaseAddress, queriedEnd, mappingGeneration); pageAddress = rangeEnd; continue; } @@ -1328,12 +1469,23 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA return false; } + CacheCommittedRange(pageAddress, rangeEnd, mappingGeneration); pageAddress = rangeEnd; } + CacheCommittedRange(startPage, endPage, mappingGeneration); return true; } + private void CacheCommittedRange(ulong startPage, ulong endPage, long mappingGeneration) + { + (_committedRangeCache ??= new CommittedRangeCache()).Add( + this, + mappingGeneration, + startPage, + endPage); + } + private bool TryTemporarilyProtectForRead( ulong address, ulong size, diff --git a/src/SharpEmu.HLE/ICpuMemory.cs b/src/SharpEmu.HLE/ICpuMemory.cs index 6cc001b0..52c4bf18 100644 --- a/src/SharpEmu.HLE/ICpuMemory.cs +++ b/src/SharpEmu.HLE/ICpuMemory.cs @@ -10,4 +10,6 @@ public interface ICpuMemory bool TryWrite(ulong virtualAddress, ReadOnlySpan source); bool TryCompare(ulong virtualAddress, ReadOnlySpan expected) => false; + + bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => false; } diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 1b5a4f47..bdc33ad2 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -1105,10 +1105,13 @@ public static partial class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } - var payload = GC.AllocateUninitializedArray(count); - if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload))) + if (count > 0 && !ctx.Memory.TryCopy(destination, source, (ulong)count)) { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + var payload = GC.AllocateUninitializedArray(count); + if (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } } ctx[CpuRegister.Rax] = destination; diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 8ec75067..3aad12e1 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -41,18 +41,87 @@ public static class KernelPthreadCompatExports private sealed class PthreadMutexState { - public ulong OwnerThreadId { get; set; } - public int RecursionCount { get; set; } + private long _ownerThreadId; + private int _recursionCount; + private int _queuedWaiterCount; + + public Lock SyncRoot { get; } = new(); + public ulong OwnerThreadId + { + get => unchecked((ulong)Volatile.Read(ref _ownerThreadId)); + set => Volatile.Write(ref _ownerThreadId, unchecked((long)value)); + } + + public int RecursionCount + { + get => Volatile.Read(ref _recursionCount); + set => Volatile.Write(ref _recursionCount, value); + } + + public int QueuedWaiterCount => Volatile.Read(ref _queuedWaiterCount); public int Type { get; set; } = MutexTypeErrorCheck; public int Protocol { get; set; } public LinkedList Waiters { get; } = new(); + + public bool TryAcquireUncontended(ulong threadId, bool allowWaiterBarge) + { + if (!allowWaiterBarge && QueuedWaiterCount != 0) + { + return false; + } + + return TryAcquireOwner(threadId); + } + + public bool TryAcquireOwner(ulong threadId) + { + if (Interlocked.CompareExchange( + ref _ownerThreadId, + unchecked((long)threadId), + 0) != 0) + { + return false; + } + + Volatile.Write(ref _recursionCount, 1); + return true; + } + + public bool TryReleaseUncontended(ulong threadId) + { + if (QueuedWaiterCount != 0 || RecursionCount != 1) + { + return false; + } + + Volatile.Write(ref _recursionCount, 0); + if (Interlocked.CompareExchange( + ref _ownerThreadId, + 0, + unchecked((long)threadId)) == unchecked((long)threadId)) + { + return true; + } + + Volatile.Write(ref _recursionCount, 1); + return false; + } + + public int IncrementRecursion() => Interlocked.Increment(ref _recursionCount); + + public int DecrementRecursion() => Interlocked.Decrement(ref _recursionCount); + + public void WaiterAddedLocked() => Interlocked.Increment(ref _queuedWaiterCount); + + public void WaiterRemovedLocked() => Interlocked.Decrement(ref _queuedWaiterCount); } private sealed class PthreadMutexWaiter { public required ulong ThreadId { get; init; } public required string WakeKey { get; init; } - public required bool Cooperative { get; init; } + public required bool Cooperative { get; set; } + public ManualResetEventSlim? HostSignal { get; set; } public LinkedListNode? Node { get; set; } public int Granted; } @@ -94,7 +163,10 @@ public static class KernelPthreadCompatExports public static int PthreadSelf(CpuContext ctx) { var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle(); - GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx); + if (GuestThreadExecution.CurrentGuestThreadHandle != currentThreadHandle) + { + GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx); + } ctx[CpuRegister.Rax] = currentThreadHandle; TracePthreadSelf(ctx, currentThreadHandle); return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -652,7 +724,7 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } - lock (state) + lock (state.SyncRoot) { if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0) { @@ -684,11 +756,56 @@ public static class KernelPthreadCompatExports } var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); + if (state.TryAcquireUncontended(currentThreadId, allowWaiterBarge: tryOnly)) + { + TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (state.OwnerThreadId == currentThreadId) + { + if (state.Type == MutexTypeRecursive) + { + state.IncrementRecursion(); + TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (state.Type == MutexTypeAdaptiveNp) + { + var adaptiveResult = tryOnly + ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY + : (int)OrbisGen2Result.ORBIS_GEN2_OK; + TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, adaptiveResult); + return adaptiveResult; + } + + if (state.Type == MutexTypeNormal) + { + if (tryOnly) + { + TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + + state.IncrementRecursion(); + TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + var ownedResult = tryOnly + ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK; + TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult); + return ownedResult; + } + var canCooperativelyBlock = !tryOnly && GuestThreadExecution.IsGuestThread && GuestThreadExecution.TryGetCurrentImportCallFrame(out _); PthreadMutexWaiter? waiter = null; - lock (state) + var acquiredWhileQueueing = false; + lock (state.SyncRoot) { if (state.OwnerThreadId == currentThreadId) { @@ -699,7 +816,23 @@ public static class KernelPthreadCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp) + if (state.Type == MutexTypeAdaptiveNp) + { + if (tryOnly) + { + TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + } + + // Gen5 runtime wrappers can layer an adaptive lock call over + // scePthreadMutexLock for one logical acquisition, followed by + // only one unlock. Keep the duplicate acquisition idempotent so + // the matching unlock fully releases the HLE mutex. + TracePthreadMutex(ctx, "lock-idempotent", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (state.Type == MutexTypeNormal) { if (tryOnly) { @@ -708,7 +841,7 @@ public static class KernelPthreadCompatExports } // Several Gen5 runtimes layer their own owner/count bookkeeping - // over a NORMAL or ADAPTIVE kernel mutex. Returning EDEADLK here + // over a NORMAL kernel mutex. Returning EDEADLK here // leaves that guest bookkeeping out of sync with the HLE owner and // turns the wrapper into a permanent lock/unlock retry loop. Keep // the compatibility recursion used by the original implementation; @@ -734,10 +867,10 @@ public static class KernelPthreadCompatExports // waiter wedge a spin-on-trylock loop forever even though the mutex // is free (owner==0). The blocking lock still honours FIFO so real // blocked waiters are not starved by a barging locker. - if (state.OwnerThreadId == 0 && (tryOnly || state.Waiters.Count == 0)) + if (state.OwnerThreadId == 0 && + (tryOnly || state.Waiters.Count == 0) && + state.TryAcquireOwner(currentThreadId)) { - state.OwnerThreadId = currentThreadId; - state.RecursionCount = 1; TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -749,6 +882,14 @@ public static class KernelPthreadCompatExports } waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock); + acquiredWhileQueueing = TryGrantMutexWaiterLocked(state, waiter); + } + + if (acquiredWhileQueueing) + { + waiter!.HostSignal?.Dispose(); + TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } if (canCooperativelyBlock && waiter is not null && @@ -782,8 +923,29 @@ public static class KernelPthreadCompatExports } var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); - string? nextWakeKey = null; - lock (state) + if (state.OwnerThreadId == currentThreadId) + { + if (state.RecursionCount > 1) + { + state.DecrementRecursion(); + TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + if (state.TryReleaseUncontended(currentThreadId)) + { + if (state.QueuedWaiterCount != 0) + { + WakeFirstMutexWaiter(state); + } + + TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + } + + PthreadMutexWaiter? nextWaiter = null; + lock (state.SyncRoot) { if (state.RecursionCount <= 0) { @@ -810,19 +972,20 @@ public static class KernelPthreadCompatExports // later locker — including the game's main thread — then queues // behind a head that never advances and the process wedges. if (state.Waiters.First is { } headNode && - TryGrantMutexWaiterLocked(state, headNode.Value) && - headNode.Value.Cooperative) + TryGrantMutexWaiterLocked(state, headNode.Value)) { - nextWakeKey = headNode.Value.WakeKey; + nextWaiter = headNode.Value; + if (!nextWaiter.Cooperative) + { + nextWaiter.HostSignal!.Set(); + } } - - Monitor.PulseAll(state); } } - if (nextWakeKey is not null) + if (nextWaiter is { Cooperative: true }) { - _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWakeKey, 1); + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1); } TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); @@ -1282,7 +1445,7 @@ public static class KernelPthreadCompatExports } var currentThreadId = KernelPthreadState.GetCurrentThreadHandle(); - lock (mutexState) + lock (mutexState.SyncRoot) { if (mutexState.OwnerThreadId == 0 && mutexState.RecursionCount == 0) { @@ -1295,10 +1458,10 @@ public static class KernelPthreadCompatExports // mutex held (the unlock below is skipped), wedging every thread // that later blocks on pthread_mutex_lock. Adopt ownership so the // unlock/wait/re-lock cycle is balanced and releases the mutex. - mutexState.OwnerThreadId = currentThreadId; - mutexState.RecursionCount = 1; + _ = mutexState.TryAcquireOwner(currentThreadId); } - else if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1) + + if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1) { return mutexState.OwnerThreadId == currentThreadId ? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT @@ -1467,6 +1630,7 @@ public static class KernelPthreadCompatExports if (node.Value.ThreadId == threadId) { state.Waiters.Remove(node); + state.WaiterRemovedLocked(); node.Value.Node = null; } @@ -1481,8 +1645,10 @@ public static class KernelPthreadCompatExports WakeKey = cooperative ? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}" : string.Empty, + HostSignal = cooperative ? null : new ManualResetEventSlim(initialState: false), }; waiter.Node = state.Waiters.AddLast(waiter); + state.WaiterAddedLocked(); return waiter; } @@ -1492,7 +1658,7 @@ public static class KernelPthreadCompatExports var mutex = new PthreadMutexState(); PthreadMutexWaiter first; PthreadMutexWaiter second; - lock (mutex) + lock (mutex.SyncRoot) { first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false); second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false); @@ -1536,26 +1702,72 @@ public static class KernelPthreadCompatExports return false; } + if (!state.TryAcquireOwner(waiter.ThreadId)) + { + return false; + } + state.Waiters.Remove(waiter.Node); + state.WaiterRemovedLocked(); waiter.Node = null; - state.OwnerThreadId = waiter.ThreadId; - state.RecursionCount = 1; Volatile.Write(ref waiter.Granted, 1); - Monitor.PulseAll(state); return true; } + private static void WakeFirstMutexWaiter(PthreadMutexState state) + { + PthreadMutexWaiter? nextWaiter; + lock (state.SyncRoot) + { + if (state.OwnerThreadId != 0) + { + return; + } + + nextWaiter = state.Waiters.First?.Value; + if (nextWaiter is { Cooperative: false }) + { + nextWaiter.HostSignal!.Set(); + } + } + + if (nextWaiter is { Cooperative: true }) + { + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWaiter.WakeKey, 1); + } + } + private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter) { - lock (state) + ManualResetEventSlim? hostSignal = null; + try { - while (!TryGrantMutexWaiterLocked(state, waiter)) + while (true) { - Monitor.Wait(state); + lock (state.SyncRoot) + { + if (waiter.HostSignal is null) + { + waiter.Cooperative = false; + waiter.HostSignal = new ManualResetEventSlim(initialState: false); + } + + hostSignal = waiter.HostSignal; + if (TryGrantMutexWaiterLocked(state, waiter)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + hostSignal.Reset(); + } + + hostSignal.Wait(); } } - - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + finally + { + hostSignal?.Dispose(); + } } private static bool TryGrantBlockedMutexLock( @@ -1566,7 +1778,7 @@ public static class KernelPthreadCompatExports PthreadMutexWaiter waiter) { var granted = false; - lock (state) + lock (state.SyncRoot) { granted = TryGrantMutexWaiterLocked(state, waiter); } @@ -1615,7 +1827,7 @@ public static class KernelPthreadCompatExports waiter.TimeoutTimer?.Dispose(); waiter.TimeoutTimer = null; - lock (waiter.MutexState) + lock (waiter.MutexState.SyncRoot) { waiter.MutexWaiter = EnqueueMutexWaiterLocked( waiter.MutexState, @@ -1663,7 +1875,7 @@ public static class KernelPthreadCompatExports return false; } - lock (waiter.MutexState) + lock (waiter.MutexState.SyncRoot) { return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter); } diff --git a/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs b/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs index 24abfae3..5ad2a8b2 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/PhysicalVirtualMemoryTests.cs @@ -39,6 +39,60 @@ public sealed class PhysicalVirtualMemoryTests Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls); } + [Fact] + public void RepeatedLazyReadUsesCommittedRangeCache() + { + using var host = new LazyZeroedHostMemory(); + using var memory = new PhysicalVirtualMemory(host); + + var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false); + host.CommitCalls.Clear(); + + Span buffer = stackalloc byte[16]; + Assert.True(memory.TryRead(address + 0x100, buffer)); + var queryCallsAfterFirstRead = host.QueryCalls; + Assert.True(memory.TryRead(address + 0x108, buffer[..8])); + + Assert.Equal(queryCallsAfterFirstRead, host.QueryCalls); + Assert.Single(host.CommitCalls); + } + + [Fact] + public void TryCopyHandlesOverlappingIdentityMappedRanges() + { + using var host = new LazyZeroedHostMemory(); + using var memory = new PhysicalVirtualMemory(host); + + var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false); + Assert.True(memory.TryWrite(address, new byte[] { 1, 2, 3, 4, 5, 6 })); + + Assert.True(memory.TryCopy(address + 2, address, 4)); + + Span result = stackalloc byte[6]; + Assert.True(memory.TryRead(address, result)); + Assert.Equal(new byte[] { 1, 2, 1, 2, 3, 4 }, result.ToArray()); + } + + [Fact] + public void RepeatedTryCopyKeepsSourceAndDestinationCommitRangesCached() + { + using var host = new LazyZeroedHostMemory(); + using var memory = new PhysicalVirtualMemory(host); + + var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false); + var source = address + 0x100; + var destination = address + 0x1100; + Assert.True(memory.TryWrite(source, new byte[] { 1, 2, 3, 4 })); + Assert.True(memory.TryWrite(destination, new byte[4])); + + host.CommitCalls.Clear(); + Assert.True(memory.TryCopy(destination, source, 4)); + var queryCallsAfterFirstCopy = host.QueryCalls; + Assert.True(memory.TryCopy(destination, source, 4)); + + Assert.Equal(queryCallsAfterFirstCopy, host.QueryCalls); + } + // 2. Reserve-only region: GetPointer commits the page before returning it, // so callers receive a valid (non-null) pointer. An unmapped address yields null. [Fact] @@ -125,12 +179,14 @@ public sealed class PhysicalVirtualMemoryTests public LazyZeroedHostMemory() { - _allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x2000); + _allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x3000); _address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL; } public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = []; + public int QueryCalls { get; private set; } + public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => _address; public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => _address; @@ -162,6 +218,7 @@ public sealed class PhysicalVirtualMemoryTests public bool Query(ulong address, out HostRegionInfo info) { + QueryCalls++; var pageAddress = address & ~0xFFFUL; info = new HostRegionInfo( pageAddress, diff --git a/tests/SharpEmu.Libs.Tests/Pthread/PthreadMutexSemanticsTests.cs b/tests/SharpEmu.Libs.Tests/Pthread/PthreadMutexSemanticsTests.cs index 31f873a5..03b44c21 100644 --- a/tests/SharpEmu.Libs.Tests/Pthread/PthreadMutexSemanticsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Pthread/PthreadMutexSemanticsTests.cs @@ -10,7 +10,7 @@ namespace SharpEmu.Libs.Tests.Pthread; public sealed class PthreadMutexSemanticsTests { [Fact] - public void AdaptiveMutex_SelfLockUsesCompatibilityRecursion() + public void AdaptiveMutex_SelfLockIsIdempotent() { const ulong memoryBase = 0x1_0000_0000; const ulong mutexAddress = memoryBase + 0x100; @@ -22,7 +22,129 @@ public sealed class PthreadMutexSemanticsTests Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context)); Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context)); Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context)); - Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(context)); + Assert.NotEqual(0, KernelPthreadCompatExports.PthreadMutexUnlock(context)); + } + + [Fact] + public async Task ContendedMutex_HandsOffOneHostWaiterAtATime() + { + const ulong memoryBase = 0x2_0000_0000; + const ulong mutexAddress = memoryBase + 0x100; + var memory = new AllocatingCpuMemory(memoryBase, 0x4000); + var ownerContext = new CpuContext(memory, Generation.Gen5); + Assert.True(ownerContext.TryWriteUInt64(mutexAddress, 1)); + ownerContext[CpuRegister.Rdi] = mutexAddress; + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(ownerContext)); + + using var waitersStarted = new CountdownEvent(2); + using var firstAcquired = new ManualResetEventSlim(false); + using var secondAcquired = new ManualResetEventSlim(false); + using var releaseFirst = new ManualResetEventSlim(false); + var acquisitionCount = 0; + + Task<(int LockResult, int UnlockResult)> StartWaiter() => + Task.Factory.StartNew( + () => + { + var waiterContext = new CpuContext(memory, Generation.Gen5); + waiterContext[CpuRegister.Rdi] = mutexAddress; + waitersStarted.Signal(); + var lockResult = KernelPthreadCompatExports.PthreadMutexLock(waiterContext); + if (lockResult != 0) + { + return (lockResult, int.MinValue); + } + + if (Interlocked.Increment(ref acquisitionCount) == 1) + { + firstAcquired.Set(); + releaseFirst.Wait(TimeSpan.FromSeconds(5)); + } + else + { + secondAcquired.Set(); + } + + var unlockResult = KernelPthreadCompatExports.PthreadMutexUnlock(waiterContext); + return (lockResult, unlockResult); + }, + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + + var firstWaiter = StartWaiter(); + var secondWaiter = StartWaiter(); + Assert.True(waitersStarted.Wait(TimeSpan.FromSeconds(5))); + Thread.Sleep(50); + Assert.Equal(0, Volatile.Read(ref acquisitionCount)); + + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(ownerContext)); + Assert.True(firstAcquired.Wait(TimeSpan.FromSeconds(5))); + Assert.Equal(1, Volatile.Read(ref acquisitionCount)); + releaseFirst.Set(); + Assert.True(secondAcquired.Wait(TimeSpan.FromSeconds(5))); + + var results = await Task.WhenAll(firstWaiter, secondWaiter).WaitAsync(TimeSpan.FromSeconds(5)); + Assert.All(results, result => Assert.Equal((0, 0), result)); + Assert.Equal(2, Volatile.Read(ref acquisitionCount)); + } + + [Fact] + public async Task ContendedMutex_PreservesMutualExclusionUnderLoad() + { + const ulong memoryBase = 0x3_0000_0000; + const ulong mutexAddress = memoryBase + 0x100; + const int workerCount = 4; + const int iterationsPerWorker = 250; + var memory = new AllocatingCpuMemory(memoryBase, 0x4000); + var initializationContext = new CpuContext(memory, Generation.Gen5); + Assert.True(initializationContext.TryWriteUInt64(mutexAddress, 1)); + initializationContext[CpuRegister.Rdi] = mutexAddress; + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(initializationContext)); + Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(initializationContext)); + + using var start = new ManualResetEventSlim(false); + var insideCriticalSection = 0; + var mutualExclusionViolations = 0; + var protectedCounter = 0; + var workers = Enumerable.Range(0, workerCount) + .Select(_ => Task.Factory.StartNew( + () => + { + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = mutexAddress; + start.Wait(); + for (var iteration = 0; iteration < iterationsPerWorker; iteration++) + { + if (KernelPthreadCompatExports.PthreadMutexLock(context) != 0) + { + throw new InvalidOperationException("pthread mutex lock failed during contention stress."); + } + + if (Interlocked.Increment(ref insideCriticalSection) != 1) + { + Interlocked.Increment(ref mutualExclusionViolations); + } + + protectedCounter++; + Thread.SpinWait(20); + Interlocked.Decrement(ref insideCriticalSection); + + if (KernelPthreadCompatExports.PthreadMutexUnlock(context) != 0) + { + throw new InvalidOperationException("pthread mutex unlock failed during contention stress."); + } + } + }, + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default)) + .ToArray(); + + start.Set(); + await Task.WhenAll(workers).WaitAsync(TimeSpan.FromSeconds(10)); + Assert.Equal(0, Volatile.Read(ref mutualExclusionViolations)); + Assert.Equal(workerCount * iterationsPerWorker, protectedCounter); } private sealed class AllocatingCpuMemory : ICpuMemory, IGuestMemoryAllocator