mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-16 00:40:59 +00: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;
|
||||
}
|
||||
|
||||
// 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
|
||||
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||
@@ -888,12 +891,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
||||
"1471ajPzxh0" or // pthread_rwlock_destroy
|
||||
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
||||
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
|
||||
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
|
||||
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
|
||||
"mqdNorrB+gI" or // scePthreadRwlockWrlock
|
||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||
// rwlock rd/wr lock removed (can block); init/destroy/unlock stay.
|
||||
"aI+OeCz8xrQ" or // scePthreadSelf
|
||||
"EotR8a3ASf4" or // pthread_self
|
||||
"eoht7mQOCmo" or // scePthreadGetspecific
|
||||
|
||||
@@ -2782,11 +2782,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_guestThreadPumpDepth != 0)
|
||||
if (Interlocked.CompareExchange(ref _guestThreadPumpDepth, 1, 0) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_guestThreadPumpDepth++;
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
@@ -2832,7 +2831,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
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;
|
||||
@@ -4422,6 +4430,26 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return;
|
||||
}
|
||||
_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);
|
||||
_stallWatchdogThread = new Thread(new ThreadStart(delegate
|
||||
{
|
||||
@@ -4450,6 +4478,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
MarkExecutionProgress();
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
@@ -4485,6 +4522,38 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
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()
|
||||
{
|
||||
_stallWatchdogStop = true;
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using SharpEmu.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
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, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||
private readonly object _registrationGate = new();
|
||||
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||
private bool _isFrozen;
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
// Deduplicated: one assembly is reached through many types.
|
||||
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var registeredCount = 0;
|
||||
var instances = new Dictionary<Type, object>();
|
||||
|
||||
@@ -50,7 +55,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -76,8 +81,195 @@ public sealed class ModuleManager : IModuleManager
|
||||
{
|
||||
_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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
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);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
@@ -117,7 +309,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
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);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
return false;
|
||||
|
||||
@@ -24,6 +24,8 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
private static readonly object _stateGate = 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, PthreadCondState> _condStates = 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);
|
||||
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
||||
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
|
||||
{
|
||||
@@ -43,6 +52,10 @@ public static class KernelPthreadCompatExports
|
||||
public int RecursionCount { get; set; }
|
||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||
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
|
||||
@@ -56,6 +69,23 @@ public static class KernelPthreadCompatExports
|
||||
public object SyncRoot { get; } = new();
|
||||
public ulong SignalEpoch { 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);
|
||||
@@ -112,6 +142,27 @@ public static class KernelPthreadCompatExports
|
||||
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(
|
||||
Nid = "EI-5-jlq2dE",
|
||||
ExportName = "scePthreadGetthreadid",
|
||||
@@ -293,6 +344,13 @@ public static class KernelPthreadCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
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(
|
||||
Nid = "kDh-NfxgMtE",
|
||||
ExportName = "scePthreadCondSignal",
|
||||
@@ -327,12 +385,6 @@ public static class KernelPthreadCompatExports
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
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(
|
||||
Nid = "m5-2bsNfv7s",
|
||||
@@ -559,42 +611,42 @@ public static class KernelPthreadCompatExports
|
||||
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++;
|
||||
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;
|
||||
else
|
||||
{
|
||||
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 acquired = state.Semaphore.Wait(0);
|
||||
if (!acquired)
|
||||
{
|
||||
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
|
||||
// Demon's Souls deadlocks during PS5SyncEvent initialization.
|
||||
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||
if (!tryOnly &&
|
||||
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||
// Cooperative deschedule on a contended lock corrupts fiber state
|
||||
// (Demon's Souls sceFiberSwitch -> ESRCH). Off by default: fall through to the
|
||||
// synchronous host-thread wait below.
|
||||
if (_enableMutexLockBlocking &&
|
||||
!tryOnly &&
|
||||
GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
GetMutexWakeKey(resolvedAddress),
|
||||
state.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
} */
|
||||
}
|
||||
|
||||
if (!tryOnly)
|
||||
{
|
||||
@@ -611,6 +663,7 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
lock (state)
|
||||
{
|
||||
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "lock");
|
||||
state.OwnerThreadId = currentThreadId;
|
||||
state.RecursionCount = 1;
|
||||
}
|
||||
@@ -633,16 +686,24 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
var lenientOwnership = state.Type is MutexTypeNormal or MutexTypeAdaptiveNp;
|
||||
|
||||
var shouldRelease = false;
|
||||
lock (state)
|
||||
{
|
||||
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);
|
||||
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);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
@@ -661,12 +722,15 @@ public static class KernelPthreadCompatExports
|
||||
try
|
||||
{
|
||||
state.Semaphore.Release();
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey, 1);
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
if (!lenientOwnership)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var spuriousWake = false;
|
||||
var releasedRecursion = 0;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
state.Waiters++;
|
||||
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)
|
||||
{
|
||||
state.Waiters--;
|
||||
@@ -1111,10 +1183,25 @@ public static class KernelPthreadCompatExports
|
||||
return unlockResult;
|
||||
}
|
||||
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (!timed && GuestThreadExecution.RequestCurrentThreadBlock("pthread_cond_wait"))
|
||||
if (consumedPendingSignal)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1137,6 +1224,21 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
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;
|
||||
break;
|
||||
}
|
||||
@@ -1163,7 +1265,7 @@ public static class KernelPthreadCompatExports
|
||||
waitResult);
|
||||
}
|
||||
|
||||
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
||||
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var shouldWakeScheduler = false;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
// Advance the epoch even with no waiter registered — it's the wait predicate.
|
||||
state.SignalEpoch++;
|
||||
if (state.Waiters > 0)
|
||||
{
|
||||
state.SignalEpoch++;
|
||||
shouldWakeScheduler = true;
|
||||
if (broadcast)
|
||||
{
|
||||
Monitor.PulseAll(state.SyncRoot);
|
||||
@@ -1208,15 +1313,135 @@ public static class KernelPthreadCompatExports
|
||||
Monitor.Pulse(state.SyncRoot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.PendingSignals++;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) =>
|
||||
$"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(
|
||||
CpuContext ctx,
|
||||
@@ -1233,6 +1458,13 @@ public static class KernelPthreadCompatExports
|
||||
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.RecursionCount = 1;
|
||||
Interlocked.Exchange(ref waiter.Reserved, 1);
|
||||
@@ -1264,6 +1496,7 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
lock (state)
|
||||
{
|
||||
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "resume");
|
||||
state.OwnerThreadId = currentThreadId;
|
||||
state.RecursionCount = 1;
|
||||
}
|
||||
@@ -1272,6 +1505,19 @@ public static class KernelPthreadCompatExports
|
||||
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)
|
||||
{
|
||||
if (timeoutUsec == 0)
|
||||
@@ -1409,7 +1655,8 @@ public static class KernelPthreadCompatExports
|
||||
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
||||
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1: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)
|
||||
|
||||
@@ -31,6 +31,8 @@ public static class KernelPthreadExtendedCompatExports
|
||||
private static long _nextSyntheticRwlockHandleId = 1;
|
||||
private static long _nextSyntheticPthreadAttrHandleId = 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();
|
||||
|
||||
@@ -85,14 +87,23 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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
|
||||
{
|
||||
public object SyncRoot { get; } = new();
|
||||
public Dictionary<ulong, int> ReaderCounts { get; } = new();
|
||||
public Dictionary<ulong, int> CompatWriterCounts { get; } = new();
|
||||
public int ReaderTotalCount { get; set; }
|
||||
public int CompatWriterTotalCount { get; set; }
|
||||
public ulong WriterThreadId { 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)
|
||||
{
|
||||
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
|
||||
@@ -105,6 +116,33 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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)
|
||||
{
|
||||
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
||||
@@ -935,7 +973,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1003,7 +1041,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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;
|
||||
}
|
||||
@@ -1014,7 +1052,11 @@ public static class KernelPthreadExtendedCompatExports
|
||||
{
|
||||
lock (rwlock.SyncRoot)
|
||||
{
|
||||
if (rwlock.WriterThreadId == currentThreadId)
|
||||
if (rwlock.RemoveCompatWriter(currentThreadId))
|
||||
{
|
||||
Monitor.PulseAll(rwlock.SyncRoot);
|
||||
}
|
||||
else if (rwlock.WriterThreadId == currentThreadId)
|
||||
{
|
||||
rwlock.WriterThreadId = 0;
|
||||
Monitor.PulseAll(rwlock.SyncRoot);
|
||||
@@ -1037,6 +1079,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1227,7 +1270,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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;
|
||||
}
|
||||
@@ -1242,20 +1285,60 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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++;
|
||||
var transferredToScheduler = false;
|
||||
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);
|
||||
}
|
||||
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
}
|
||||
finally
|
||||
{
|
||||
rwlock.WaitingWriters--;
|
||||
if (!transferredToScheduler)
|
||||
{
|
||||
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||
}
|
||||
}
|
||||
|
||||
rwlock.WriterThreadId = currentThreadId;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1264,12 +1347,30 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||
}
|
||||
|
||||
while (rwlock.WriterThreadId != 0 ||
|
||||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
|
||||
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1277,6 +1378,86 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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)
|
||||
{
|
||||
if (rwlockAddress == 0)
|
||||
|
||||
Reference in New Issue
Block a user