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); + } +}