mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-18 17:06:12 +08:00
[HLE] Fix guest-thread sync and boot for Unreal Engine titles (#102)
* [HLE] Fix guest-thread sync and boot for Unreal Engine titles Silent Hill: The Short Message (and other UE titles) now boot the full engine thread graph instead of hanging early. Four related fixes: - pthread cond/mutex semantics: retain a signal raised with no waiter as pending, and key block/wake on the state's identity rather than a resolved address that could differ between lock and unlock. This ends the ~1.5M-call cond_wait busy-spin. - Warm HLE type initializers and force-JIT their methods on a host thread at Freeze(). A .cctor or first-time JIT running on a guest thread's hijacked stack fail-fasts the CLR as "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed code". - Guest thread scheduling: pump after a wake so a readied thread actually runs, add a dispatcher thread for when every guest thread is parked, and make the pump-depth guard an atomic CAS. - Route mutex/rwlock lock/unlock off the non-blocking leaf-import fast path so a contended lock can deschedule its guest thread. Ported from the unreal-boot-fixes branch. * [HLE] Keep mutex/rwlock unlock on the leaf-import fast path The previous change routed all mutex/rwlock lock and unlock NIDs off the leaf fast path so a contended lock could deschedule its guest thread. But unlock never blocks, and taking it off the fast path made it slow enough that Demon's Souls' job workers livelocked in a guest spinlock (millions of mutex_unlock calls, no import progress, main thread stuck in sceKernelWaitEventFlag). Only *lock* needs to leave the leaf path. Restore the four unlock NIDs (mutex + rwlock) so guest spinlocks stay cheap, while lock/rd/wrlock remain off it for the blocking case Silent Hill needs. * [HLE] Gate pthread_mutex_lock guest-thread blocking (fixes Demon's Souls) Re-enabling cooperative deschedule on a contended pthread_mutex_lock regressed Demon's Souls: its job workers run on libSceFiber, and blocking a guest thread mid-fiber left sceFiberSwitch returning ESRCH followed by a null fiber-context deref (0xC0000005). Bisect confirmed the pthread change as the cause; the game reaches the same point as before it once the block is skipped. Gate the block behind SHARPEMU_MUTEX_LOCK_BLOCKING (off by default) so contended locks fall through to the synchronous host-thread wait. The rest of the pthread fixes (cond_wait pending signals, identity wake keys) are unaffected.
This commit is contained in:
@@ -810,11 +810,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return !_logUsleep;
|
return !_logUsleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
|
||||||
|
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
|
||||||
|
// slows guest spinlocks enough to livelock (Demon's Souls).
|
||||||
return nid is
|
return nid is
|
||||||
"9UK1vLZQft4" or // scePthreadMutexLock
|
|
||||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||||
"7H0iTOciTLo" or // pthread_mutex_lock
|
|
||||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||||
|
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||||
|
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||||
@@ -888,12 +891,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
||||||
"1471ajPzxh0" or // pthread_rwlock_destroy
|
"1471ajPzxh0" or // pthread_rwlock_destroy
|
||||||
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
||||||
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
|
// rwlock rd/wr lock removed (can block); init/destroy/unlock stay.
|
||||||
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
|
|
||||||
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
|
|
||||||
"mqdNorrB+gI" or // scePthreadRwlockWrlock
|
|
||||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
|
||||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
|
||||||
"aI+OeCz8xrQ" or // scePthreadSelf
|
"aI+OeCz8xrQ" or // scePthreadSelf
|
||||||
"EotR8a3ASf4" or // pthread_self
|
"EotR8a3ASf4" or // pthread_self
|
||||||
"eoht7mQOCmo" or // scePthreadGetspecific
|
"eoht7mQOCmo" or // scePthreadGetspecific
|
||||||
|
|||||||
@@ -2782,11 +2782,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_guestThreadPumpDepth != 0)
|
if (Interlocked.CompareExchange(ref _guestThreadPumpDepth, 1, 0) != 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_guestThreadPumpDepth++;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 8; i++)
|
for (int i = 0; i < 8; i++)
|
||||||
@@ -2832,7 +2831,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_guestThreadPumpDepth--;
|
Volatile.Write(ref _guestThreadPumpDepth, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2875,9 +2874,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wakeCount != 0 && _logGuestThreads)
|
if (wakeCount != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] guest_threads.wake key={wakeKey} count={wakeCount}");
|
if (_logGuestThreads)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] guest_threads.wake key={wakeKey} count={wakeCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pump or the readied thread waits for an import dispatch that never comes.
|
||||||
|
if (_cpuContext is { } wakeContext)
|
||||||
|
{
|
||||||
|
Pump(wakeContext, "wake");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return wakeCount;
|
return wakeCount;
|
||||||
@@ -4422,6 +4430,26 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_stallWatchdogStop = false;
|
_stallWatchdogStop = false;
|
||||||
|
|
||||||
|
// Drives woken threads when every guest thread is parked (nothing dispatches then).
|
||||||
|
var dispatcherThread = new Thread(new ThreadStart(delegate
|
||||||
|
{
|
||||||
|
while (!_stallWatchdogStop)
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
WakeExpiredBlockedGuestThreads();
|
||||||
|
if (Volatile.Read(ref _readyGuestThreadCount) > 0 && _cpuContext is { } dispatchContext)
|
||||||
|
{
|
||||||
|
Pump(dispatchContext, "dispatcher");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "SharpEmu-GuestThreadDispatcher"
|
||||||
|
};
|
||||||
|
dispatcherThread.Start();
|
||||||
|
|
||||||
long num = (long)((double)stallWatchdogSeconds * Stopwatch.Frequency);
|
long num = (long)((double)stallWatchdogSeconds * Stopwatch.Frequency);
|
||||||
_stallWatchdogThread = new Thread(new ThreadStart(delegate
|
_stallWatchdogThread = new Thread(new ThreadStart(delegate
|
||||||
{
|
{
|
||||||
@@ -4450,6 +4478,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
MarkExecutionProgress();
|
MarkExecutionProgress();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (IsExpectedBlockingImportStall(out var blockingNid, out var blockingName))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] No import progress for {stallWatchdogSeconds}s while waiting in {blockingName} ({blockingNid}); continuing.");
|
||||||
|
LogStallWatchdogSnapshot();
|
||||||
|
Console.Error.Flush();
|
||||||
|
MarkExecutionProgress();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (Interlocked.Exchange(ref _stallWatchdogTriggered, 1) != 0)
|
if (Interlocked.Exchange(ref _stallWatchdogTriggered, 1) != 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -4485,6 +4522,38 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A thread parked in a blocking wait is idle by design, not stalled.
|
||||||
|
private bool IsExpectedBlockingImportStall(out string nid, out string name)
|
||||||
|
{
|
||||||
|
nid = string.Empty;
|
||||||
|
name = string.Empty;
|
||||||
|
var cpuContext = _cpuContext;
|
||||||
|
if (cpuContext is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var importAddress = cpuContext.Rip & 0xFFFFFFFFFFFFFFF0uL;
|
||||||
|
foreach (var entry in _importEntries)
|
||||||
|
{
|
||||||
|
if (entry.Address != importAddress)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nid = entry.Nid;
|
||||||
|
name = _moduleManager.TryGetExport(nid, out var export)
|
||||||
|
? $"{export.LibraryName}:{export.Name}"
|
||||||
|
: nid;
|
||||||
|
return nid is
|
||||||
|
"Op8TBGY5KHg" or // pthread_cond_wait
|
||||||
|
"27bAgiJmOh0" or // pthread_cond_timedwait
|
||||||
|
"fzyMKs9kim0"; // sceKernelWaitEqueue
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void StopStallWatchdog()
|
private void StopStallWatchdog()
|
||||||
{
|
{
|
||||||
_stallWatchdogStop = true;
|
_stallWatchdogStop = true;
|
||||||
|
|||||||
@@ -3,18 +3,17 @@
|
|||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using SharpEmu.Logging;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
public sealed class ModuleManager : IModuleManager
|
public sealed class ModuleManager : IModuleManager
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||||
private readonly object _registrationGate = new();
|
private readonly object _registrationGate = new();
|
||||||
|
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||||
private bool _isFrozen;
|
private bool _isFrozen;
|
||||||
|
|
||||||
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
||||||
@@ -28,6 +27,12 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
throw new InvalidOperationException("Module registration is frozen.");
|
throw new InvalidOperationException("Module registration is frozen.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deduplicated: one assembly is reached through many types.
|
||||||
|
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
var registeredCount = 0;
|
var registeredCount = 0;
|
||||||
var instances = new Dictionary<Type, object>();
|
var instances = new Dictionary<Type, object>();
|
||||||
|
|
||||||
@@ -50,7 +55,7 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
var handler = CreateHandler(type, method, instances);
|
var handler = CreateHandler(type, method, instances);
|
||||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||||
{
|
{
|
||||||
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +81,195 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
{
|
{
|
||||||
_isFrozen = true;
|
_isFrozen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WarmHleTypeInitializers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A .cctor or first JIT running on a guest thread's hijacked stack fail-fasts the CLR.
|
||||||
|
// Run every HLE type's initializer and JIT its methods here first, on a host thread.
|
||||||
|
private void WarmHleTypeInitializers()
|
||||||
|
{
|
||||||
|
Assembly[] assemblies;
|
||||||
|
lock (_registrationGate)
|
||||||
|
{
|
||||||
|
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
assemblies = WithGuestReachableDependencies(assemblies);
|
||||||
|
var bclWarmed = WarmFrameworkTypeInitializers();
|
||||||
|
|
||||||
|
var warmed = 0;
|
||||||
|
var failed = 0;
|
||||||
|
var jitted = 0;
|
||||||
|
var jitFailed = 0;
|
||||||
|
foreach (var assembly in assemblies)
|
||||||
|
{
|
||||||
|
Type[] types;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
types = assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException ex)
|
||||||
|
{
|
||||||
|
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BindingFlags allMembers = BindingFlags.Public | BindingFlags.NonPublic |
|
||||||
|
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||||
|
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
if (type is null || type.ContainsGenericParameters)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||||
|
warmed++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// A throw here beats a guest-thread fail-fast later; swallow and continue.
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force-JIT (not execute) every method so no guest thread compiles one first.
|
||||||
|
MethodBase[] members;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
members = type.GetConstructors(allMembers)
|
||||||
|
.Concat<MethodBase>(type.GetMethods(allMembers))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var member in members)
|
||||||
|
{
|
||||||
|
if (member.ContainsGenericParameters || member.IsAbstract || member.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RuntimeHelpers.PrepareMethod(member.MethodHandle);
|
||||||
|
jitted++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
jitFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[HLE] Warmed {warmed} type initializers ({failed} threw) + JIT-compiled {jitted} methods ({jitFailed} skipped) across {assemblies.Length} HLE assemblies, plus {bclWarmed} framework type initializers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Framework .cctors too (but not JIT — the BCL is too large).
|
||||||
|
private static int WarmFrameworkTypeInitializers()
|
||||||
|
{
|
||||||
|
var warmed = 0;
|
||||||
|
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
{
|
||||||
|
var name = assembly.GetName().Name;
|
||||||
|
if (name is null || !IsFrameworkAssembly(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type[] types;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
types = assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException ex)
|
||||||
|
{
|
||||||
|
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
if (type is null || type.ContainsGenericParameters)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||||
|
warmed++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return warmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFrameworkAssembly(string assemblyName) =>
|
||||||
|
assemblyName.StartsWith("System", StringComparison.Ordinal) ||
|
||||||
|
string.Equals(assemblyName, "netstandard", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// Warm the interop assemblies guest threads reach (e.g. Silk.NET via the flip path).
|
||||||
|
private static Assembly[] WithGuestReachableDependencies(Assembly[] scanned)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
|
||||||
|
foreach (var assembly in scanned)
|
||||||
|
{
|
||||||
|
result[assembly.FullName ?? assembly.GetName().Name ?? string.Empty] = assembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var assembly in scanned)
|
||||||
|
{
|
||||||
|
AssemblyName[] references;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
references = assembly.GetReferencedAssemblies();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var reference in references)
|
||||||
|
{
|
||||||
|
var name = reference.Name;
|
||||||
|
if (name is null || !IsGuestReachableInterop(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var loaded = Assembly.Load(reference);
|
||||||
|
result[loaded.FullName ?? name] = loaded;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGuestReachableInterop(string assemblyName) =>
|
||||||
|
assemblyName.StartsWith("Silk.NET", StringComparison.Ordinal) ||
|
||||||
|
assemblyName.StartsWith("SharpEmu", StringComparison.Ordinal);
|
||||||
|
|
||||||
public bool TryGetFunction(string nid, out Delegate function)
|
public bool TryGetFunction(string nid, out Delegate function)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
@@ -109,7 +301,7 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
|
|
||||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||||
{
|
{
|
||||||
Log.Warning($"NID '{nid}' not found in dispatch table.");
|
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
|
||||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
return false;
|
return false;
|
||||||
@@ -117,7 +309,7 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
|
|
||||||
if ((export.Target & context.TargetGeneration) == 0)
|
if ((export.Target & context.TargetGeneration) == 0)
|
||||||
{
|
{
|
||||||
Log.Warning($"NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
|
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, string> _mutexWakeKeys = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, string> _condWakeKeys = new();
|
||||||
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
||||||
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
||||||
private static readonly Dictionary<ulong, object> _onceGates = new();
|
private static readonly Dictionary<ulong, object> _onceGates = new();
|
||||||
@@ -35,6 +37,13 @@ public static class KernelPthreadCompatExports
|
|||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
||||||
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
||||||
|
private static readonly bool _enableMutexLockBlocking =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_MUTEX_LOCK_BLOCKING"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// On the outer class deliberately: a static on the nested state classes gives them a
|
||||||
|
// type initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||||
|
private static long _nextMutexWakeId;
|
||||||
|
private static long _nextCondWakeId;
|
||||||
|
|
||||||
private sealed class PthreadMutexState
|
private sealed class PthreadMutexState
|
||||||
{
|
{
|
||||||
@@ -43,6 +52,10 @@ public static class KernelPthreadCompatExports
|
|||||||
public int RecursionCount { get; set; }
|
public int RecursionCount { get; set; }
|
||||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||||
public int Protocol { get; set; }
|
public int Protocol { get; set; }
|
||||||
|
|
||||||
|
// Keyed on state identity, not the resolved address (which can differ between a
|
||||||
|
// lock and its unlock for the same state, stranding the waiter).
|
||||||
|
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadMutexWaiter
|
private sealed class PthreadMutexWaiter
|
||||||
@@ -56,6 +69,23 @@ public static class KernelPthreadCompatExports
|
|||||||
public object SyncRoot { get; } = new();
|
public object SyncRoot { get; } = new();
|
||||||
public ulong SignalEpoch { get; set; }
|
public ulong SignalEpoch { get; set; }
|
||||||
public int Waiters { get; set; }
|
public int Waiters { get; set; }
|
||||||
|
|
||||||
|
// See PthreadMutexState.WakeKey.
|
||||||
|
public string WakeKey { get; } = "pthread_cond#" + Interlocked.Increment(ref _nextCondWakeId).ToString("X");
|
||||||
|
|
||||||
|
// A signal with no waiter stays pending; the guest often signals before the wait.
|
||||||
|
public int PendingSignals { get; set; }
|
||||||
|
|
||||||
|
public bool TryConsumePendingSignal()
|
||||||
|
{
|
||||||
|
if (PendingSignals <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingSignals--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
||||||
@@ -112,6 +142,27 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "GBUY7ywdULE",
|
||||||
|
ExportName = "scePthreadRename",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadRename(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (_tracePthreads)
|
||||||
|
{
|
||||||
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var name = nameAddress != 0 &&
|
||||||
|
KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, nameAddress, 64, out var value)
|
||||||
|
? value
|
||||||
|
: "<unreadable>";
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] pthread.rename thread=0x{ctx[CpuRegister.Rdi]:X16} name=\"{name}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "EI-5-jlq2dE",
|
Nid = "EI-5-jlq2dE",
|
||||||
ExportName = "scePthreadGetthreadid",
|
ExportName = "scePthreadGetthreadid",
|
||||||
@@ -293,6 +344,13 @@ public static class KernelPthreadCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "27bAgiJmOh0",
|
||||||
|
ExportName = "pthread_cond_timedwait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondTimedwait(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "kDh-NfxgMtE",
|
Nid = "kDh-NfxgMtE",
|
||||||
ExportName = "scePthreadCondSignal",
|
ExportName = "scePthreadCondSignal",
|
||||||
@@ -327,12 +385,6 @@ public static class KernelPthreadCompatExports
|
|||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "27bAgiJmOh0",
|
|
||||||
ExportName = "pthread_cond_timedwait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "m5-2bsNfv7s",
|
Nid = "m5-2bsNfv7s",
|
||||||
@@ -559,42 +611,42 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal/adaptive mutexes do not report EDEADLK on self-lock.
|
|
||||||
// Under the current single-host-thread guest execution model,
|
|
||||||
// treating them as nested ownership keeps init paths moving
|
|
||||||
// without turning a would-block path into a hard error.
|
|
||||||
state.RecursionCount++;
|
state.RecursionCount++;
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var ownedResult = tryOnly
|
{
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
var ownedResult = tryOnly
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
return ownedResult;
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
||||||
|
return ownedResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var acquired = state.Semaphore.Wait(0);
|
var acquired = state.Semaphore.Wait(0);
|
||||||
if (!acquired)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
|
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||||
// Demon's Souls deadlocks during PS5SyncEvent initialization.
|
// Cooperative deschedule on a contended lock corrupts fiber state
|
||||||
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
// (Demon's Souls sceFiberSwitch -> ESRCH). Off by default: fall through to the
|
||||||
if (!tryOnly &&
|
// synchronous host-thread wait below.
|
||||||
|
if (_enableMutexLockBlocking &&
|
||||||
|
!tryOnly &&
|
||||||
GuestThreadExecution.IsGuestThread &&
|
GuestThreadExecution.IsGuestThread &&
|
||||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"pthread_mutex_lock",
|
"pthread_mutex_lock",
|
||||||
GetMutexWakeKey(resolvedAddress),
|
state.WakeKey,
|
||||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
} */
|
}
|
||||||
|
|
||||||
if (!tryOnly)
|
if (!tryOnly)
|
||||||
{
|
{
|
||||||
@@ -611,6 +663,7 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
|
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "lock");
|
||||||
state.OwnerThreadId = currentThreadId;
|
state.OwnerThreadId = currentThreadId;
|
||||||
state.RecursionCount = 1;
|
state.RecursionCount = 1;
|
||||||
}
|
}
|
||||||
@@ -633,16 +686,24 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
|
var lenientOwnership = state.Type is MutexTypeNormal or MutexTypeAdaptiveNp;
|
||||||
|
|
||||||
var shouldRelease = false;
|
var shouldRelease = false;
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
if (state.RecursionCount <= 0)
|
if (state.RecursionCount <= 0)
|
||||||
{
|
{
|
||||||
|
if (lenientOwnership)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireOwner && state.OwnerThreadId != currentThreadId)
|
if (requireOwner && !lenientOwnership && state.OwnerThreadId != currentThreadId)
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
@@ -661,12 +722,15 @@ public static class KernelPthreadCompatExports
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
state.Semaphore.Release();
|
state.Semaphore.Release();
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey, 1);
|
||||||
}
|
}
|
||||||
catch (SemaphoreFullException)
|
catch (SemaphoreFullException)
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
if (!lenientOwnership)
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1090,20 +1154,28 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
|
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
var spuriousWake = false;
|
var spuriousWake = false;
|
||||||
|
var releasedRecursion = 0;
|
||||||
lock (state.SyncRoot)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
state.Waiters++;
|
state.Waiters++;
|
||||||
var observedEpoch = state.SignalEpoch;
|
var observedEpoch = state.SignalEpoch;
|
||||||
TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, waitResult);
|
var consumedPendingSignal = state.TryConsumePendingSignal();
|
||||||
|
TracePthreadCond(
|
||||||
|
consumedPendingSignal ? "wait-enter-pending" : "wait-enter",
|
||||||
|
condAddress,
|
||||||
|
mutexAddress,
|
||||||
|
state,
|
||||||
|
timed,
|
||||||
|
waitResult);
|
||||||
|
|
||||||
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
var unlockResult = PthreadMutexFullUnlockForCondWait(ctx, mutexAddress, out releasedRecursion);
|
||||||
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
state.Waiters--;
|
state.Waiters--;
|
||||||
@@ -1111,10 +1183,25 @@ public static class KernelPthreadCompatExports
|
|||||||
return unlockResult;
|
return unlockResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var scheduler = GuestThreadExecution.Scheduler;
|
if (consumedPendingSignal)
|
||||||
if (!timed && GuestThreadExecution.RequestCurrentThreadBlock("pthread_cond_wait"))
|
|
||||||
{
|
{
|
||||||
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||||
|
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
|
||||||
|
var pendingLockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||||
|
return pendingLockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK ? pendingLockResult : waitResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||||
|
state.WakeKey,
|
||||||
|
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion),
|
||||||
|
() => state.SignalEpoch != observedEpoch,
|
||||||
|
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||||
|
{
|
||||||
|
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
||||||
return waitResult;
|
return waitResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,6 +1224,21 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
|
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
|
||||||
{
|
{
|
||||||
|
if (scheduler is not null && !GuestThreadExecution.IsGuestThread)
|
||||||
|
{
|
||||||
|
Monitor.Exit(state.SyncRoot);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scheduler.Pump(ctx, "pthread_cond_wait");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Enter(state.SyncRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
spuriousWake = true;
|
spuriousWake = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1163,7 +1265,7 @@ public static class KernelPthreadCompatExports
|
|||||||
waitResult);
|
waitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||||
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult);
|
TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult);
|
||||||
@@ -1189,16 +1291,19 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
|
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldWakeScheduler = false;
|
||||||
lock (state.SyncRoot)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
|
// Advance the epoch even with no waiter registered — it's the wait predicate.
|
||||||
|
state.SignalEpoch++;
|
||||||
if (state.Waiters > 0)
|
if (state.Waiters > 0)
|
||||||
{
|
{
|
||||||
state.SignalEpoch++;
|
shouldWakeScheduler = true;
|
||||||
if (broadcast)
|
if (broadcast)
|
||||||
{
|
{
|
||||||
Monitor.PulseAll(state.SyncRoot);
|
Monitor.PulseAll(state.SyncRoot);
|
||||||
@@ -1208,15 +1313,135 @@ public static class KernelPthreadCompatExports
|
|||||||
Monitor.Pulse(state.SyncRoot);
|
Monitor.Pulse(state.SyncRoot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.PendingSignals++;
|
||||||
|
}
|
||||||
|
|
||||||
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldWakeScheduler)
|
||||||
|
{
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(
|
||||||
|
state.WakeKey,
|
||||||
|
broadcast ? int.MaxValue : 1);
|
||||||
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ResumePthreadCondWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong condAddress,
|
||||||
|
ulong mutexAddress,
|
||||||
|
PthreadCondState state,
|
||||||
|
ulong observedEpoch,
|
||||||
|
bool timed,
|
||||||
|
int releasedRecursion)
|
||||||
|
{
|
||||||
|
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
lock (state.SyncRoot)
|
||||||
|
{
|
||||||
|
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||||
|
if (timed && state.SignalEpoch == observedEpoch)
|
||||||
|
{
|
||||||
|
waitResult = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadCond(
|
||||||
|
waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-resume" : "wait-resume-timeout",
|
||||||
|
condAddress,
|
||||||
|
mutexAddress,
|
||||||
|
state,
|
||||||
|
timed,
|
||||||
|
waitResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||||
|
return lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCondWakeKey(ulong resolvedCondAddress) =>
|
||||||
|
_condWakeKeys.GetOrAdd(resolvedCondAddress, static address => string.Create(
|
||||||
|
31,
|
||||||
|
address,
|
||||||
|
static (destination, value) =>
|
||||||
|
{
|
||||||
|
"pthread_cond:0x".AsSpan().CopyTo(destination);
|
||||||
|
_ = value.TryFormat(destination[15..], out _, "X16");
|
||||||
|
}));
|
||||||
|
|
||||||
|
private static int PthreadMutexFullUnlockForCondWait(CpuContext ctx, ulong mutexAddress, out int releasedRecursion)
|
||||||
|
{
|
||||||
|
releasedRecursion = 0;
|
||||||
|
if (!TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
|
||||||
|
{
|
||||||
|
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
if (state.RecursionCount <= 0 || state.OwnerThreadId != currentThreadId)
|
||||||
|
{
|
||||||
|
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
releasedRecursion = state.RecursionCount;
|
||||||
|
state.RecursionCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexRelockForCondWait(CpuContext ctx, ulong mutexAddress, int releasedRecursion)
|
||||||
|
{
|
||||||
|
var result = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
||||||
|
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releasedRecursion > 1 &&
|
||||||
|
TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
|
||||||
|
{
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
if (state.OwnerThreadId == KernelPthreadState.GetCurrentThreadHandle())
|
||||||
|
{
|
||||||
|
state.RecursionCount = releasedRecursion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? DumpMutexStateForStall(ulong mutexAddress)
|
||||||
|
{
|
||||||
|
if (!_mutexStates.TryGetValue(mutexAddress, out var state))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var waiterThreads = string.Join(",", _mutexStates
|
||||||
|
.Where(pair => ReferenceEquals(pair.Value, state))
|
||||||
|
.Select(pair => $"0x{pair.Key:X}"));
|
||||||
|
|
||||||
|
return $"mutex=0x{mutexAddress:X16} owner=0x{state.OwnerThreadId:X} recursion={state.RecursionCount} " +
|
||||||
|
$"type={state.Type} semaphore_count={state.Semaphore.CurrentCount} aliases=[{waiterThreads}]";
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
|
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
|
||||||
$"pthread_mutex:0x{resolvedMutexAddress:X16}";
|
_mutexWakeKeys.GetOrAdd(resolvedMutexAddress, static address => string.Create(
|
||||||
|
32,
|
||||||
|
address,
|
||||||
|
static (destination, value) =>
|
||||||
|
{
|
||||||
|
"pthread_mutex:0x".AsSpan().CopyTo(destination);
|
||||||
|
_ = value.TryFormat(destination[16..], out _, "X16");
|
||||||
|
}));
|
||||||
|
|
||||||
private static bool TryReserveBlockedMutexLock(
|
private static bool TryReserveBlockedMutexLock(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
@@ -1233,6 +1458,13 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.Semaphore.Wait(0))
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectMutexDoubleOwner(resolvedAddress, state, waiter.ThreadId, "reserve");
|
||||||
state.OwnerThreadId = waiter.ThreadId;
|
state.OwnerThreadId = waiter.ThreadId;
|
||||||
state.RecursionCount = 1;
|
state.RecursionCount = 1;
|
||||||
Interlocked.Exchange(ref waiter.Reserved, 1);
|
Interlocked.Exchange(ref waiter.Reserved, 1);
|
||||||
@@ -1264,6 +1496,7 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
|
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "resume");
|
||||||
state.OwnerThreadId = currentThreadId;
|
state.OwnerThreadId = currentThreadId;
|
||||||
state.RecursionCount = 1;
|
state.RecursionCount = 1;
|
||||||
}
|
}
|
||||||
@@ -1272,6 +1505,19 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A nonzero foreign prior owner means two guest threads are in the same critical
|
||||||
|
// section. Call while holding lock(state).
|
||||||
|
private static void DetectMutexDoubleOwner(ulong resolvedAddress, PthreadMutexState state, ulong currentThreadId, string site)
|
||||||
|
{
|
||||||
|
var priorOwner = state.OwnerThreadId;
|
||||||
|
if (priorOwner != 0 && priorOwner != currentThreadId)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] MUTEX DOUBLE-OWNER at {site}: resolved=0x{resolvedAddress:X} acquirer=0x{currentThreadId:X} " +
|
||||||
|
$"prior_owner=0x{priorOwner:X} recursion={state.RecursionCount} type={state.Type} sem_count={state.Semaphore.CurrentCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
||||||
{
|
{
|
||||||
if (timeoutUsec == 0)
|
if (timeoutUsec == 0)
|
||||||
@@ -1409,7 +1655,8 @@ public static class KernelPthreadCompatExports
|
|||||||
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
||||||
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
||||||
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
|
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
|
||||||
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}");
|
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8} " +
|
||||||
|
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} managed={Environment.CurrentManagedThreadId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private static long _nextSyntheticRwlockHandleId = 1;
|
private static long _nextSyntheticRwlockHandleId = 1;
|
||||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||||
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
||||||
|
private static readonly bool _strictRwlockWriterPreference =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_STRICT_RWLOCK_WRITER_PREFERENCE"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||||
|
|
||||||
@@ -85,14 +87,23 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On the outer class deliberately: a static on the nested state class gives it a type
|
||||||
|
// initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||||
|
private static long _nextRwlockWakeId;
|
||||||
|
|
||||||
private sealed class PthreadRwlockState
|
private sealed class PthreadRwlockState
|
||||||
{
|
{
|
||||||
public object SyncRoot { get; } = new();
|
public object SyncRoot { get; } = new();
|
||||||
public Dictionary<ulong, int> ReaderCounts { get; } = new();
|
public Dictionary<ulong, int> ReaderCounts { get; } = new();
|
||||||
|
public Dictionary<ulong, int> CompatWriterCounts { get; } = new();
|
||||||
public int ReaderTotalCount { get; set; }
|
public int ReaderTotalCount { get; set; }
|
||||||
|
public int CompatWriterTotalCount { get; set; }
|
||||||
public ulong WriterThreadId { get; set; }
|
public ulong WriterThreadId { get; set; }
|
||||||
public int WaitingWriters { get; set; }
|
public int WaitingWriters { get; set; }
|
||||||
|
|
||||||
|
// See PthreadMutexState.WakeKey.
|
||||||
|
public string WakeKey { get; } = "pthread_rwlock#" + Interlocked.Increment(ref _nextRwlockWakeId).ToString("X");
|
||||||
|
|
||||||
public int GetReaderCount(ulong threadId)
|
public int GetReaderCount(ulong threadId)
|
||||||
{
|
{
|
||||||
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
|
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
|
||||||
@@ -105,6 +116,33 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
ReaderTotalCount++;
|
ReaderTotalCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddCompatWriter(ulong threadId)
|
||||||
|
{
|
||||||
|
CompatWriterCounts.TryGetValue(threadId, out var currentCount);
|
||||||
|
CompatWriterCounts[threadId] = currentCount + 1;
|
||||||
|
CompatWriterTotalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveCompatWriter(ulong threadId)
|
||||||
|
{
|
||||||
|
if (!CompatWriterCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCount == 1)
|
||||||
|
{
|
||||||
|
CompatWriterCounts.Remove(threadId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CompatWriterCounts[threadId] = currentCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompatWriterTotalCount = Math.Max(0, CompatWriterTotalCount - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public bool RemoveReader(ulong threadId)
|
public bool RemoveReader(ulong threadId)
|
||||||
{
|
{
|
||||||
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
||||||
@@ -935,7 +973,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
|
|
||||||
lock (state.SyncRoot)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0)
|
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0 || state.CompatWriterTotalCount != 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
@@ -1003,7 +1041,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock))
|
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out var resolvedAddress, out var rwlock))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -1014,7 +1052,11 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
{
|
{
|
||||||
lock (rwlock.SyncRoot)
|
lock (rwlock.SyncRoot)
|
||||||
{
|
{
|
||||||
if (rwlock.WriterThreadId == currentThreadId)
|
if (rwlock.RemoveCompatWriter(currentThreadId))
|
||||||
|
{
|
||||||
|
Monitor.PulseAll(rwlock.SyncRoot);
|
||||||
|
}
|
||||||
|
else if (rwlock.WriterThreadId == currentThreadId)
|
||||||
{
|
{
|
||||||
rwlock.WriterThreadId = 0;
|
rwlock.WriterThreadId = 0;
|
||||||
Monitor.PulseAll(rwlock.SyncRoot);
|
Monitor.PulseAll(rwlock.SyncRoot);
|
||||||
@@ -1037,6 +1079,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,7 +1270,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock))
|
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -1242,20 +1285,60 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
|
||||||
|
{
|
||||||
|
rwlock.AddCompatWriter(currentThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
!_strictRwlockWriterPreference &&
|
||||||
|
rwlock.WriterThreadId == 0 &&
|
||||||
|
rwlock.ReaderTotalCount == 0 &&
|
||||||
|
rwlock.CompatWriterTotalCount == 0)
|
||||||
|
{
|
||||||
|
rwlock.AddCompatWriter(currentThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rwlock.WriterThreadId == 0 && rwlock.ReaderTotalCount == 0 && rwlock.CompatWriterTotalCount == 0)
|
||||||
|
{
|
||||||
|
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "wrlock");
|
||||||
|
rwlock.WriterThreadId = currentThreadId;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
rwlock.WaitingWriters++;
|
rwlock.WaitingWriters++;
|
||||||
|
var transferredToScheduler = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0)
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"pthread_rwlock_wrlock",
|
||||||
|
rwlock.WakeKey,
|
||||||
|
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
|
||||||
|
{
|
||||||
|
transferredToScheduler = true;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||||
{
|
{
|
||||||
Monitor.Wait(rwlock.SyncRoot);
|
Monitor.Wait(rwlock.SyncRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rwlock.WriterThreadId = currentThreadId;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
rwlock.WaitingWriters--;
|
if (!transferredToScheduler)
|
||||||
|
{
|
||||||
|
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rwlock.WriterThreadId = currentThreadId;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1264,12 +1347,30 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (rwlock.WriterThreadId != 0 ||
|
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
|
|
||||||
{
|
{
|
||||||
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"pthread_rwlock_rdlock",
|
||||||
|
rwlock.WakeKey,
|
||||||
|
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
Monitor.Wait(rwlock.SyncRoot);
|
Monitor.Wait(rwlock.SyncRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rwlock.WriterThreadId != 0 ||
|
||||||
|
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] RWLOCK READER/WRITER COEXIST: resolved=0x{resolvedAddress:X} reader=0x{currentThreadId:X} " +
|
||||||
|
$"writer=0x{rwlock.WriterThreadId:X} compat_total={rwlock.CompatWriterTotalCount} readers_total={rwlock.ReaderTotalCount}");
|
||||||
|
}
|
||||||
rwlock.AddReader(currentThreadId);
|
rwlock.AddReader(currentThreadId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1277,6 +1378,86 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryAcquireBlockedRwlock(PthreadRwlockState rwlock, ulong currentThreadId, bool write)
|
||||||
|
{
|
||||||
|
lock (rwlock.SyncRoot)
|
||||||
|
{
|
||||||
|
if (write)
|
||||||
|
{
|
||||||
|
if (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectRwlockWriterConflict(0, rwlock, currentThreadId, "wrlock-resume");
|
||||||
|
rwlock.WriterThreadId = currentThreadId;
|
||||||
|
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rwlock.AddReader(currentThreadId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call while holding lock(rwlock.SyncRoot): an existing reader/writer here means a
|
||||||
|
// writer would share the rwlock with another holder — a data race.
|
||||||
|
private static void DetectRwlockWriterConflict(ulong resolvedAddress, PthreadRwlockState rwlock, ulong currentThreadId, string site)
|
||||||
|
{
|
||||||
|
if (rwlock.WriterThreadId != 0 ||
|
||||||
|
rwlock.ReaderTotalCount != 0 ||
|
||||||
|
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] RWLOCK WRITER CONFLICT at {site}: resolved=0x{resolvedAddress:X} writer=0x{currentThreadId:X} " +
|
||||||
|
$"existing_writer=0x{rwlock.WriterThreadId:X} readers_total={rwlock.ReaderTotalCount} compat_total={rwlock.CompatWriterTotalCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ReaderMustWaitForRwlock(PthreadRwlockState rwlock, ulong currentThreadId)
|
||||||
|
{
|
||||||
|
if (rwlock.WriterThreadId != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rwlock.WaitingWriters > 0 &&
|
||||||
|
rwlock.GetReaderCount(currentThreadId) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
|
||||||
|
|
||||||
|
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
|
||||||
|
{
|
||||||
|
PthreadRwlockState? rwlock;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_rwlockStates.TryGetValue(rwlockAddress, out rwlock))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (rwlock.SyncRoot)
|
||||||
|
{
|
||||||
|
var readers = string.Join(",", rwlock.ReaderCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
|
||||||
|
var compatWriters = string.Join(",", rwlock.CompatWriterCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
|
||||||
|
return $"rwlock=0x{rwlockAddress:X16} writer=0x{rwlock.WriterThreadId:X} waiting_writers={rwlock.WaitingWriters} " +
|
||||||
|
$"readers_total={rwlock.ReaderTotalCount} readers=[{readers}] " +
|
||||||
|
$"compat_writers_total={rwlock.CompatWriterTotalCount} compat_writers=[{compatWriters}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static ulong ResolveRwlockHandle(CpuContext ctx, ulong rwlockAddress)
|
private static ulong ResolveRwlockHandle(CpuContext ctx, ulong rwlockAddress)
|
||||||
{
|
{
|
||||||
if (rwlockAddress == 0)
|
if (rwlockAddress == 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user