mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 10:56:20 +08:00
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
This commit is contained in:
@@ -530,9 +530,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||||
}
|
}
|
||||||
DeliverPendingGuestExceptionAtSafePoint(
|
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
||||||
cpuContext,
|
{
|
||||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
DeliverPendingGuestExceptionAtSafePoint(
|
||||||
|
cpuContext,
|
||||||
|
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, num7));
|
||||||
|
}
|
||||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||||
if (dispatchResolved &&
|
if (dispatchResolved &&
|
||||||
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
orbisGen2Result == OrbisGen2Result.ORBIS_GEN2_OK &&
|
||||||
@@ -1326,9 +1329,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DeliverPendingGuestExceptionAtSafePoint(
|
if (Volatile.Read(ref _pendingGuestExceptionCount) != 0)
|
||||||
cpuContext,
|
{
|
||||||
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
DeliverPendingGuestExceptionAtSafePoint(
|
||||||
|
cpuContext,
|
||||||
|
CaptureImportBoundaryContinuation(cpuContext, argPackPtr, returnRip));
|
||||||
|
}
|
||||||
StoreImportVectorReturn(cpuContext, argPackPtr);
|
StoreImportVectorReturn(cpuContext, argPackPtr);
|
||||||
|
|
||||||
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
|||||||
@@ -712,6 +712,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
|
private readonly Dictionary<ulong, PendingGuestException> _pendingGuestExceptions = new Dictionary<ulong, PendingGuestException>();
|
||||||
|
|
||||||
|
// 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<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
|
private readonly HashSet<ulong> _activeGuestExceptionDeliveries = new HashSet<ulong>();
|
||||||
|
|
||||||
private int _guestThreadPumpDepth;
|
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
|
// unwinding. Unity can begin its next stop-the-world cycle in
|
||||||
// that window; treating the new raise as part of the old delivery
|
// that window; treating the new raise as part of the old delivery
|
||||||
// strands the collector waiting for an acknowledgement.
|
// strands the collector waiting for an acknowledgement.
|
||||||
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
external.ExceptionStackBase);
|
external.ExceptionStackBase));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3960,10 +3965,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
// managed thread corrupts the worker's control state. Queue the
|
// managed thread corrupts the worker's control state. Queue the
|
||||||
// request and let that exact executor consume it at its next HLE
|
// request and let that exact executor consume it at its next HLE
|
||||||
// boundary, where the original guest thread is safely paused.
|
// boundary, where the original guest thread is safely paused.
|
||||||
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
external.ExceptionStackBase);
|
external.ExceptionStackBase));
|
||||||
if (logGuestExceptions)
|
if (logGuestExceptions)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -4008,17 +4013,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
if (target.ExceptionDeliveryActive)
|
if (target.ExceptionDeliveryActive)
|
||||||
{
|
{
|
||||||
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
exceptionStackBase);
|
exceptionStackBase));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_pendingGuestExceptions[threadHandle] = new PendingGuestException(
|
QueuePendingGuestExceptionLocked(threadHandle, new PendingGuestException(
|
||||||
handler,
|
handler,
|
||||||
exceptionType,
|
exceptionType,
|
||||||
exceptionStackBase);
|
exceptionStackBase));
|
||||||
if (logGuestExceptions)
|
if (logGuestExceptions)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -4179,7 +4184,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
RestoreInterruptedGuestThread();
|
RestoreInterruptedGuestThread();
|
||||||
if (target.State == GuestThreadRunState.Blocked &&
|
if (target.State == GuestThreadRunState.Blocked &&
|
||||||
!target.ExecutorActive &&
|
!target.ExecutorActive &&
|
||||||
_pendingGuestExceptions.Remove(threadHandle, out var queued))
|
TryRemovePendingGuestExceptionLocked(threadHandle, out var queued))
|
||||||
{
|
{
|
||||||
followUp = queued;
|
followUp = queued;
|
||||||
}
|
}
|
||||||
@@ -4265,6 +4270,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
CpuContext currentContext,
|
CpuContext currentContext,
|
||||||
GuestCpuContinuation interruptedContinuation)
|
GuestCpuContinuation interruptedContinuation)
|
||||||
{
|
{
|
||||||
|
if (Volatile.Read(ref _pendingGuestExceptionCount) == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
var threadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
if (threadHandle == 0)
|
if (threadHandle == 0)
|
||||||
{
|
{
|
||||||
@@ -4278,7 +4288,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_pendingGuestExceptions.Remove(threadHandle, out pending))
|
if (!TryRemovePendingGuestExceptionLocked(threadHandle, out pending))
|
||||||
{
|
{
|
||||||
return;
|
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(
|
private static bool TryWriteGuestExceptionContext(
|
||||||
CpuContext context,
|
CpuContext context,
|
||||||
ulong address,
|
ulong address,
|
||||||
@@ -4434,6 +4465,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
_guestThreads.Clear();
|
_guestThreads.Clear();
|
||||||
_externalGuestThreads.Clear();
|
_externalGuestThreads.Clear();
|
||||||
_pendingGuestExceptions.Clear();
|
_pendingGuestExceptions.Clear();
|
||||||
|
Volatile.Write(ref _pendingGuestExceptionCount, 0);
|
||||||
_activeGuestExceptionDeliveries.Clear();
|
_activeGuestExceptionDeliveries.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||||||
return result;
|
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)
|
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||||
{
|
{
|
||||||
if (_inner is IGuestMemoryAllocator allocator)
|
if (_inner is IGuestMemoryAllocator allocator)
|
||||||
|
|||||||
@@ -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<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
||||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static CommittedRangeCache? _committedRangeCache;
|
||||||
|
|
||||||
|
private long _mappingGeneration;
|
||||||
private const ulong PageSize = 0x1000;
|
private const ulong PageSize = 0x1000;
|
||||||
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
||||||
private const ulong GuestAllocationArenaSize = 0x0100_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 FullCommitRegionLimit = 4UL << 30;
|
||||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 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
|
// Raw Windows PAGE_* values retained for the internal region/protection
|
||||||
// bookkeeping: regions and saved old-protection values always carry the raw
|
// bookkeeping: regions and saved old-protection values always carry the raw
|
||||||
@@ -440,6 +516,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.ExitWriteLock();
|
_gate.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _mappingGeneration);
|
||||||
_hostMemory.Free(address);
|
_hostMemory.Free(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,6 +688,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
_allocationSearchHints.Clear();
|
_allocationSearchHints.Clear();
|
||||||
}
|
}
|
||||||
|
Interlocked.Increment(ref _mappingGeneration);
|
||||||
}
|
}
|
||||||
finally
|
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<byte>((void*)sourcePointer, checked((int)length)).CopyTo(
|
||||||
|
new Span<byte>((void*)destinationPointer, checked((int)length)));
|
||||||
|
NotifyGuestWriteWatch(
|
||||||
|
destinationAddress,
|
||||||
|
new ReadOnlySpan<byte>((void*)destinationPointer, checked((int)length)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||||
{
|
{
|
||||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||||
@@ -1292,6 +1424,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var startPage = AlignDown(address, PageSize);
|
var startPage = AlignDown(address, PageSize);
|
||||||
var endPage = AlignUp(address + size, 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 commitProtection = GetCommitProtection(region);
|
||||||
|
|
||||||
var pageAddress = startPage;
|
var pageAddress = startPage;
|
||||||
@@ -1313,6 +1451,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
if (info.State == HostRegionState.Committed)
|
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;
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1328,12 +1469,23 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CacheCommittedRange(pageAddress, rangeEnd, mappingGeneration);
|
||||||
pageAddress = rangeEnd;
|
pageAddress = rangeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CacheCommittedRange(startPage, endPage, mappingGeneration);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CacheCommittedRange(ulong startPage, ulong endPage, long mappingGeneration)
|
||||||
|
{
|
||||||
|
(_committedRangeCache ??= new CommittedRangeCache()).Add(
|
||||||
|
this,
|
||||||
|
mappingGeneration,
|
||||||
|
startPage,
|
||||||
|
endPage);
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryTemporarilyProtectForRead(
|
private bool TryTemporarilyProtectForRead(
|
||||||
ulong address,
|
ulong address,
|
||||||
ulong size,
|
ulong size,
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ public interface ICpuMemory
|
|||||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||||
|
|
||||||
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
||||||
|
|
||||||
|
bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1105,10 +1105,13 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
if (count > 0 && !ctx.Memory.TryCopy(destination, source, (ulong)count))
|
||||||
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
var payload = GC.AllocateUninitializedArray<byte>(count);
|
||||||
|
if (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = destination;
|
ctx[CpuRegister.Rax] = destination;
|
||||||
|
|||||||
@@ -41,18 +41,87 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
private sealed class PthreadMutexState
|
private sealed class PthreadMutexState
|
||||||
{
|
{
|
||||||
public ulong OwnerThreadId { get; set; }
|
private long _ownerThreadId;
|
||||||
public int RecursionCount { get; set; }
|
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 Type { get; set; } = MutexTypeErrorCheck;
|
||||||
public int Protocol { get; set; }
|
public int Protocol { get; set; }
|
||||||
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
|
public LinkedList<PthreadMutexWaiter> 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
|
private sealed class PthreadMutexWaiter
|
||||||
{
|
{
|
||||||
public required ulong ThreadId { get; init; }
|
public required ulong ThreadId { get; init; }
|
||||||
public required string WakeKey { 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<PthreadMutexWaiter>? Node { get; set; }
|
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
|
||||||
public int Granted;
|
public int Granted;
|
||||||
}
|
}
|
||||||
@@ -94,7 +163,10 @@ public static class KernelPthreadCompatExports
|
|||||||
public static int PthreadSelf(CpuContext ctx)
|
public static int PthreadSelf(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
if (GuestThreadExecution.CurrentGuestThreadHandle != currentThreadHandle)
|
||||||
|
{
|
||||||
|
GuestThreadExecution.Scheduler?.RegisterGuestThreadContext(currentThreadHandle, ctx);
|
||||||
|
}
|
||||||
ctx[CpuRegister.Rax] = currentThreadHandle;
|
ctx[CpuRegister.Rax] = currentThreadHandle;
|
||||||
TracePthreadSelf(ctx, currentThreadHandle);
|
TracePthreadSelf(ctx, currentThreadHandle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -652,7 +724,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
|
||||||
{
|
{
|
||||||
@@ -684,11 +756,56 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
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 &&
|
var canCooperativelyBlock = !tryOnly &&
|
||||||
GuestThreadExecution.IsGuestThread &&
|
GuestThreadExecution.IsGuestThread &&
|
||||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
|
||||||
PthreadMutexWaiter? waiter = null;
|
PthreadMutexWaiter? waiter = null;
|
||||||
lock (state)
|
var acquiredWhileQueueing = false;
|
||||||
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
if (state.OwnerThreadId == currentThreadId)
|
if (state.OwnerThreadId == currentThreadId)
|
||||||
{
|
{
|
||||||
@@ -699,7 +816,23 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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)
|
if (tryOnly)
|
||||||
{
|
{
|
||||||
@@ -708,7 +841,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Several Gen5 runtimes layer their own owner/count bookkeeping
|
// 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
|
// leaves that guest bookkeeping out of sync with the HLE owner and
|
||||||
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
// turns the wrapper into a permanent lock/unlock retry loop. Keep
|
||||||
// the compatibility recursion used by the original implementation;
|
// 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
|
// waiter wedge a spin-on-trylock loop forever even though the mutex
|
||||||
// is free (owner==0). The blocking lock still honours FIFO so real
|
// is free (owner==0). The blocking lock still honours FIFO so real
|
||||||
// blocked waiters are not starved by a barging locker.
|
// 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);
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -749,6 +882,14 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
|
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 &&
|
if (canCooperativelyBlock && waiter is not null &&
|
||||||
@@ -782,8 +923,29 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
string? nextWakeKey = null;
|
if (state.OwnerThreadId == currentThreadId)
|
||||||
lock (state)
|
{
|
||||||
|
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)
|
if (state.RecursionCount <= 0)
|
||||||
{
|
{
|
||||||
@@ -810,19 +972,20 @@ public static class KernelPthreadCompatExports
|
|||||||
// later locker — including the game's main thread — then queues
|
// later locker — including the game's main thread — then queues
|
||||||
// behind a head that never advances and the process wedges.
|
// behind a head that never advances and the process wedges.
|
||||||
if (state.Waiters.First is { } headNode &&
|
if (state.Waiters.First is { } headNode &&
|
||||||
TryGrantMutexWaiterLocked(state, headNode.Value) &&
|
TryGrantMutexWaiterLocked(state, headNode.Value))
|
||||||
headNode.Value.Cooperative)
|
|
||||||
{
|
{
|
||||||
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);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
@@ -1282,7 +1445,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
lock (mutexState)
|
lock (mutexState.SyncRoot)
|
||||||
{
|
{
|
||||||
if (mutexState.OwnerThreadId == 0 && mutexState.RecursionCount == 0)
|
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
|
// mutex held (the unlock below is skipped), wedging every thread
|
||||||
// that later blocks on pthread_mutex_lock. Adopt ownership so the
|
// that later blocks on pthread_mutex_lock. Adopt ownership so the
|
||||||
// unlock/wait/re-lock cycle is balanced and releases the mutex.
|
// unlock/wait/re-lock cycle is balanced and releases the mutex.
|
||||||
mutexState.OwnerThreadId = currentThreadId;
|
_ = mutexState.TryAcquireOwner(currentThreadId);
|
||||||
mutexState.RecursionCount = 1;
|
|
||||||
}
|
}
|
||||||
else if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
|
||||||
|
if (mutexState.OwnerThreadId != currentThreadId || mutexState.RecursionCount != 1)
|
||||||
{
|
{
|
||||||
return mutexState.OwnerThreadId == currentThreadId
|
return mutexState.OwnerThreadId == currentThreadId
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
||||||
@@ -1467,6 +1630,7 @@ public static class KernelPthreadCompatExports
|
|||||||
if (node.Value.ThreadId == threadId)
|
if (node.Value.ThreadId == threadId)
|
||||||
{
|
{
|
||||||
state.Waiters.Remove(node);
|
state.Waiters.Remove(node);
|
||||||
|
state.WaiterRemovedLocked();
|
||||||
node.Value.Node = null;
|
node.Value.Node = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1481,8 +1645,10 @@ public static class KernelPthreadCompatExports
|
|||||||
WakeKey = cooperative
|
WakeKey = cooperative
|
||||||
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
|
||||||
: string.Empty,
|
: string.Empty,
|
||||||
|
HostSignal = cooperative ? null : new ManualResetEventSlim(initialState: false),
|
||||||
};
|
};
|
||||||
waiter.Node = state.Waiters.AddLast(waiter);
|
waiter.Node = state.Waiters.AddLast(waiter);
|
||||||
|
state.WaiterAddedLocked();
|
||||||
return waiter;
|
return waiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1492,7 +1658,7 @@ public static class KernelPthreadCompatExports
|
|||||||
var mutex = new PthreadMutexState();
|
var mutex = new PthreadMutexState();
|
||||||
PthreadMutexWaiter first;
|
PthreadMutexWaiter first;
|
||||||
PthreadMutexWaiter second;
|
PthreadMutexWaiter second;
|
||||||
lock (mutex)
|
lock (mutex.SyncRoot)
|
||||||
{
|
{
|
||||||
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
|
||||||
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
|
||||||
@@ -1536,26 +1702,72 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.TryAcquireOwner(waiter.ThreadId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
state.Waiters.Remove(waiter.Node);
|
state.Waiters.Remove(waiter.Node);
|
||||||
|
state.WaiterRemovedLocked();
|
||||||
waiter.Node = null;
|
waiter.Node = null;
|
||||||
state.OwnerThreadId = waiter.ThreadId;
|
|
||||||
state.RecursionCount = 1;
|
|
||||||
Volatile.Write(ref waiter.Granted, 1);
|
Volatile.Write(ref waiter.Granted, 1);
|
||||||
Monitor.PulseAll(state);
|
|
||||||
return true;
|
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)
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
{
|
||||||
|
hostSignal?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGrantBlockedMutexLock(
|
private static bool TryGrantBlockedMutexLock(
|
||||||
@@ -1566,7 +1778,7 @@ public static class KernelPthreadCompatExports
|
|||||||
PthreadMutexWaiter waiter)
|
PthreadMutexWaiter waiter)
|
||||||
{
|
{
|
||||||
var granted = false;
|
var granted = false;
|
||||||
lock (state)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
granted = TryGrantMutexWaiterLocked(state, waiter);
|
granted = TryGrantMutexWaiterLocked(state, waiter);
|
||||||
}
|
}
|
||||||
@@ -1615,7 +1827,7 @@ public static class KernelPthreadCompatExports
|
|||||||
waiter.TimeoutTimer?.Dispose();
|
waiter.TimeoutTimer?.Dispose();
|
||||||
waiter.TimeoutTimer = null;
|
waiter.TimeoutTimer = null;
|
||||||
|
|
||||||
lock (waiter.MutexState)
|
lock (waiter.MutexState.SyncRoot)
|
||||||
{
|
{
|
||||||
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
|
||||||
waiter.MutexState,
|
waiter.MutexState,
|
||||||
@@ -1663,7 +1875,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (waiter.MutexState)
|
lock (waiter.MutexState.SyncRoot)
|
||||||
{
|
{
|
||||||
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,60 @@ public sealed class PhysicalVirtualMemoryTests
|
|||||||
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
|
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<byte> 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<byte> 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,
|
// 2. Reserve-only region: GetPointer commits the page before returning it,
|
||||||
// so callers receive a valid (non-null) pointer. An unmapped address yields null.
|
// so callers receive a valid (non-null) pointer. An unmapped address yields null.
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -125,12 +179,14 @@ public sealed class PhysicalVirtualMemoryTests
|
|||||||
|
|
||||||
public LazyZeroedHostMemory()
|
public LazyZeroedHostMemory()
|
||||||
{
|
{
|
||||||
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x2000);
|
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x3000);
|
||||||
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
|
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
|
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 Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
|
||||||
|
|
||||||
public ulong Reserve(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)
|
public bool Query(ulong address, out HostRegionInfo info)
|
||||||
{
|
{
|
||||||
|
QueryCalls++;
|
||||||
var pageAddress = address & ~0xFFFUL;
|
var pageAddress = address & ~0xFFFUL;
|
||||||
info = new HostRegionInfo(
|
info = new HostRegionInfo(
|
||||||
pageAddress,
|
pageAddress,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace SharpEmu.Libs.Tests.Pthread;
|
|||||||
public sealed class PthreadMutexSemanticsTests
|
public sealed class PthreadMutexSemanticsTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AdaptiveMutex_SelfLockUsesCompatibilityRecursion()
|
public void AdaptiveMutex_SelfLockIsIdempotent()
|
||||||
{
|
{
|
||||||
const ulong memoryBase = 0x1_0000_0000;
|
const ulong memoryBase = 0x1_0000_0000;
|
||||||
const ulong mutexAddress = memoryBase + 0x100;
|
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.PthreadMutexLock(context));
|
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexLock(context));
|
||||||
Assert.Equal(0, KernelPthreadCompatExports.PthreadMutexUnlock(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
|
private sealed class AllocatingCpuMemory : ICpuMemory, IGuestMemoryAllocator
|
||||||
|
|||||||
Reference in New Issue
Block a user