diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index e245dab..befc6ce 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -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 diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index b213ca8..f4ad3f6 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -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; diff --git a/src/SharpEmu.HLE/ModuleManager.cs b/src/SharpEmu.HLE/ModuleManager.cs index 4484278..3e116e5 100644 --- a/src/SharpEmu.HLE/ModuleManager.cs +++ b/src/SharpEmu.HLE/ModuleManager.cs @@ -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 _dispatchTable = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _exportTable = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _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(); @@ -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(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(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; diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 5721229..8c89b4f 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -24,6 +24,8 @@ public static class KernelPthreadCompatExports private static readonly object _stateGate = new(); private static readonly ConcurrentDictionary _mutexStates = new(); + private static readonly ConcurrentDictionary _mutexWakeKeys = new(); + private static readonly ConcurrentDictionary _condWakeKeys = new(); private static readonly Dictionary _mutexAttrStates = new(); private static readonly Dictionary _condStates = new(); private static readonly Dictionary _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? _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 + : ""; + 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) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index 39e96ab..ea83603 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -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> _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 ReaderCounts { get; } = new(); + public Dictionary 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)