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).
This commit is contained in:
kuba
2026-07-31 11:13:15 +02:00
committed by GitHub
parent 82c2c7f48c
commit 816ec4ad27
6 changed files with 1393 additions and 139 deletions
File diff suppressed because it is too large Load Diff
@@ -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))
@@ -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;
/// <summary>
/// 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.
/// </summary>
internal static class KernelSyncTraceFormatter
{
internal static string FormatContext(CpuContext ctx)
{
_ = KernelPthreadState.TryGetCurrentThreadIdentity(out var pthread, out var identity);
var threadName = identity.Name ?? "<unknown>";
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 ?? "<unknown>";
return $"thread='{threadName}' pthread=0x{pthread:X16} " +
$"gth=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} " +
$"managed={Environment.CurrentManagedThreadId}";
}
internal static string FormatFrameChain(CpuContext ctx)
{
Span<ulong> 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;
}
}
@@ -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<byte> 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));
}
}
@@ -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()
@@ -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<IGuestThreadBlockWaiter>(waiter);
}
finally
{
GuestThreadExecution.RestoreImportCallFrame(previousFrame);
GuestThreadExecution.RestoreGuestThread(previousThread);
}
}
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
}
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
Assert.True(memory.TryRead(address, bytes));
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
}
}