[Kernel] Phase 4: rwlocks block in place; park-state diagnostics

Fourth phase of the 1:1 threading rework. pthread rwlocks were the last
primitive on the cooperative path; with them converted, nothing calls
RequestCurrentThreadBlock any more, so the continuation/wake-key machinery
(including the nested-callback Thread.Sleep(1) spin the primary thread used)
is unreachable by construction — the main thread now parks in place inside
HLE calls like every other thread.

- PthreadRwlockLockCore: drop the RequestCurrentThreadBlock branches bolted
  in front of the existing Monitor.Wait loops; writers and readers park in
  place on SyncRoot (sliced for teardown). Removed RwlockWaiter,
  TryAcquireBlockedRwlock, the per-lock WakeKey, and the unlock's
  WakeBlockedThreads dispatch.
- Diagnostics: in-place-parked threads previously reported state=Running
  block=none, blinding the stall watchdog. GuestThreadBlocking now tracks
  what each parked thread waits on (recorded only on the contended slow
  path); the stall dump's per-thread block= field falls back to it, and a
  new "Stall in-place blocks" line lists every parked thread — covering the
  primary thread, which has no _guestThreads entry.

The new diagnostics immediately paid for themselves on Silent Hill f: all
38 parked threads are workers (cond_wait/WaitEqueue); the main thread is
NOT parked in any primitive — it spins in guest code calling the
scePthreadGetspecific leaf (leaf fast-path calls are neither counted nor
traced), i.e. it polls a TLS slot that is never filled. That relocates the
game's wall from "scheduler wedge" to "missing produced value," to be
chased in the bootstrap-cohort investigation.

