diff --git a/src/SharpEmu.HLE/OrbisGen2Result.cs b/src/SharpEmu.HLE/OrbisGen2Result.cs
index 56a22b6..9562c49 100644
--- a/src/SharpEmu.HLE/OrbisGen2Result.cs
+++ b/src/SharpEmu.HLE/OrbisGen2Result.cs
@@ -40,6 +40,13 @@ public enum OrbisGen2Result : int
///
ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B),
+ ///
+ /// Indicates that the waited-on object was deleted while the caller was
+ /// blocked on it. Matches the SCE kernel EACCES code that waiters of a
+ /// deleted semaphore observe.
+ ///
+ ORBIS_GEN2_ERROR_DELETED = unchecked((int)0x8002000D),
+
///
/// Indicates that the target resource is busy.
///
@@ -50,6 +57,13 @@ public enum OrbisGen2Result : int
///
ORBIS_GEN2_ERROR_TRY_AGAIN = unchecked((int)0x80020023),
+ ///
+ /// Indicates that a blocked wait was canceled (e.g. sceKernelCancelSema).
+ /// Matches the SCE kernel ECANCELED code that canceled semaphore waiters
+ /// observe.
+ ///
+ ORBIS_GEN2_ERROR_CANCELED = unchecked((int)0x80020055),
+
///
/// Indicates that behavior is recognized but not implemented yet.
///
diff --git a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs
index 3203199..171362f 100644
--- a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs
+++ b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs
@@ -19,9 +19,22 @@ public static class KernelSemaphoreCompatExports
public required int MaxCount { get; init; }
public int Count { get; set; }
public int WaitingThreads { get; set; }
+ public int CancelEpoch { get; set; }
+ public bool Deleted { get; set; }
public object Gate { get; } = new();
}
+ private sealed class SemaphoreWaiter
+ {
+ public required int NeedCount { get; init; }
+ public required int CancelEpochAtBlock { get; init; }
+
+ // Written and read only under the owning semaphore's Gate.
+ public int? Result { get; set; }
+ }
+
+ private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
+
[SysAbiExport(
Nid = "188x57JYp0g",
ExportName = "sceKernelCreateSema",
@@ -58,17 +71,27 @@ public static class KernelSemaphoreCompatExports
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
}
- _semaphores[handle] = new KernelSemaphoreState
+ var state = new KernelSemaphoreState
{
Name = name,
InitialCount = initialCount,
MaxCount = maxCount,
Count = initialCount,
};
+ _semaphores[handle] = state;
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
+ // Handles are sequential and guest-predictable, so a hostile guest can
+ // race a WaitSema onto the handle between publication above and this
+ // rollback. Strand-proof that waiter exactly like DeleteSema does.
+ lock (state.Gate)
+ {
+ state.Deleted = true;
+ }
+
+ _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -118,7 +141,17 @@ public static class KernelSemaphoreCompatExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
- if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
+ var waiter = new SemaphoreWaiter
+ {
+ NeedCount = needCount,
+ CancelEpochAtBlock = semaphore.CancelEpoch,
+ };
+ if (!GuestThreadExecution.RequestCurrentThreadBlock(
+ ctx,
+ "sceKernelWaitSema",
+ GetSemaphoreWakeKey(handle),
+ resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
+ wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
@@ -193,8 +226,14 @@ public static class KernelSemaphoreCompatExports
semaphore.Count += signalCount;
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
- return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
+
+ // Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
+ // Wake everyone; the wake handler consumes the count per waiter, so a waiter
+ // whose needCount exceeds the remaining count stays parked while a smaller
+ // waiter can proceed.
+ _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
+ return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -226,10 +265,16 @@ public static class KernelSemaphoreCompatExports
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
- semaphore.WaitingThreads = 0;
+ semaphore.CancelEpoch++;
+ // WaitingThreads is NOT zeroed here: each canceled waiter decrements it
+ // exactly once in its wake handler. Zeroing here as well would double-count
+ // and silently absorb the increment of a waiter that parks between this
+ // gate release and the wake-all below.
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
- return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
+
+ _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
+ return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -245,10 +290,84 @@ public static class KernelSemaphoreCompatExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
+ // Delete succeeds even with blocked waiters; they wake with the deleted
+ // result (the SCE kernel wakes waiters with the EACCES-class code).
+ lock (semaphore.Gate)
+ {
+ semaphore.Deleted = true;
+ }
+
+ _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
+ // Wake handler: runs under the scheduler's guest-thread gate (lock order:
+ // scheduler gate -> semaphore gate). Returns true iff the waiter has a final
+ // result and should be re-readied; false leaves it parked.
+ private static bool TryConsumeBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
+ {
+ lock (semaphore.Gate)
+ {
+ return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
+ }
+ }
+
+ private static bool TryConsumeBlockedSemaWaitLocked(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
+ {
+ if (waiter.Result is not null)
+ {
+ return true;
+ }
+
+ if (semaphore.Deleted)
+ {
+ waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
+ semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
+ TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
+ return true;
+ }
+
+ if (semaphore.CancelEpoch != waiter.CancelEpochAtBlock)
+ {
+ waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
+ semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
+ TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
+ return true;
+ }
+
+ if (semaphore.Count >= waiter.NeedCount)
+ {
+ semaphore.Count -= waiter.NeedCount;
+ waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
+ semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
+ TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
+ return true;
+ }
+
+ return false;
+ }
+
+ // Resume handler: runs on the woken guest thread outside the scheduler gate;
+ // its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
+ private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
+ {
+ lock (semaphore.Gate)
+ {
+ if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
+ {
+ // Nothing readies a parked semaphore waiter without the wake handler
+ // resolving it, so reaching here means the scheduler contract changed.
+ Console.Error.WriteLine(
+ $"[LOADER][GAP] sema.resume-no-outcome name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count}");
+ waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
+ semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
+ }
+
+ return waiter.Result!.Value;
+ }
+ }
+
private static void TraceSemaphore(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))