mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +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:
@@ -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<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,
|
||||
// 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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user