mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-01 23:49:44 +08:00
816ec4ad27
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).
187 lines
7.3 KiB
C#
187 lines
7.3 KiB
C#
// 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 KernelEventQueueCompatExportsTests
|
|
{
|
|
private const ulong MemoryBase = 0x1_0000_0000;
|
|
private const int MemorySize = 0x4000;
|
|
|
|
[Fact]
|
|
public void CreateEqueue_WritesNonZeroHandleAndSucceeds()
|
|
{
|
|
var (context, outAddress) = NewContextWithOutSlot();
|
|
context[CpuRegister.Rdi] = outAddress;
|
|
|
|
var result = KernelEventQueueCompatExports.KernelCreateEqueue(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
|
Assert.True(context.TryReadUInt64(outAddress, out var handle));
|
|
Assert.NotEqual(0UL, handle);
|
|
Assert.True(KernelEventQueueCompatExports.IsValidEqueue(handle));
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateEqueue_NullOutAddressReturnsInvalidArgument()
|
|
{
|
|
var (context, _) = NewContextWithOutSlot();
|
|
context[CpuRegister.Rdi] = 0;
|
|
|
|
var result = KernelEventQueueCompatExports.KernelCreateEqueue(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeleteEqueue_RemovesQueueFromRegistry()
|
|
{
|
|
var (context, outAddress) = NewContextWithOutSlot();
|
|
context[CpuRegister.Rdi] = outAddress;
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, KernelEventQueueCompatExports.KernelCreateEqueue(context));
|
|
Assert.True(context.TryReadUInt64(outAddress, out var handle));
|
|
|
|
context[CpuRegister.Rdi] = handle;
|
|
var result = KernelEventQueueCompatExports.KernelDeleteEqueue(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
|
Assert.False(KernelEventQueueCompatExports.IsValidEqueue(handle));
|
|
}
|
|
|
|
[Fact]
|
|
public void AddUserEvent_OnUnknownQueueReturnsNotFound()
|
|
{
|
|
var (context, _) = NewContextWithOutSlot();
|
|
const ulong unknownHandle = 0xDEAD_BEEF;
|
|
context[CpuRegister.Rdi] = unknownHandle;
|
|
context[CpuRegister.Rsi] = 42;
|
|
|
|
var result = KernelEventQueueCompatExports.KernelAddUserEvent(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddUserEvent_OnValidQueueSucceeds()
|
|
{
|
|
var handle = CreateEqueue();
|
|
var (context, _) = NewContextWithOutSlot();
|
|
context[CpuRegister.Rdi] = handle;
|
|
context[CpuRegister.Rsi] = 0x1234;
|
|
|
|
var result = KernelEventQueueCompatExports.KernelAddUserEvent(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void TriggerUserEvent_OnUnknownQueueReturnsNotFound()
|
|
{
|
|
var (context, _) = NewContextWithOutSlot();
|
|
context[CpuRegister.Rdi] = 0xDEAD_BEEF;
|
|
context[CpuRegister.Rsi] = 1;
|
|
context[CpuRegister.Rdx] = 0;
|
|
|
|
var result = KernelEventQueueCompatExports.KernelTriggerUserEvent(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void TriggerUserEvent_OnUnregisteredEventReturnsNotFound()
|
|
{
|
|
var handle = CreateEqueue();
|
|
var (context, _) = NewContextWithOutSlot();
|
|
context[CpuRegister.Rdi] = handle;
|
|
context[CpuRegister.Rsi] = 0xABCD; // never registered
|
|
context[CpuRegister.Rdx] = 0;
|
|
|
|
var result = KernelEventQueueCompatExports.KernelTriggerUserEvent(context);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result);
|
|
}
|
|
|
|
// Full lifecycle: create -> register user event -> trigger -> wait delivers
|
|
// the queued event with the registered ident/filter and the trigger data.
|
|
// DequeueEvents runs before the blocking path, so a pre-triggered queue
|
|
// returns immediately without touching the guest thread scheduler.
|
|
[Fact]
|
|
public void CreateAddTriggerWait_DeliversTriggeredUserEvent()
|
|
{
|
|
const ulong eventIdent = 0x4242;
|
|
const ulong triggerData = 0x55AA_55AA;
|
|
var handle = CreateEqueue();
|
|
|
|
// Register the user event on the queue.
|
|
var (addCtx, _) = NewContextWithOutSlot();
|
|
addCtx[CpuRegister.Rdi] = handle;
|
|
addCtx[CpuRegister.Rsi] = eventIdent;
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK,
|
|
KernelEventQueueCompatExports.KernelAddUserEvent(addCtx));
|
|
|
|
// Trigger it with a distinct data payload.
|
|
var (triggerCtx, _) = NewContextWithOutSlot();
|
|
triggerCtx[CpuRegister.Rdi] = handle;
|
|
triggerCtx[CpuRegister.Rsi] = eventIdent;
|
|
triggerCtx[CpuRegister.Rdx] = triggerData;
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK,
|
|
KernelEventQueueCompatExports.KernelTriggerUserEvent(triggerCtx));
|
|
|
|
// Wait should deliver the single pending event without blocking.
|
|
var memory = new FakeCpuMemory(MemoryBase, MemorySize);
|
|
var waitContext = new CpuContext(memory, Generation.Gen5);
|
|
const ulong eventsAddress = MemoryBase + 0x100;
|
|
const ulong outCountAddress = MemoryBase + 0x300;
|
|
waitContext[CpuRegister.Rdi] = handle;
|
|
waitContext[CpuRegister.Rsi] = eventsAddress;
|
|
waitContext[CpuRegister.Rdx] = 1; // capacity
|
|
waitContext[CpuRegister.Rcx] = outCountAddress;
|
|
waitContext[CpuRegister.R8] = 0; // no timeout -> would block, but event is pending
|
|
|
|
var result = KernelEventQueueCompatExports.KernelWaitEqueue(waitContext);
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
|
Assert.True(waitContext.TryReadUInt32(outCountAddress, out var delivered));
|
|
Assert.Equal(1u, delivered);
|
|
|
|
// KernelEvent layout (0x20): ident(0x00) filter(0x08) flags(0x0A)
|
|
// fflags(0x0C) data(0x10) userdata(0x18).
|
|
Span<byte> evt = stackalloc byte[0x20];
|
|
Assert.True(memory.TryRead(eventsAddress, evt));
|
|
Assert.Equal(eventIdent, BinaryPrimitives.ReadUInt64LittleEndian(evt[0x00..]));
|
|
Assert.Equal(KernelEventQueueCompatExports.KernelEventFilterUser,
|
|
BinaryPrimitives.ReadInt16LittleEndian(evt[0x08..]));
|
|
|
|
// 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()
|
|
{
|
|
var memory = new FakeCpuMemory(MemoryBase, MemorySize);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
const ulong outAddress = MemoryBase + 0x10;
|
|
context[CpuRegister.Rdi] = outAddress;
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK,
|
|
KernelEventQueueCompatExports.KernelCreateEqueue(context));
|
|
Assert.True(context.TryReadUInt64(outAddress, out var handle));
|
|
return handle;
|
|
}
|
|
|
|
private static (CpuContext Context, ulong OutAddress) NewContextWithOutSlot()
|
|
{
|
|
var memory = new FakeCpuMemory(MemoryBase, MemorySize);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
return (context, MemoryBase + 0x10);
|
|
}
|
|
}
|