From 73e8821d5bc54760002ab707bac63feb5ee56335 Mon Sep 17 00:00:00 2001 From: Youss <156013413+ftre120@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:33:21 +0200 Subject: [PATCH] [Kernel] Hand off mutex ownership directly to the head waiter on unlock (#439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pthread_mutex_unlock cleared ownership (OwnerThreadId = 0) and only woke the head waiter, relying on that woken thread to re-acquire the lock itself. If the wake raced or was lost, the mutex was left "free but with a queued waiter" — a state the fast-acquire path in PthreadMutexLockCore explicitly refuses (OwnerThreadId == 0 && Waiters.Count == 0), so every later locker, including the game's main thread, queued behind a head that never advanced and the whole process wedged. Grant the mutex to the head waiter directly inside unlock (the same TryGrantMutexWaiterLocked hand-off the thread-exit cleanup already uses), then wake it. The mutex is therefore never observable as free-with-waiter. Verified against The Invincible (PPSA06426): forward progress jumps from ~3.5M to ~40M dispatched imports and the repeated unlock INVALID_ARGUMENT errors disappear. No regression on Dead Cells (PPSA15552), which still reaches AGC rendering with zero mutex errors. --- .../Kernel/KernelPthreadCompatExports.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index cd2c92c2..72e5c9de 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -777,9 +777,21 @@ public static class KernelPthreadCompatExports if (state.RecursionCount == 0) { state.OwnerThreadId = 0; - nextWakeKey = state.Waiters.First?.Value.Cooperative == true - ? state.Waiters.First.Value.WakeKey - : null; + + // Hand the mutex directly to the head waiter instead of only + // waking it and relying on it to re-acquire. A woken waiter that + // fails to self-grant (its wake races or is lost) would leave the + // mutex "free with a queued waiter"; the fast-acquire path refuses + // such a mutex (OwnerThreadId == 0 && Waiters.Count == 0), so every + // later locker — including the game's main thread — then queues + // behind a head that never advances and the process wedges. + if (state.Waiters.First is { } headNode && + TryGrantMutexWaiterLocked(state, headNode.Value) && + headNode.Value.Cooperative) + { + nextWakeKey = headNode.Value.WakeKey; + } + Monitor.PulseAll(state); } }