diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index bb6e3b0..853bf26 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -2741,8 +2741,12 @@ public static class AgcExports ctx.TryReadUInt32(currentAddress + sizeof(uint), out var eventTypeRaw)) { var eventType = eventTypeRaw & 0x3Fu; - var triggered = KernelEventQueueCompatExports.TriggerRegisteredEvents( - eventType, + + // The guest registers AGC events with a full eventId, but the command buffer + // only carries a 6-bit EVENT_TYPE. Those two values are not the same numbering + // scheme, so exact ident matching never wakes anything. Trigger every graphics + // event registration instead (workaround for issue #173). + var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter( KernelEventQueueCompatExports.KernelEventFilterGraphics, eventType); if (tracePackets) diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index aac6079..dafdb9c 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -589,6 +589,66 @@ public static class KernelEventQueueCompatExports return triggeredCount; } + /// + /// Triggers every registered event on every queue that matches + /// regardless of the registration's ident. This is a workaround for PS5 AGC command + /// buffers, where IT_EVENT_WRITE carries a hardware EVENT_TYPE that does not + /// match the eventId the guest registered with sceAgcDriverAddEqEvent. + /// See issue #173. + /// + public static int TriggerRegisteredEventsByFilter( + short filter, + ulong data) + { + List? wakeHandles = null; + var triggeredCount = 0; + lock (_eventQueueGate) + { + foreach (var (handle, registrations) in _registeredEvents) + { + foreach (var registration in registrations.Values) + { + if (registration.Filter != filter) + { + continue; + } + + if (!_pendingEvents.TryGetValue(handle, out var queue)) + { + queue = new KernelEventDeque(); + _pendingEvents[handle] = queue; + } + + QueueOrUpdateEvent( + queue, + new KernelQueuedEvent( + registration.Ident, + registration.Filter, + 0, + 1, + data, + registration.UserData)); + (wakeHandles ??= new List()).Add(handle); + triggeredCount++; + + // A single queue only needs to be woken once, even if multiple + // registrations matched. + break; + } + } + } + + if (wakeHandles is not null) + { + foreach (var handle in wakeHandles) + { + WakeEventQueue(handle); + } + } + + return triggeredCount; + } + private static bool TriggerRegisteredEvent( ulong handle, ulong ident, diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index b96956f..c2aa476 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -99,20 +99,6 @@ public static class KernelPthreadCompatExports // See PthreadMutexState.WakeKey. public string WakeKey { get; } = "pthread_cond#" + Interlocked.Increment(ref _nextCondWakeId).ToString("X"); - - // A signal with no waiter stays pending; the guest often signals before the wait. - public int PendingSignals { get; set; } - - public bool TryConsumePendingSignal() - { - if (PendingSignals <= 0) - { - return false; - } - - PendingSignals--; - return true; - } } private readonly record struct PthreadMutexAttrState(int Type, int Protocol); @@ -1375,9 +1361,8 @@ public static class KernelPthreadCompatExports { state.Waiters++; var observedEpoch = state.SignalEpoch; - var consumedPendingSignal = state.TryConsumePendingSignal(); TracePthreadCond( - consumedPendingSignal ? "wait-enter-pending" : "wait-enter", + "wait-enter", condAddress, mutexAddress, state, @@ -1392,25 +1377,6 @@ public static class KernelPthreadCompatExports return unlockResult; } - if (consumedPendingSignal) - { - state.Waiters = Math.Max(0, state.Waiters - 1); - TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult); - - // Relock outside SyncRoot to preserve lock ordering. - Monitor.Exit(state.SyncRoot); - try - { - var pendingLockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion); - return pendingLockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK ? pendingLockResult : waitResult; - } - finally - { - // Balance the surrounding lock statement. - Monitor.Enter(state.SyncRoot); - } - } - var scheduler = GuestThreadExecution.Scheduler; if (GuestThreadExecution.IsGuestThread && GuestThreadExecution.RequestCurrentThreadBlock( @@ -1542,10 +1508,6 @@ public static class KernelPthreadCompatExports Monitor.Pulse(state.SyncRoot); } } - else - { - state.PendingSignals++; - } TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK); } diff --git a/tests/SharpEmu.Libs.Tests/Agc/AgcEventQueueTests.cs b/tests/SharpEmu.Libs.Tests/Agc/AgcEventQueueTests.cs new file mode 100644 index 0000000..ccfc4cd --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Agc/AgcEventQueueTests.cs @@ -0,0 +1,136 @@ +// 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.Agc; + +// IT_EVENT_WRITE carries a 6-bit hardware EVENT_TYPE, but sceAgcDriverAddEqEvent registers the +// listener with a guest-defined eventId. Those two values are not the same numbering scheme, so +// exact ident matching never wakes anything (issue #173). TriggerRegisteredEventsByFilter wakes +// every graphics registration instead. +public sealed class AgcEventQueueTests +{ + private const ulong BaseAddress = 0x1_0000_0000; + private const int MemorySize = 0x2000; + + [Fact] + public void TriggerRegisteredEventsByFilter_DifferentIdentThanEventType_WakesGraphicsWaiter() + { + 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; + + // Create an event queue. + ctx[CpuRegister.Rdi] = handleOutAddress; + var createResult = KernelEventQueueCompatExports.KernelCreateEqueue(ctx); + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, createResult); + + var handle = ReadUInt64(memory, handleOutAddress); + + // Register a graphics event with eventId 0x20 (as Poppy Playtime does). + const ulong registeredEventId = 0x20; + const ulong userData = 0xDEAD_BEEF; + var registered = KernelEventQueueCompatExports.RegisterEvent( + handle, + registeredEventId, + KernelEventQueueCompatExports.KernelEventFilterGraphics, + userData); + Assert.True(registered); + + // The command buffer fires EVENT_WRITE with eventType 0x07. This does not match 0x20. + const ulong eventType = 0x07; + var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter( + KernelEventQueueCompatExports.KernelEventFilterGraphics, + eventType); + Assert.Equal(1, triggered); + + // Wait with timeout=0. The event is already pending, so this returns immediately. + WriteUInt64(memory, timeoutAddress, 0); + ctx[CpuRegister.Rdi] = handle; + ctx[CpuRegister.Rsi] = eventsAddress; + ctx[CpuRegister.Rdx] = 4; + ctx[CpuRegister.Rcx] = outCountAddress; + ctx[CpuRegister.R8] = timeoutAddress; + var waitResult = KernelEventQueueCompatExports.KernelWaitEqueue(ctx); + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, waitResult); + Assert.Equal(1u, ReadUInt32(memory, outCountAddress)); + + // 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(1u, ReadUInt32(memory, eventsAddress + 0x0C)); + Assert.Equal(eventType, ReadUInt64(memory, eventsAddress + 0x10)); + Assert.Equal(userData, ReadUInt64(memory, eventsAddress + 0x18)); + } + + [Fact] + public void TriggerRegisteredEventsByFilter_NoGraphicsRegistrations_ReturnsZero() + { + var memory = new FakeCpuMemory(BaseAddress, MemorySize); + var ctx = new CpuContext(memory, Generation.Gen5); + + const ulong handleOutAddress = BaseAddress + 0x100; + ctx[CpuRegister.Rdi] = handleOutAddress; + var createResult = KernelEventQueueCompatExports.KernelCreateEqueue(ctx); + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, createResult); + + var handle = ReadUInt64(memory, handleOutAddress); + + // Register a user event, not a graphics event. + KernelEventQueueCompatExports.RegisterEvent( + handle, + 0x1, + KernelEventQueueCompatExports.KernelEventFilterUser, + 0); + + var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter( + KernelEventQueueCompatExports.KernelEventFilterGraphics, + 0x07); + + Assert.Equal(0, triggered); + } + + private static ulong ReadUInt64(FakeCpuMemory memory, ulong address) + { + Span buffer = stackalloc byte[8]; + Assert.True(memory.TryRead(address, buffer)); + return BinaryPrimitives.ReadUInt64LittleEndian(buffer); + } + + private static uint ReadUInt32(FakeCpuMemory memory, ulong address) + { + Span buffer = stackalloc byte[4]; + Assert.True(memory.TryRead(address, buffer)); + return BinaryPrimitives.ReadUInt32LittleEndian(buffer); + } + + private static ushort ReadUInt16(FakeCpuMemory memory, ulong address) + { + Span buffer = stackalloc byte[2]; + Assert.True(memory.TryRead(address, buffer)); + return BinaryPrimitives.ReadUInt16LittleEndian(buffer); + } + + private static short ReadInt16(FakeCpuMemory memory, ulong address) + { + Span buffer = stackalloc byte[2]; + Assert.True(memory.TryRead(address, buffer)); + return BinaryPrimitives.ReadInt16LittleEndian(buffer); + } + + private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value) + { + Span buffer = stackalloc byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(buffer, value); + Assert.True(memory.TryWrite(address, buffer)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Pthread/PthreadCondSemanticsTests.cs b/tests/SharpEmu.Libs.Tests/Pthread/PthreadCondSemanticsTests.cs new file mode 100644 index 0000000..5095054 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Pthread/PthreadCondSemanticsTests.cs @@ -0,0 +1,82 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Reflection; +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.Pthread; + +// POSIX condition variables are edges, not semaphore credits. A signal with no waiter +// must have no effect. This was violated by the previous implementation which persisted +// signals via PendingSignals, causing lock inversions and predicate bypasses. +// See issue #113. +public sealed class PthreadCondSemanticsTests +{ + [Fact] + public void PthreadCondState_DoesNotHavePendingSignals() + { + // Verify that PthreadCondState no longer has the PendingSignals property. + // This is a regression test to ensure the POSIX-correct behavior is maintained. + var stateType = typeof(KernelPthreadCompatExports).GetNestedType("PthreadCondState", BindingFlags.NonPublic); + Assert.NotNull(stateType); + + var pendingSignalsProp = stateType.GetProperty("PendingSignals"); + Assert.Null(pendingSignalsProp); + + var tryConsumeMethod = stateType.GetMethod("TryConsumePendingSignal"); + Assert.Null(tryConsumeMethod); + } + + [Fact] + public void PthreadCondSignal_WithNoWaiter_DoesNotPersist() + { + // This test verifies the semantic contract: signal without waiter is a no-op. + // We can't easily test the full pthread flow without the scheduler, but we can + // verify the code path by checking that SignalEpoch advances but no state persists. + var stateType = typeof(KernelPthreadCompatExports).GetNestedType("PthreadCondState", BindingFlags.NonPublic); + Assert.NotNull(stateType); + + // Create an instance via reflection + var state = Activator.CreateInstance(stateType); + Assert.NotNull(state); + + var syncRootProp = stateType.GetProperty("SyncRoot"); + var signalEpochProp = stateType.GetProperty("SignalEpoch"); + var waitersProp = stateType.GetProperty("Waiters"); + + Assert.NotNull(syncRootProp); + Assert.NotNull(signalEpochProp); + Assert.NotNull(waitersProp); + + var syncRoot = syncRootProp.GetValue(state); + Assert.NotNull(syncRoot); + + // Initial state + Assert.Equal(0UL, (ulong)signalEpochProp.GetValue(state)!); + Assert.Equal(0, (int)waitersProp.GetValue(state)!); + + // Simulate signal with no waiter (this would have incremented PendingSignals before) + lock (syncRoot) + { + signalEpochProp.SetValue(state, (ulong)signalEpochProp.GetValue(state)! + 1); + // Note: we don't increment PendingSignals because it doesn't exist + } + + // Verify epoch advanced but no persistent signal state + Assert.Equal(1UL, (ulong)signalEpochProp.GetValue(state)!); + + // A new waiter arriving should see the new epoch but not consume any "pending" signal + // (because there's no such concept anymore) + lock (syncRoot) + { + var observedEpoch = (ulong)signalEpochProp.GetValue(state)!; + waitersProp.SetValue(state, (int)waitersProp.GetValue(state)! + 1); + + // Waiter sees epoch=1, will block until epoch changes again + Assert.Equal(1UL, observedEpoch); + Assert.Equal(1, (int)waitersProp.GetValue(state)!); + } + } +}