Verified (Metal, headless): void 5.6M imports, Hades 34.4M, Dead Cells
10.1M, Dream Sara, Astro 6.8M (rwlock-heavy: zero writer-conflict reports),
Silent Hill / Lunar stable — zero unrecovered crashes across all eight.
All 351 Libs tests pass.
This commit is contained in:
Gutemberg Ribeiro
2026-07-18 22:52:16 +01:00
parent ca67322243
commit c17c6ed303
7 changed files with 129 additions and 105 deletions
@@ -6033,6 +6033,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
Console.Error.WriteLine($"[LOADER][ERROR] Stall stack: [rsp]=0x{value:X16} [rsp+8]=0x{value2:X16}");
}
// Threads blocked in place on host primitives (incl. the primary
// thread, which has no _guestThreads entry) and what they wait on.
var parked = GuestThreadBlocking.SnapshotBlockDescriptions();
if (parked.Length != 0)
{
var builder = new System.Text.StringBuilder(64 + parked.Length * 40);
builder.Append("[LOADER][ERROR] Stall in-place blocks:");
foreach (var entry in parked)
{
builder.Append($" 0x{entry.Key:X16}={entry.Value}");
}
Console.Error.WriteLine(builder.ToString());
}
var threads = SnapshotGuestThreads();
if (threads.Length != 0)
{
@@ -6053,12 +6068,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
hostContextText = $" host_tid={hostThreadId} host_ctx=unavailable";
}
// Threads parked in place on a host primitive stay state=Running;
// GuestThreadBlocking records what they are parked on.
var blockText = thread.BlockReason
?? GuestThreadBlocking.DescribeBlock(thread.ThreadHandle)
?? "none";
Console.Error.WriteLine(
$"[LOADER][ERROR] Stall guest-thread: handle=0x{thread.ThreadHandle:X16} name='{thread.Name}' " +
$"state={thread.State} imports={Interlocked.Read(ref thread.ImportCount)} " +
$"nid={Volatile.Read(ref thread.LastImportNid) ?? "none"} ret=0x{Volatile.Read(ref thread.LastReturnRip):X16} " +
$"rdi=0x{Volatile.Read(ref thread.LastImportRdi):X16} rsi=0x{Volatile.Read(ref thread.LastImportRsi):X16} " +
$"rdx=0x{Volatile.Read(ref thread.LastImportRdx):X16} block={thread.BlockReason ?? "none"}{hostContextText}");
$"rdx=0x{Volatile.Read(ref thread.LastImportRdx):X16} block={blockText}{hostContextText}");
logged++;
if (logged >= 48 && threads.Length > logged)
{
+30
View File
@@ -22,9 +22,39 @@ public static class GuestThreadBlocking
private static volatile bool _shutdownRequested;
// Guest thread handle -> what it is parked on. Populated only while a
// thread is blocked (the slow path), read by the stall watchdog so
// in-place-blocked threads are not reported as opaque "Running" threads.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, string> _blockDescriptions = new();
/// <summary>True once emulator teardown has begun; parked guest threads unwind.</summary>
public static bool ShutdownRequested => _shutdownRequested;
/// <summary>Called by the execution backend when guest execution is being torn down.</summary>
public static void RequestShutdown() => _shutdownRequested = true;
/// <summary>Records what the given guest thread is about to park on (diagnostics only).</summary>
public static void NoteBlocked(ulong guestThreadHandle, string description)
{
if (guestThreadHandle != 0)
{
_blockDescriptions[guestThreadHandle] = description;
}
}
/// <summary>Clears the parked-state note recorded by <see cref="NoteBlocked"/>.</summary>
public static void NoteUnblocked(ulong guestThreadHandle)
{
if (guestThreadHandle != 0)
{
_blockDescriptions.TryRemove(guestThreadHandle, out _);
}
}
/// <summary>What the thread is parked on, or null if it is not parked in place.</summary>
public static string? DescribeBlock(ulong guestThreadHandle) =>
_blockDescriptions.TryGetValue(guestThreadHandle, out var description) ? description : null;
/// <summary>All currently parked threads (diagnostics; covers the primary thread too).</summary>
public static KeyValuePair<ulong, string>[] SnapshotBlockDescriptions() => _blockDescriptions.ToArray();
}
@@ -249,6 +249,7 @@ public static class KernelEventFlagCompatExports
// event flags: OR/AND over the bit pattern, optional clear).
state.WaitingThreads++;
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{returnRip:X16}");
GuestThreadBlocking.NoteBlocked(GuestThreadExecution.CurrentGuestThreadHandle, "sceKernelWaitEventFlag");
try
{
while (true)
@@ -282,6 +283,7 @@ public static class KernelEventFlagCompatExports
finally
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
GuestThreadBlocking.NoteUnblocked(GuestThreadExecution.CurrentGuestThreadHandle);
}
}
}
@@ -421,29 +421,37 @@ public static class KernelEventQueueCompatExports
}
TraceEventQueue(ctx, "wait-block", handle);
lock (_eventQueueGate)
GuestThreadBlocking.NoteBlocked(GuestThreadExecution.CurrentGuestThreadHandle, "sceKernelWaitEqueue");
try
{
while (true)
lock (_eventQueueGate)
{
if ((_pendingEvents.TryGetValue(handle, out var queue) && queue.Count != 0) ||
!_eventQueues.Contains(handle) ||
GuestThreadBlocking.ShutdownRequested)
while (true)
{
break;
}
if ((_pendingEvents.TryGetValue(handle, out var queue) && queue.Count != 0) ||
!_eventQueues.Contains(handle) ||
GuestThreadBlocking.ShutdownRequested)
{
break;
}
var remaining = deadline - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
break;
}
var remaining = deadline - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
break;
}
var slice = timeoutAddress == 0
? GuestThreadBlocking.WaitSliceMilliseconds
: (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds);
_ = Monitor.Wait(_eventQueueGate, slice);
var slice = timeoutAddress == 0
? GuestThreadBlocking.WaitSliceMilliseconds
: (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds);
_ = Monitor.Wait(_eventQueueGate, slice);
}
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(GuestThreadExecution.CurrentGuestThreadHandle);
}
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
@@ -691,6 +691,7 @@ public static class KernelPthreadCompatExports
// parks, so an unlock's PulseAll cannot be missed. Waits are
// sliced only so teardown can unwind parked threads.
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_mutex_lock");
state.WaiterCount++;
try
{
@@ -711,6 +712,7 @@ public static class KernelPthreadCompatExports
finally
{
state.WaiterCount--;
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
@@ -1260,22 +1262,30 @@ public static class KernelPthreadCompatExports
var deadline = timed
? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec))
: long.MaxValue;
while (state.SignalsPending == 0 && !GuestThreadBlocking.ShutdownRequested)
GuestThreadBlocking.NoteBlocked(currentThreadId, timed ? "pthread_cond_timedwait" : "pthread_cond_wait");
try
{
var remaining = timed
? GetRemainingTimeout(deadline)
: TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
if (timed && remaining <= TimeSpan.Zero)
while (state.SignalsPending == 0 && !GuestThreadBlocking.ShutdownRequested)
{
break;
}
var remaining = timed
? GetRemainingTimeout(deadline)
: TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
if (timed && remaining <= TimeSpan.Zero)
{
break;
}
if (remaining > TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds))
{
remaining = TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
}
if (remaining > TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds))
{
remaining = TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
}
_ = Monitor.Wait(state.SyncRoot, remaining);
_ = Monitor.Wait(state.SyncRoot, remaining);
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
if (state.SignalsPending > 0)
@@ -91,10 +91,6 @@ 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();
@@ -105,8 +101,6 @@ public static class KernelPthreadExtendedCompatExports
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)
{
@@ -168,17 +162,6 @@ public static class KernelPthreadExtendedCompatExports
}
}
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
{
public required PthreadRwlockState Rwlock { get; init; }
public required ulong ThreadId { get; init; }
public required bool Write { get; init; }
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
}
private readonly record struct TlsKeyState(ulong Destructor);
private readonly record struct PthreadAttrState(
@@ -1184,7 +1167,6 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1479,35 +1461,29 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// In-place block: Monitor.Wait releases SyncRoot and parks
// atomically, so an unlock's PulseAll cannot be lost. Sliced
// only so teardown can unwind parked threads.
rwlock.WaitingWriters++;
var transferredToScheduler = false;
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_wrlock");
try
{
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_wrlock",
rwlock.WakeKey,
new RwlockWaiter { Rwlock = rwlock, ThreadId = 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);
if (GuestThreadBlocking.ShutdownRequested)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
}
rwlock.WriterThreadId = currentThreadId;
}
finally
{
if (!transferredToScheduler)
{
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
}
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
}
else
@@ -1517,20 +1493,25 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_rdlock",
rwlock.WakeKey,
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_rdlock");
try
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
if (GuestThreadBlocking.ShutdownRequested)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
Monitor.Wait(rwlock.SyncRoot);
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
}
if (rwlock.WriterThreadId != 0 ||
@@ -1547,33 +1528,6 @@ 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)
@@ -1604,8 +1558,6 @@ public static class KernelPthreadExtendedCompatExports
rwlock.GetReaderCount(currentThreadId) == 0;
}
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
{
PthreadRwlockState? rwlock;
@@ -127,6 +127,7 @@ public static class KernelSemaphoreCompatExports
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
}
GuestThreadBlocking.NoteBlocked(GuestThreadExecution.CurrentGuestThreadHandle, "sceKernelWaitSema");
try
{
while (semaphore.Count < needCount)
@@ -159,6 +160,7 @@ public static class KernelSemaphoreCompatExports
finally
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
GuestThreadBlocking.NoteUnblocked(GuestThreadExecution.CurrentGuestThreadHandle);
}
}