From 816ec4ad27662bb8e505aeeec65ef9c621478d6c Mon Sep 17 00:00:00 2001 From: kuba Date: Fri, 31 Jul 2026 11:13:15 +0200 Subject: [PATCH] Kernel: correct equeue event delivery and waiter lifetime (#723) Ports the event-queue rework from the archived silent-hill-minimal branch (968e9606, 9e3abb12, 2be9cfbf, f157a115) onto current upstream. The equeue implementation had not been touched upstream since that branch forked, so none of it had landed. Behaviour fixed: - Per-waiter event reservation. A blocked waiter used to wake on "the queue has any pending event" (TryWake => HasPendingEvents) and then re-read the queue on resume, so a woken waiter could find the event already drained by another waiter and park again with the wake consumed. Events are now reserved to the waiter they are delivered to. - Level-triggered events are preserved instead of being cleared by an unrelated read; only events that declare clear-on-read reset their trigger state. - Queued interrupts are bound to the registration generation that produced them, so an event registered after a queue was reused cannot consume an interrupt raised for the previous registration. - Deleting an equeue now terminates its waiters instead of leaving them blocked on a handle that no longer resolves. - sceKernelTriggerUserEvent stores its third argument in the event's udata (0x18) rather than its data word (0x10). The guest reads it back with sceKernelGetEventUserData, which loads 0x18, so every triggered user event previously read back as 0. This matches the reference behaviour in shadPS4, where TriggerEvent takes udata and sceKernelGetEventUserData returns ev->udata. The upstream test asserting the data word is updated, since it encoded the inconsistency rather than the ABI. KernelPthreadState gains TryGetCurrentThreadIdentity and a new KernelSyncTraceFormatter carries the shared, opt-in diagnostic formatting the ported code calls; both are gated behind the existing trace flag and do no work when it is off. Tests: 662 pass, 0 fail (SharpEmu.Libs.Tests 588 -> 598). --- .../Kernel/KernelEventQueueCompatExports.cs | 889 +++++++++++++++--- .../Kernel/KernelPthreadState.cs | 20 + .../Kernel/KernelSyncTraceFormatter.cs | 76 ++ .../Agc/AgcEventQueueTests.cs | 388 +++++++- .../KernelEventQueueCompatExportsTests.cs | 8 +- .../KernelEventQueueWaiterLifetimeTests.cs | 151 +++ 6 files changed, 1393 insertions(+), 139 deletions(-) create mode 100644 src/SharpEmu.Libs/Kernel/KernelSyncTraceFormatter.cs create mode 100644 tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueWaiterLifetimeTests.cs diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index b7223d71..5fb6971c 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -4,7 +4,7 @@ using SharpEmu.HLE; using System.Buffers; using System.Buffers.Binary; -using System.Collections.Concurrent; +using System.Runtime.CompilerServices; using System.Threading; namespace SharpEmu.Libs.Kernel; @@ -16,12 +16,30 @@ public static class KernelEventQueueCompatExports public const short KernelEventFilterUser = -11; public const short KernelEventFilterAmpr = -16; public const short KernelEventFilterAmprSystem = -17; + public const ushort KernelEventFlagClear = 0x20; private static readonly object _eventQueueGate = new(); - private static readonly HashSet _eventQueues = new(); + private static readonly Dictionary _eventQueues = new(); + private static readonly ConditionalWeakTable + _eventQueueRuntimeIdentities = new(); private static readonly Dictionary _pendingEvents = new(); private static readonly Dictionary> _registeredEvents = new(); private static long _nextEventQueueHandle = 1; + private static long _nextEventQueueWaiterId; + private static long _nextEventRegistrationGeneration; + private static long _nextEventQueueGeneration; + private static long _nextEventQueueRuntimeId; + + private sealed record EventQueueRuntimeIdentity(ulong Id); + + private sealed class EventQueueState + { + public required ulong Handle { get; init; } + public required ulong RuntimeId { get; init; } + public required ulong Generation { get; init; } + public required string WakeKey { get; init; } + public bool Deleted { get; set; } + } public readonly record struct KernelQueuedEvent( ulong Ident, @@ -34,7 +52,26 @@ public static class KernelEventQueueCompatExports private readonly record struct KernelEventRegistration( ulong Ident, short Filter, - ulong UserData); + ulong UserData, + ushort Flags, + ulong Generation); + + internal readonly record struct KernelEventRegistrationToken( + ulong EqueueHandle, + ulong EqueueGeneration, + ulong Ident, + short Filter, + ulong Generation); + + internal sealed record KernelEventRegistrationSnapshot( + ulong RuntimeId, + ulong Ident, + short Filter, + KernelEventRegistrationToken[] Targets); + + internal readonly record struct CapturedEventDeliveryResult( + int TriggeredCount, + int StaleCount); // Grow-only ring buffer standing in for LinkedList, which // allocated a node per enqueue — steady churn at one enqueue per vblank/flip edge @@ -91,19 +128,122 @@ public static class KernelEventQueueCompatExports return -1; } + + public bool Remove(ulong ident, short filter) + { + var index = FindIndex(ident, filter); + if (index < 0) + { + return false; + } + + for (var i = index; i + 1 < Count; i++) + { + this[i] = this[i + 1]; + } + + Count--; + return true; + } } private sealed class EqueueWaiter : IGuestThreadBlockWaiter { + private enum WaitCompletion + { + Waiting, + Reserved, + TimedOut, + Deleted, + } + + private KernelQueuedEvent[]? _reservedEvents; + private int _reservedCount; + private WaitCompletion _completion; + public required CpuContext Ctx { get; init; } - public required ulong Handle { get; init; } + public required EventQueueState State { get; init; } public required ulong EventsAddress { get; init; } public required int EventCapacity { get; init; } public required ulong OutCountAddress { get; init; } + public required long WaiterId { get; init; } - public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress); + public int Resume() + { + KernelQueuedEvent[]? reservedEvents; + int reservedCount; + WaitCompletion completion; + lock (this) + { + if (_completion == WaitCompletion.Waiting) + { + _completion = WaitCompletion.TimedOut; + } - public bool TryWake() => HasPendingEvents(Handle); + completion = _completion; + reservedEvents = _reservedEvents; + reservedCount = _reservedCount; + _reservedEvents = null; + _reservedCount = 0; + } + + var result = completion switch + { + WaitCompletion.Reserved when reservedEvents is not null => + DeliverReservedEvents( + Ctx, + reservedEvents, + reservedCount, + EventsAddress, + OutCountAddress), + WaitCompletion.Deleted => + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED, + _ => (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT, + }; + + if (_logEqueue) + { + TraceEventQueue( + Ctx, + "wait-resume", + State.Handle, + $"generation={State.Generation} waiter={WaiterId} " + + $"capacity={EventCapacity} result=0x{unchecked((uint)result):X8}"); + } + return result; + } + + public bool TryWake() + { + lock (this) + { + if (_completion != WaitCompletion.Waiting) + { + return true; + } + + if (!TryReserveEvents( + State, + EventCapacity, + out var events, + out var count, + out var deleted)) + { + if (deleted) + { + _completion = WaitCompletion.Deleted; + return true; + } + + return false; + } + + _reservedEvents = events; + _reservedCount = count; + _completion = WaitCompletion.Reserved; + return true; + } + } } [SysAbiExport( @@ -120,15 +260,32 @@ public static class KernelEventQueueCompatExports } var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventQueueHandle)); + var generation = unchecked((ulong)Interlocked.Increment( + ref _nextEventQueueGeneration)); + var state = new EventQueueState + { + Handle = handle, + RuntimeId = GetEventQueueRuntimeId(ctx.Memory), + Generation = generation, + WakeKey = $"sceKernelWaitEqueue:{handle:X16}:{generation:X16}", + }; lock (_eventQueueGate) { - _eventQueues.Add(handle); + _eventQueues.Add(handle, state); _pendingEvents[handle] = new KernelEventDeque(); _registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>(); } if (!ctx.TryWriteUInt64(outAddress, handle)) { + lock (_eventQueueGate) + { + state.Deleted = true; + _eventQueues.Remove(handle); + _pendingEvents.Remove(handle); + _registeredEvents.Remove(handle); + Monitor.PulseAll(_eventQueueGate); + } return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -144,16 +301,28 @@ public static class KernelEventQueueCompatExports public static int KernelDeleteEqueue(CpuContext ctx) { var handle = ctx[CpuRegister.Rdi]; + EventQueueState state; lock (_eventQueueGate) { - _eventQueues.Remove(handle); + if (!_eventQueues.Remove(handle, out state!) || state.Deleted) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + state.Deleted = true; _pendingEvents.Remove(handle); _registeredEvents.Remove(handle); + Monitor.PulseAll(_eventQueueGate); } - _wakeKeys.TryRemove(handle, out _); - - TraceEventQueue(ctx, "delete", handle); + WakeEventQueue( + state, + _logEqueue ? $"source=delete generation={state.Generation}" : null); + TraceEventQueue( + ctx, + "delete", + handle, + $"generation={state.Generation}"); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -169,7 +338,8 @@ public static class KernelEventQueueCompatExports handle, ctx[CpuRegister.Rsi], KernelEventFilterUser, - 0); + 0, + KernelEventFlagClear); TraceEventQueue(ctx, "add_user_edge", handle); return registered ? (int)OrbisGen2Result.ORBIS_GEN2_OK @@ -188,7 +358,8 @@ public static class KernelEventQueueCompatExports handle, ctx[CpuRegister.Rsi], KernelEventFilterUser, - 0); + 0, + flags: 0); TraceEventQueue(ctx, "add_user", handle); return registered ? (int)OrbisGen2Result.ORBIS_GEN2_OK @@ -225,9 +396,7 @@ public static class KernelEventQueueCompatExports handle, ctx[CpuRegister.Rsi], KernelEventFilterUser, - flags: 0x21, - fflags: 0, - data: ctx[CpuRegister.Rdx]); + userData: ctx[CpuRegister.Rdx]); TraceEventQueue(ctx, "trigger_user", handle); return triggered ? (int)OrbisGen2Result.ORBIS_GEN2_OK @@ -384,7 +553,7 @@ public static class KernelEventQueueCompatExports var outCountAddress = ctx[CpuRegister.Rcx]; var timeoutAddress = ctx[CpuRegister.R8]; - if (!IsValidEqueue(handle)) + if (!TryGetLiveEventQueue(handle, out var state)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } @@ -400,7 +569,11 @@ public static class KernelEventQueueCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity); + var deliveredCount = DequeueEvents( + ctx, + state, + eventsAddress, + eventCapacity); if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; @@ -408,36 +581,99 @@ public static class KernelEventQueueCompatExports if (deliveredCount > 0) { - TraceEventQueue(ctx, "wait-deliver", handle); + if (_logEqueue) + { + TraceEventQueue( + ctx, + "wait-deliver", + handle, + $"delivered={deliveredCount} capacity={eventCapacity}"); + } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - if (timeoutAddress == 0 && - GuestThreadExecution.RequestCurrentThreadBlock( + lock (_eventQueueGate) + { + if (!IsLiveEventQueueLocked(state)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED; + } + } + + if (IsSynchronousPoll(timeoutAddress, timeoutUsec)) + { + if (_logEqueue) + { + TraceEventQueue( + ctx, + "wait-poll-timeout", + handle, + $"capacity={eventCapacity}"); + } + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; + } + + var waiterId = Interlocked.Increment(ref _nextEventQueueWaiterId); + if (timeoutAddress == 0) + { + var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock( ctx, "sceKernelWaitEqueue", - GetEventQueueWakeKey(handle), + state.WakeKey, new EqueueWaiter { Ctx = ctx, - Handle = handle, + State = state, EventsAddress = eventsAddress, EventCapacity = eventCapacity, OutCountAddress = outCountAddress, - })) - { - TraceEventQueue(ctx, "wait-block", handle); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + WaiterId = waiterId, + }); + if (requestedBlock) + { + var wakeAfterRegistration = false; + lock (_eventQueueGate) + { + wakeAfterRegistration = + !IsLiveEventQueueLocked(state) || + HasPendingEventsLocked(state.Handle); + } + + if (wakeAfterRegistration) + { + WakeEventQueue( + state, + _logEqueue + ? "source=post-registration-state-check" + : null); + } + + if (_logEqueue) + { + TraceEventQueue( + ctx, + "wait-block", + handle, + $"generation={state.Generation} waiter={waiterId} " + + $"capacity={eventCapacity} timeout=infinite " + + $"events=0x{eventsAddress:X16} out_count=0x{outCountAddress:X16}"); + } + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } } - if (timeoutAddress != 0 && ctx.TryReadUInt64(timeoutAddress, out var timeoutRaw)) + if (timeoutAddress != 0) { - var timeoutMicros = timeoutRaw & 0xFFFF_FFFFUL; var deadline = Environment.TickCount64 + - Math.Max(1L, (long)Math.Min(timeoutMicros / 1000, int.MaxValue)); + Math.Max( + 1L, + (long)Math.Min( + ((ulong)timeoutUsec + 999UL) / 1000UL, + int.MaxValue)); lock (_eventQueueGate) { - while (!HasPendingEvents(handle)) + while (IsLiveEventQueueLocked(state) && + !HasPendingEventsLocked(handle)) { var remaining = deadline - Environment.TickCount64; if (remaining <= 0) @@ -447,9 +683,18 @@ public static class KernelEventQueueCompatExports Monitor.Wait(_eventQueueGate, (int)Math.Min(remaining, 100)); } + + if (!IsLiveEventQueueLocked(state)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED; + } } - deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity); + deliveredCount = DequeueEvents( + ctx, + state, + eventsAddress, + eventCapacity); if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; @@ -457,32 +702,93 @@ public static class KernelEventQueueCompatExports if (deliveredCount > 0) { - TraceEventQueue(ctx, "wait-timed-deliver", handle); + if (_logEqueue) + { + TraceEventQueue( + ctx, + "wait-timed-deliver", + handle, + $"waiter={waiterId} delivered={deliveredCount} capacity={eventCapacity} " + + $"timeout_usec={timeoutUsec}"); + } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - TraceEventQueue(ctx, "wait-timeout", handle); + if (_logEqueue) + { + TraceEventQueue( + ctx, + "wait-timeout", + handle, + $"waiter={waiterId} capacity={eventCapacity} timeout_usec={timeoutUsec}"); + } return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; } - TraceEventQueue(ctx, "wait", handle); + if (_logEqueue) + { + TraceEventQueue( + ctx, + "wait", + handle, + $"waiter={waiterId} capacity={eventCapacity} timeout_usec={timeoutUsec}"); + } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } public static bool IsValidEqueue(ulong handle) + { + return TryGetLiveEventQueue(handle, out _); + } + + private static bool TryGetLiveEventQueue( + ulong handle, + out EventQueueState state) { lock (_eventQueueGate) { - return _eventQueues.Contains(handle); + return _eventQueues.TryGetValue(handle, out state!) && + !state.Deleted; } } + private static bool IsLiveEventQueueLocked(EventQueueState state) => + !state.Deleted && + _eventQueues.TryGetValue(state.Handle, out var current) && + ReferenceEquals(current, state); + + private static ulong GetEventQueueRuntimeId(ICpuMemory memory) + { + object key = memory; + while (key is ICpuMemoryWrapper wrapper && + !ReferenceEquals(wrapper.Inner, key)) + { + key = wrapper.Inner; + } + + return _eventQueueRuntimeIdentities.GetValue( + key, + static _ => new EventQueueRuntimeIdentity( + unchecked((ulong)Interlocked.Increment( + ref _nextEventQueueRuntimeId)))).Id; + } + + internal static bool IsSynchronousPoll( + ulong timeoutAddress, + uint timeoutUsec) => + timeoutAddress != 0 && timeoutUsec == 0; + + private static bool HasPendingEventsLocked(ulong handle) => + _pendingEvents.TryGetValue(handle, out var events) && + events.Count != 0; + public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent) { - var queued = false; + EventQueueState state; lock (_eventQueueGate) { - if (!_eventQueues.Contains(handle)) + if (!_eventQueues.TryGetValue(handle, out state!) || + state.Deleted) { return false; } @@ -494,27 +800,30 @@ public static class KernelEventQueueCompatExports } queue.AddLast(queuedEvent); - queued = true; Monitor.PulseAll(_eventQueueGate); } - if (queued) - { - WakeEventQueue(handle); - } + WakeEventQueue( + state, + _logEqueue + ? $"source=enqueue ident=0x{queuedEvent.Ident:X16} " + + $"filter={queuedEvent.Filter} data=0x{queuedEvent.Data:X16}" + : null); - return queued; + return true; } public static bool RegisterEvent( ulong handle, ulong ident, short filter, - ulong userData) + ulong userData, + ushort flags = KernelEventFlagClear) { lock (_eventQueueGate) { - if (!_eventQueues.Contains(handle)) + if (!_eventQueues.TryGetValue(handle, out var state) || + state.Deleted) { return false; } @@ -525,11 +834,131 @@ public static class KernelEventQueueCompatExports _registeredEvents[handle] = events; } - events[(ident, filter)] = new KernelEventRegistration(ident, filter, userData); + events[(ident, filter)] = new KernelEventRegistration( + ident, + filter, + userData, + flags, + unchecked((ulong)Interlocked.Increment( + ref _nextEventRegistrationGeneration))); return true; } } + /// + /// Captures the exact lifetime of every matching registration owned by one + /// guest runtime. A later delete/re-add of the same tuple receives a new + /// generation and therefore cannot consume an interrupt that was already + /// bound to this snapshot. + /// + internal static KernelEventRegistrationSnapshot CaptureRegisteredEvents( + ICpuMemory memory, + ulong ident, + short filter) + { + ArgumentNullException.ThrowIfNull(memory); + var runtimeId = GetEventQueueRuntimeId(memory); + List? targets = null; + lock (_eventQueueGate) + { + foreach (var (handle, registrations) in _registeredEvents) + { + if (!_eventQueues.TryGetValue(handle, out var state) || + state.Deleted || + state.RuntimeId != runtimeId || + !registrations.TryGetValue((ident, filter), out var registration)) + { + continue; + } + + (targets ??= []).Add(new KernelEventRegistrationToken( + handle, + state.Generation, + registration.Ident, + registration.Filter, + registration.Generation)); + } + } + + return new KernelEventRegistrationSnapshot( + runtimeId, + ident, + filter, + targets?.ToArray() ?? []); + } + + /// + /// Delivers a previously captured interrupt only to registrations whose + /// runtime, queue handle, and generation are still live. + /// + internal static CapturedEventDeliveryResult TriggerCapturedEvents( + KernelEventRegistrationSnapshot snapshot, + ulong data) + { + ArgumentNullException.ThrowIfNull(snapshot); + HashSet? wakeQueues = null; + var triggeredCount = 0; + var staleCount = 0; + lock (_eventQueueGate) + { + foreach (var target in snapshot.Targets) + { + if (!_eventQueues.TryGetValue( + target.EqueueHandle, + out var state) || + state.Deleted || + state.Generation != target.EqueueGeneration || + state.RuntimeId != snapshot.RuntimeId || + !_registeredEvents.TryGetValue( + target.EqueueHandle, + out var registrations) || + !registrations.TryGetValue( + (target.Ident, target.Filter), + out var registration) || + registration.Generation != target.Generation) + { + staleCount++; + continue; + } + + if (!_pendingEvents.TryGetValue( + target.EqueueHandle, + out var queue)) + { + queue = new KernelEventDeque(); + _pendingEvents[target.EqueueHandle] = queue; + } + + QueueOrUpdateEvent( + queue, + new KernelQueuedEvent( + registration.Ident, + registration.Filter, + registration.Flags, + 1, + data, + registration.UserData)); + (wakeQueues ??= []).Add(state); + triggeredCount++; + } + } + + if (wakeQueues is not null) + { + foreach (var state in wakeQueues) + { + WakeEventQueue( + state, + _logEqueue + ? $"source=trigger-captured ident=0x{snapshot.Ident:X16} " + + $"filter={snapshot.Filter} data=0x{data:X16}" + : null); + } + } + + return new CapturedEventDeliveryResult(triggeredCount, staleCount); + } + public static bool DeleteRegisteredEvent( ulong handle, ulong ident, @@ -537,8 +966,18 @@ public static class KernelEventQueueCompatExports { lock (_eventQueueGate) { - return _registeredEvents.TryGetValue(handle, out var events) && - events.Remove((ident, filter)); + if (!_registeredEvents.TryGetValue(handle, out var events) || + !events.Remove((ident, filter))) + { + return false; + } + + if (_pendingEvents.TryGetValue(handle, out var pending)) + { + _ = pending.Remove(ident, filter); + } + + return true; } } @@ -547,13 +986,15 @@ public static class KernelEventQueueCompatExports short filter, ulong data) { - List? wakeHandles = null; + List? wakeQueues = null; var triggeredCount = 0; lock (_eventQueueGate) { foreach (var (handle, registrations) in _registeredEvents) { - if (!registrations.TryGetValue((ident, filter), out var registration)) + if (!_eventQueues.TryGetValue(handle, out var state) || + state.Deleted || + !registrations.TryGetValue((ident, filter), out var registration)) { continue; } @@ -569,20 +1010,24 @@ public static class KernelEventQueueCompatExports new KernelQueuedEvent( registration.Ident, registration.Filter, - 0, + registration.Flags, 1, data, registration.UserData)); - (wakeHandles ??= new List()).Add(handle); + (wakeQueues ??= []).Add(state); triggeredCount++; } } - if (wakeHandles is not null) + if (wakeQueues is not null) { - foreach (var handle in wakeHandles) + foreach (var state in wakeQueues) { - WakeEventQueue(handle); + WakeEventQueue( + state, + _logEqueue + ? $"source=trigger ident=0x{ident:X16} filter={filter} data=0x{data:X16}" + : null); } } @@ -600,12 +1045,18 @@ public static class KernelEventQueueCompatExports short filter, ulong data) { - List? wakeHandles = null; + List? wakeQueues = null; var triggeredCount = 0; lock (_eventQueueGate) { foreach (var (handle, registrations) in _registeredEvents) { + if (!_eventQueues.TryGetValue(handle, out var state) || + state.Deleted) + { + continue; + } + foreach (var registration in registrations.Values) { if (registration.Filter != filter) @@ -624,11 +1075,11 @@ public static class KernelEventQueueCompatExports new KernelQueuedEvent( registration.Ident, registration.Filter, - 0, + registration.Flags, 1, data, registration.UserData)); - (wakeHandles ??= new List()).Add(handle); + (wakeQueues ??= []).Add(state); triggeredCount++; // A single queue only needs to be woken once, even if multiple @@ -638,11 +1089,15 @@ public static class KernelEventQueueCompatExports } } - if (wakeHandles is not null) + if (wakeQueues is not null) { - foreach (var handle in wakeHandles) + foreach (var state in wakeQueues) { - WakeEventQueue(handle); + WakeEventQueue( + state, + _logEqueue + ? $"source=trigger-filter filter={filter} data=0x{data:X16}" + : null); } } @@ -658,12 +1113,18 @@ public static class KernelEventQueueCompatExports /// public static int TriggerRegisteredEventsDistinct(short filter) { - HashSet? wakeHandles = null; + HashSet? wakeQueues = null; var triggeredCount = 0; lock (_eventQueueGate) { foreach (var (handle, registrations) in _registeredEvents) { + if (!_eventQueues.TryGetValue(handle, out var state) || + state.Deleted) + { + continue; + } + foreach (var registration in registrations.Values) { if (registration.Filter != filter) @@ -682,21 +1143,23 @@ public static class KernelEventQueueCompatExports new KernelQueuedEvent( registration.Ident, registration.Filter, - 0, + registration.Flags, 1, registration.Ident, registration.UserData)); - (wakeHandles ??= []).Add(handle); + (wakeQueues ??= []).Add(state); triggeredCount++; } } } - if (wakeHandles is not null) + if (wakeQueues is not null) { - foreach (var handle in wakeHandles) + foreach (var state in wakeQueues) { - WakeEventQueue(handle); + WakeEventQueue( + state, + _logEqueue ? $"source=trigger-distinct filter={filter}" : null); } } @@ -707,13 +1170,14 @@ public static class KernelEventQueueCompatExports ulong handle, ulong ident, short filter, - ushort flags, - uint fflags, - ulong data) + ulong userData) { + EventQueueState state; lock (_eventQueueGate) { - if (!_registeredEvents.TryGetValue(handle, out var registrations) || + if (!_eventQueues.TryGetValue(handle, out state!) || + state.Deleted || + !_registeredEvents.TryGetValue(handle, out var registrations) || !registrations.TryGetValue((ident, filter), out var registration)) { return false; @@ -730,13 +1194,18 @@ public static class KernelEventQueueCompatExports new KernelQueuedEvent( registration.Ident, registration.Filter, - flags, - fflags, - data, - registration.UserData)); + registration.Flags, + 0, + 0, + userData)); } - WakeEventQueue(handle); + WakeEventQueue( + state, + _logEqueue + ? $"source=trigger-one ident=0x{ident:X16} filter={filter} " + + $"user_data=0x{userData:X16}" + : null); return true; } @@ -747,10 +1216,11 @@ public static class KernelEventQueueCompatExports ulong eventHint, ulong userData) { - var triggered = false; + EventQueueState state; lock (_eventQueueGate) { - if (!_eventQueues.Contains(handle)) + if (!_eventQueues.TryGetValue(handle, out state!) || + state.Deleted) { return false; } @@ -786,41 +1256,51 @@ public static class KernelEventQueueCompatExports { events.AddLast(triggeredEvent); } - - triggered = true; } - if (triggered) - { - WakeEventQueue(handle); - } + WakeEventQueue( + state, + _logEqueue + ? $"source=display ident=0x{ident:X16} filter={filter} hint=0x{eventHint:X16}" + : null); - return triggered; + return true; } - private static int ResumeWaitEqueue( + private static int DeliverReservedEvents( CpuContext ctx, - ulong handle, + KernelQueuedEvent[] events, + int count, ulong eventsAddress, - int eventCapacity, ulong outCountAddress) { - var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity); - if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount)) + var deliveredCount = 0; + try { - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + for (; deliveredCount < count; deliveredCount++) + { + if (!WriteKernelEvent( + ctx, + eventsAddress + ((ulong)deliveredCount * KernelEventSize), + events[deliveredCount])) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + } + + if (outCountAddress != 0 && + !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + return deliveredCount > 0 + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; } - - return deliveredCount > 0 - ? (int)OrbisGen2Result.ORBIS_GEN2_OK - : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; - } - - private static bool HasPendingEvents(ulong handle) - { - lock (_eventQueueGate) + finally { - return _pendingEvents.TryGetValue(handle, out var events) && events.Count != 0; + ArrayPool.Shared.Return(events); } } @@ -835,57 +1315,61 @@ public static class KernelEventQueueCompatExports return; } - queue[pendingIndex] = queuedEvent with + queue[pendingIndex] = queuedEvent.Filter == KernelEventFilterUser + ? queuedEvent + : queuedEvent with { Fflags = Math.Max(queue[pendingIndex].Fflags + 1, queuedEvent.Fflags), }; } - // Wake keys are formatted once per handle: WakeEventQueue runs on every event - // enqueue (vblank/flip edges included), so formatting there is steady string churn. - private static readonly ConcurrentDictionary _wakeKeys = new(); - - private static string GetEventQueueWakeKey(ulong handle) => - _wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}"); - - private static void WakeEventQueue(ulong handle) + private static void WakeEventQueue( + EventQueueState state, + string? detail = null) { - _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventQueueWakeKey(handle)); + if (_logEqueue) + { + TraceEventQueueHost( + "wake", + state.Handle, + $"generation={state.Generation}" + + (detail is null ? string.Empty : $" {detail}")); + } + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey); } - private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity) + private static int DequeueEvents( + CpuContext ctx, + EventQueueState state, + ulong eventsAddress, + int eventCapacity) { if (eventsAddress == 0 || eventCapacity <= 0) { return 0; } - // Engines wait on the vblank/flip equeue every frame, so the delivery buffer - // (usually a single event) comes from the pool instead of a per-call array. - KernelQueuedEvent[] events; - int count; - lock (_eventQueueGate) + if (!TryReserveEvents( + state, + eventCapacity, + out var events, + out var count, + out _)) { - if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0) - { - return 0; - } - - count = Math.Min(eventCapacity, queue.Count); - events = ArrayPool.Shared.Rent(count); - for (var i = 0; i < count; i++) - { - events[i] = queue.RemoveFirst(); - } + return 0; } + var deliveredCount = 0; try { - for (var i = 0; i < count; i++) + for (; deliveredCount < count; deliveredCount++) { - if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i])) + if (!WriteKernelEvent( + ctx, + eventsAddress + ((ulong)deliveredCount * KernelEventSize), + events[deliveredCount])) { - return i; + break; } } } @@ -894,7 +1378,105 @@ public static class KernelEventQueueCompatExports ArrayPool.Shared.Return(events); } - return count; + return deliveredCount; + } + + private static bool TryReserveEvents( + EventQueueState state, + int eventCapacity, + out KernelQueuedEvent[] events, + out int count, + out bool deleted) + { + events = null!; + count = 0; + deleted = false; + lock (_eventQueueGate) + { + if (!IsLiveEventQueueLocked(state)) + { + deleted = true; + return false; + } + + if (!_pendingEvents.TryGetValue(state.Handle, out var queue) || + queue.Count == 0) + { + return false; + } + + count = Math.Min(eventCapacity, queue.Count); + events = ArrayPool.Shared.Rent(count); + for (var i = 0; i < count; i++) + { + events[i] = queue.RemoveFirst(); + } + + // Level-triggered events remain ready until their registration is + // deleted or their source clears. EV_CLEAR events model edges and + // are consumed by this delivery. + for (var i = 0; i < count; i++) + { + if ((events[i].Flags & KernelEventFlagClear) == 0) + { + queue.AddLast(events[i]); + } + } + } + + return true; + } + + internal static int ReservePendingEventCountForTest( + ulong handle, + int eventCapacity) + { + if (!TryGetLiveEventQueue(handle, out var state) || + !TryReserveEvents( + state, + eventCapacity, + out var events, + out var count, + out _)) + { + return 0; + } + + try + { + return count; + } + finally + { + ArrayPool.Shared.Return(events); + } + } + + internal static bool TryReservePendingEventForTest( + ulong handle, + out KernelQueuedEvent queuedEvent) + { + queuedEvent = default; + if (!TryGetLiveEventQueue(handle, out var state) || + !TryReserveEvents( + state, + 1, + out var events, + out var count, + out _)) + { + return false; + } + + try + { + queuedEvent = events[0]; + return count == 1; + } + finally + { + ArrayPool.Shared.Return(events); + } } private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent) @@ -912,17 +1494,50 @@ public static class KernelEventQueueCompatExports private static readonly bool _logEqueue = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal); - private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle) + private static void TraceEventQueue( + CpuContext ctx, + string operation, + ulong handle, + string? detail = null) { if (!_logEqueue) { return; } - var returnRip = 0UL; - _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip); + var suffix = string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}"; Console.Error.WriteLine( - $"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}"); + $"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} " + + $"depth={GetPendingEventCount(handle)} registrations={GetRegistrationCount(handle)}" + + $"{suffix} {KernelSyncTraceFormatter.FormatContext(ctx)}"); + } + + private static void TraceEventQueueHost( + string operation, + ulong handle, + string? detail = null) + { + var suffix = string.IsNullOrWhiteSpace(detail) ? string.Empty : $" {detail}"; + Console.Error.WriteLine( + $"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} " + + $"depth={GetPendingEventCount(handle)} registrations={GetRegistrationCount(handle)} " + + $"{KernelSyncTraceFormatter.FormatCurrentThread()}{suffix}"); + } + + private static int GetPendingEventCount(ulong handle) + { + lock (_eventQueueGate) + { + return _pendingEvents.TryGetValue(handle, out var events) ? events.Count : 0; + } + } + + private static int GetRegistrationCount(ulong handle) + { + lock (_eventQueueGate) + { + return _registeredEvents.TryGetValue(handle, out var events) ? events.Count : 0; + } } private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value) diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs index 41651e83..10e1ff9f 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadState.cs @@ -75,6 +75,26 @@ internal static class KernelPthreadState return Threads.TryGetValue(threadHandle, out identity); } + internal static bool TryGetCurrentThreadIdentity( + out ulong threadHandle, + out ThreadIdentity identity) + { + threadHandle = GuestThreadExecution.CurrentGuestThreadHandle; + if (threadHandle != 0 && TryGetThreadIdentity(threadHandle, out identity)) + { + return true; + } + + threadHandle = _currentThreadHandle; + if (threadHandle != 0 && TryGetThreadIdentity(threadHandle, out identity)) + { + return true; + } + + identity = default; + return false; + } + private static ThreadIdentity EnsureGuestThreadIdentity(ulong guestThreadHandle) { if (Threads.TryGetValue(guestThreadHandle, out var existing)) diff --git a/src/SharpEmu.Libs/Kernel/KernelSyncTraceFormatter.cs b/src/SharpEmu.Libs/Kernel/KernelSyncTraceFormatter.cs new file mode 100644 index 00000000..2fb1941f --- /dev/null +++ b/src/SharpEmu.Libs/Kernel/KernelSyncTraceFormatter.cs @@ -0,0 +1,76 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Kernel; + +/// +/// Shared formatting for opt-in kernel synchronization diagnostics. +/// Callers must gate this behind their trace flag so normal synchronization +/// paths do not allocate strings or walk guest frame chains. +/// +internal static class KernelSyncTraceFormatter +{ + internal static string FormatContext(CpuContext ctx) + { + _ = KernelPthreadState.TryGetCurrentThreadIdentity(out var pthread, out var identity); + var threadName = identity.Name ?? ""; + var returnRip = GuestThreadExecution.TryGetCurrentImportCallFrame(out var importFrame) + ? importFrame.ReturnRip + : TryReadReturnRip(ctx); + + return $"thread='{threadName}' pthread=0x{pthread:X16} " + + $"gth=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} " + + $"managed={Environment.CurrentManagedThreadId} ret=0x{returnRip:X16} " + + $"frames={FormatFrameChain(ctx)}"; + } + + internal static string FormatCurrentThread() + { + _ = KernelPthreadState.TryGetCurrentThreadIdentity(out var pthread, out var identity); + var threadName = identity.Name ?? Thread.CurrentThread.Name ?? ""; + return $"thread='{threadName}' pthread=0x{pthread:X16} " + + $"gth=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} " + + $"managed={Environment.CurrentManagedThreadId}"; + } + + internal static string FormatFrameChain(CpuContext ctx) + { + Span returns = stackalloc ulong[4]; + var count = 0; + var frame = ctx[CpuRegister.Rbp]; + while (count < returns.Length && frame != 0) + { + if (!ctx.TryReadUInt64(frame, out var nextFrame) || + !ctx.TryReadUInt64(frame + sizeof(ulong), out var returnAddress)) + { + break; + } + + returns[count++] = returnAddress; + if (nextFrame <= frame || nextFrame - frame > 0x100000) + { + break; + } + + frame = nextFrame; + } + + return count switch + { + 0 => "none", + 1 => $"0x{returns[0]:X16}", + 2 => $"0x{returns[0]:X16},0x{returns[1]:X16}", + 3 => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16}", + _ => $"0x{returns[0]:X16},0x{returns[1]:X16}," + + $"0x{returns[2]:X16},0x{returns[3]:X16}", + }; + } + + private static ulong TryReadReturnRip(CpuContext ctx) + { + _ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip); + return returnRip; + } +} diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcEventQueueTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcEventQueueTests.cs index 9a13da48..7f106870 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/AgcEventQueueTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcEventQueueTests.cs @@ -76,7 +76,9 @@ public sealed class AgcEventQueueTests // Verify the queued event carries the registered ident and the event type as data. Assert.Equal(registeredEventId, ReadUInt64(memory, eventsAddress + 0x00)); Assert.Equal(KernelEventQueueCompatExports.KernelEventFilterGraphics, ReadInt16(memory, eventsAddress + 0x08)); - Assert.Equal(0u, ReadUInt16(memory, eventsAddress + 0x0A)); + Assert.Equal( + KernelEventQueueCompatExports.KernelEventFlagClear, + ReadUInt16(memory, eventsAddress + 0x0A)); Assert.Equal(1u, ReadUInt32(memory, eventsAddress + 0x0C)); Assert.Equal(eventType, ReadUInt64(memory, eventsAddress + 0x10)); Assert.Equal(userData, ReadUInt64(memory, eventsAddress + 0x18)); @@ -117,6 +119,370 @@ public sealed class AgcEventQueueTests Assert.Equal(0, triggered); } + [Fact] + public void CapturedCompletion_DoesNotWakeDeleteAndReAddGeneration() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + var handle = CreateEqueue(ctx, memory, BaseAddress + 0x100); + const ulong eventId = 0x20; + + Assert.True(KernelEventQueueCompatExports.RegisterEvent( + handle, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + userData: 0x1111)); + var staleSnapshot = + KernelEventQueueCompatExports.CaptureRegisteredEvents( + memory, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics); + Assert.Single(staleSnapshot.Targets); + + Assert.True(KernelEventQueueCompatExports.DeleteRegisteredEvent( + handle, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics)); + Assert.True(KernelEventQueueCompatExports.RegisterEvent( + handle, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + userData: 0x2222)); + + var staleDelivery = + KernelEventQueueCompatExports.TriggerCapturedEvents( + staleSnapshot, + eventId); + Assert.Equal(0, staleDelivery.TriggeredCount); + Assert.Equal(1, staleDelivery.StaleCount); + Assert.False( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + handle, + out _)); + + var liveSnapshot = + KernelEventQueueCompatExports.CaptureRegisteredEvents( + memory, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics); + var liveDelivery = + KernelEventQueueCompatExports.TriggerCapturedEvents( + liveSnapshot, + eventId); + Assert.Equal(1, liveDelivery.TriggeredCount); + Assert.Equal(0, liveDelivery.StaleCount); + Assert.True( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + handle, + out var delivered)); + Assert.Equal(0x2222UL, delivered.UserData); + + DeleteEqueue(ctx, handle); + } + + [Fact] + public void CapturedCompletion_PreservesEachQueuesRegistrationGeneration() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + var firstHandle = CreateEqueue(ctx, memory, BaseAddress + 0x100); + var secondHandle = CreateEqueue(ctx, memory, BaseAddress + 0x108); + const ulong eventId = 0x20; + + Assert.True(KernelEventQueueCompatExports.RegisterEvent( + firstHandle, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + userData: 0xAAAA)); + Assert.True(KernelEventQueueCompatExports.RegisterEvent( + secondHandle, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + userData: 0xBBBB)); + var snapshot = KernelEventQueueCompatExports.CaptureRegisteredEvents( + memory, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics); + Assert.Equal(2, snapshot.Targets.Length); + + Assert.True(KernelEventQueueCompatExports.DeleteRegisteredEvent( + secondHandle, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics)); + var delivery = KernelEventQueueCompatExports.TriggerCapturedEvents( + snapshot, + eventId); + + Assert.Equal(1, delivery.TriggeredCount); + Assert.Equal(1, delivery.StaleCount); + Assert.True( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + firstHandle, + out var delivered)); + Assert.Equal(0xAAAAUL, delivered.UserData); + Assert.False( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + secondHandle, + out _)); + + DeleteEqueue(ctx, firstHandle); + DeleteEqueue(ctx, secondHandle); + } + + [Fact] + public void CapturedCompletion_DoesNotWakeDeletedEqueue() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + var handle = CreateEqueue(ctx, memory, BaseAddress + 0x100); + + Assert.True(KernelEventQueueCompatExports.RegisterEvent( + handle, + ident: 0, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + userData: 0)); + var snapshot = KernelEventQueueCompatExports.CaptureRegisteredEvents( + memory, + ident: 0, + KernelEventQueueCompatExports.KernelEventFilterGraphics); + DeleteEqueue(ctx, handle); + + var delivery = KernelEventQueueCompatExports.TriggerCapturedEvents( + snapshot, + data: 0); + + Assert.Equal(0, delivery.TriggeredCount); + Assert.Equal(1, delivery.StaleCount); + } + + [Fact] + public void CapturedCompletion_IsScopedToCreatingRuntime() + { + var firstMemory = new FakeCpuMemory(BaseAddress, MemorySize); + var secondMemory = new FakeCpuMemory(BaseAddress, MemorySize); + var firstContext = new CpuContext(firstMemory, Generation.Gen5); + var secondContext = new CpuContext(secondMemory, Generation.Gen5); + var firstHandle = CreateEqueue( + firstContext, + firstMemory, + BaseAddress + 0x100); + var secondHandle = CreateEqueue( + secondContext, + secondMemory, + BaseAddress + 0x100); + const ulong eventId = 0x20; + + Assert.True(KernelEventQueueCompatExports.RegisterEvent( + firstHandle, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + userData: 0x1111)); + Assert.True(KernelEventQueueCompatExports.RegisterEvent( + secondHandle, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + userData: 0x2222)); + + var snapshot = KernelEventQueueCompatExports.CaptureRegisteredEvents( + firstMemory, + eventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics); + Assert.Single(snapshot.Targets); + var delivery = KernelEventQueueCompatExports.TriggerCapturedEvents( + snapshot, + eventId); + + Assert.Equal(1, delivery.TriggeredCount); + Assert.Equal(0, delivery.StaleCount); + Assert.True( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + firstHandle, + out var delivered)); + Assert.Equal(0x1111UL, delivered.UserData); + Assert.False( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + secondHandle, + out _)); + + DeleteEqueue(firstContext, firstHandle); + DeleteEqueue(secondContext, secondHandle); + } + + [Fact] + public void OnePendingEventCanBeReservedByOnlyOneWaiter() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + + const ulong handleOutAddress = BaseAddress + 0x100; + ctx[CpuRegister.Rdi] = handleOutAddress; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelCreateEqueue(ctx)); + var handle = ReadUInt64(memory, handleOutAddress); + + Assert.True(KernelEventQueueCompatExports.EnqueueEvent( + handle, + new KernelEventQueueCompatExports.KernelQueuedEvent( + 7, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + KernelEventQueueCompatExports.KernelEventFlagClear, + 1, + 0, + 0))); + + Assert.Equal( + 1, + KernelEventQueueCompatExports.ReservePendingEventCountForTest( + handle, + eventCapacity: 1)); + Assert.Equal( + 0, + KernelEventQueueCompatExports.ReservePendingEventCountForTest( + handle, + eventCapacity: 1)); + } + + [Fact] + public void ZeroTimeoutWithNoEventReturnsWithoutHostWait() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + + const ulong handleOutAddress = BaseAddress + 0x100; + const ulong eventsAddress = BaseAddress + 0x200; + const ulong outCountAddress = BaseAddress + 0x300; + const ulong timeoutAddress = BaseAddress + 0x400; + ctx[CpuRegister.Rdi] = handleOutAddress; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelCreateEqueue(ctx)); + var handle = ReadUInt64(memory, handleOutAddress); + WriteUInt64(memory, timeoutAddress, 0); + + ctx[CpuRegister.Rdi] = handle; + ctx[CpuRegister.Rsi] = eventsAddress; + ctx[CpuRegister.Rdx] = 1; + ctx[CpuRegister.Rcx] = outCountAddress; + ctx[CpuRegister.R8] = timeoutAddress; + + var result = KernelEventQueueCompatExports.KernelWaitEqueue(ctx); + + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT, + result); + Assert.Equal(0u, ReadUInt32(memory, outCountAddress)); + Assert.True( + KernelEventQueueCompatExports.IsSynchronousPoll( + timeoutAddress, + timeoutUsec: 0)); + } + + [Fact] + public void LevelUserEventPersistsButEdgeEventClears() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + const ulong handleOutAddress = BaseAddress + 0x100; + ctx[CpuRegister.Rdi] = handleOutAddress; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelCreateEqueue(ctx)); + var handle = ReadUInt64(memory, handleOutAddress); + + const ulong levelIdent = 0xA1; + ctx[CpuRegister.Rdi] = handle; + ctx[CpuRegister.Rsi] = levelIdent; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelAddUserEvent(ctx)); + ctx[CpuRegister.Rdx] = 0x1111; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelTriggerUserEvent(ctx)); + + Assert.True( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + handle, + out var firstLevel)); + Assert.True( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + handle, + out var secondLevel)); + Assert.Equal((ushort)0, firstLevel.Flags); + Assert.Equal(0x1111UL, firstLevel.UserData); + Assert.Equal(firstLevel, secondLevel); + + ctx[CpuRegister.Rsi] = levelIdent; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelDeleteUserEvent(ctx)); + + const ulong edgeIdent = 0xA2; + ctx[CpuRegister.Rsi] = edgeIdent; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelAddUserEventEdge(ctx)); + ctx[CpuRegister.Rdx] = 0x2222; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelTriggerUserEvent(ctx)); + + Assert.True( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + handle, + out var edge)); + Assert.Equal( + KernelEventQueueCompatExports.KernelEventFlagClear, + edge.Flags); + Assert.Equal(0x2222UL, edge.UserData); + Assert.False( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + handle, + out _)); + + ctx[CpuRegister.Rdi] = handle; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelDeleteEqueue(ctx)); + } + + [Fact] + public void DeletingLevelRegistrationClearsItsReadyState() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + const ulong handleOutAddress = BaseAddress + 0x100; + ctx[CpuRegister.Rdi] = handleOutAddress; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelCreateEqueue(ctx)); + var handle = ReadUInt64(memory, handleOutAddress); + + ctx[CpuRegister.Rdi] = handle; + ctx[CpuRegister.Rsi] = 0xB1; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelAddUserEvent(ctx)); + ctx[CpuRegister.Rdx] = 0x3333; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelTriggerUserEvent(ctx)); + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelDeleteUserEvent(ctx)); + + Assert.False( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + handle, + out _)); + + ctx[CpuRegister.Rdi] = handle; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelDeleteEqueue(ctx)); + } + private static ulong ReadUInt64(FakeCpuMemory memory, ulong address) { Span buffer = stackalloc byte[8]; @@ -151,4 +517,24 @@ public sealed class AgcEventQueueTests BinaryPrimitives.WriteUInt64LittleEndian(buffer, value); Assert.True(memory.TryWrite(address, buffer)); } + + private static ulong CreateEqueue( + CpuContext ctx, + FakeCpuMemory memory, + ulong handleOutAddress) + { + ctx[CpuRegister.Rdi] = handleOutAddress; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelCreateEqueue(ctx)); + return ReadUInt64(memory, handleOutAddress); + } + + private static void DeleteEqueue(CpuContext ctx, ulong handle) + { + ctx[CpuRegister.Rdi] = handle; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelDeleteEqueue(ctx)); + } } diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueCompatExportsTests.cs index 90ed32a3..22de16ec 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueCompatExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueCompatExportsTests.cs @@ -156,7 +156,13 @@ public sealed class KernelEventQueueCompatExportsTests Assert.Equal(eventIdent, BinaryPrimitives.ReadUInt64LittleEndian(evt[0x00..])); Assert.Equal(KernelEventQueueCompatExports.KernelEventFilterUser, BinaryPrimitives.ReadInt16LittleEndian(evt[0x08..])); - Assert.Equal(triggerData, BinaryPrimitives.ReadUInt64LittleEndian(evt[0x10..])); + + // sceKernelTriggerUserEvent's third argument is the event's *udata*, not + // its data word: the guest reads it back with sceKernelGetEventUserData, + // which loads offset 0x18. Routing the payload to data(0x10) instead left + // sceKernelGetEventUserData returning 0 for every triggered user event. + Assert.Equal(triggerData, BinaryPrimitives.ReadUInt64LittleEndian(evt[0x18..])); + Assert.Equal(0UL, BinaryPrimitives.ReadUInt64LittleEndian(evt[0x10..])); } private static ulong CreateEqueue() diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueWaiterLifetimeTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueWaiterLifetimeTests.cs new file mode 100644 index 00000000..19f25462 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueWaiterLifetimeTests.cs @@ -0,0 +1,151 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.Kernel; + +public sealed class KernelEventQueueWaiterLifetimeTests +{ + private const ulong BaseAddress = 0x1_0000_0000; + private const ulong HandleAddress = BaseAddress + 0x100; + private const ulong EventsAddress = BaseAddress + 0x200; + private const ulong OutCountAddress = BaseAddress + 0x300; + + [Fact] + public void DeleteEqueue_CompletesStagedWaiterAsDeleted() + { + var (memory, ctx, handle) = CreateEqueue(); + var waiter = StageGuestWait(ctx, handle, threadHandle: 0x701); + + ctx[CpuRegister.Rdi] = handle; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelDeleteEqueue(ctx)); + + Assert.True(waiter.TryWake()); + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED, + waiter.Resume()); + Assert.Equal(0u, ReadUInt32(memory, OutCountAddress)); + Assert.False(KernelEventQueueCompatExports.IsValidEqueue(handle)); + + ctx[CpuRegister.Rdi] = handle; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, + KernelEventQueueCompatExports.KernelDeleteEqueue(ctx)); + } + + [Fact] + public void DeletedGenerationWaiter_CannotConsumeNewQueueEvent() + { + var (memory, ctx, oldHandle) = CreateEqueue(); + var oldWaiter = StageGuestWait(ctx, oldHandle, threadHandle: 0x702); + + ctx[CpuRegister.Rdi] = oldHandle; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelDeleteEqueue(ctx)); + + var newHandle = CreateEqueue(ctx, memory); + Assert.NotEqual(oldHandle, newHandle); + var expected = new KernelEventQueueCompatExports.KernelQueuedEvent( + Ident: 0x77, + Filter: KernelEventQueueCompatExports.KernelEventFilterUser, + Flags: KernelEventQueueCompatExports.KernelEventFlagClear, + Fflags: 1, + Data: 0x1234, + UserData: 0x5678); + Assert.True(KernelEventQueueCompatExports.EnqueueEvent( + newHandle, + expected)); + + Assert.True(oldWaiter.TryWake()); + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED, + oldWaiter.Resume()); + Assert.True( + KernelEventQueueCompatExports.TryReservePendingEventForTest( + newHandle, + out var delivered)); + Assert.Equal(expected, delivered); + + ctx[CpuRegister.Rdi] = newHandle; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelDeleteEqueue(ctx)); + } + + private static (FakeCpuMemory Memory, CpuContext Context, ulong Handle) + CreateEqueue() + { + var memory = new FakeCpuMemory(BaseAddress, 0x1000); + var ctx = new CpuContext(memory, Generation.Gen5); + return (memory, ctx, CreateEqueue(ctx, memory)); + } + + private static ulong CreateEqueue(CpuContext ctx, FakeCpuMemory memory) + { + ctx[CpuRegister.Rdi] = HandleAddress; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelCreateEqueue(ctx)); + return ReadUInt64(memory, HandleAddress); + } + + private static IGuestThreadBlockWaiter StageGuestWait( + CpuContext ctx, + ulong handle, + ulong threadHandle) + { + var previousThread = GuestThreadExecution.EnterGuestThread(threadHandle); + var previousFrame = GuestThreadExecution.EnterImportCallFrame( + returnRip: 0x1_0000 + threadHandle, + resumeRsp: 0x2_0000 + threadHandle, + returnSlotAddress: 0x3_0000 + threadHandle); + try + { + ctx[CpuRegister.Rdi] = handle; + ctx[CpuRegister.Rsi] = EventsAddress; + ctx[CpuRegister.Rdx] = 1; + ctx[CpuRegister.Rcx] = OutCountAddress; + ctx[CpuRegister.R8] = 0; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelWaitEqueue(ctx)); + + Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock( + out var reason, + out _, + out var hasContinuation, + out _, + out var waiter, + out _)); + Assert.Equal("sceKernelWaitEqueue", reason); + Assert.True(hasContinuation); + return Assert.IsAssignableFrom(waiter); + } + finally + { + GuestThreadExecution.RestoreImportCallFrame(previousFrame); + GuestThreadExecution.RestoreGuestThread(previousThread); + } + } + + private static uint ReadUInt32(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[sizeof(uint)]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadUInt32LittleEndian(bytes); + } + + private static ulong ReadUInt64(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[sizeof(ulong)]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadUInt64LittleEndian(bytes); + } +}