[Tests] Cover kernel sync, pthread stack bounds and NpWebApi2
Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
This commit is contained in:
parent
c6744d9b13
commit
9667375750
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
public sealed class GuestThreadStackBoundsTests
|
||||
{
|
||||
[NativeX64Fact]
|
||||
public void SchedulerReturnsAuthoritativeBoundsAcrossTheFullSlotWindow()
|
||||
{
|
||||
const BindingFlags privateInstance = BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
const ulong stackSize = 0x20_0000;
|
||||
const ulong stackStride = 0x100_0000;
|
||||
var highestStack = OperatingSystem.IsWindows()
|
||||
? 0x0000_7FFF_E000_0000UL
|
||||
: 0x0000_6FFF_E000_0000UL;
|
||||
var backendType = typeof(DirectExecutionBackend);
|
||||
var threadType = backendType.GetNestedType("GuestThreadState", BindingFlags.NonPublic)!;
|
||||
var modules = new ModuleManager();
|
||||
modules.Freeze();
|
||||
using var backend = new DirectExecutionBackend(modules);
|
||||
var gate = backendType.GetField("_guestThreadGate", privateInstance)!.GetValue(backend)!;
|
||||
var threads = (IDictionary)backendType.GetField("_guestThreads", privateInstance)!.GetValue(backend)!;
|
||||
|
||||
foreach (var slot in new[] { 0, 48, 56, 1023 })
|
||||
{
|
||||
var handle = 0x1000UL + (ulong)slot;
|
||||
var expectedBase = highestStack - ((ulong)slot * stackStride);
|
||||
var thread = Activator.CreateInstance(threadType)!;
|
||||
threadType.GetProperty("ThreadHandle")!.SetValue(thread, handle);
|
||||
threadType.GetProperty("StackBase")!.SetValue(thread, expectedBase);
|
||||
threadType.GetProperty("StackSize")!.SetValue(thread, stackSize);
|
||||
lock (gate)
|
||||
threads.Add(handle, thread);
|
||||
|
||||
Assert.True(backend.TryGetGuestThreadStackBounds(handle, out var actualBase, out var actualSize));
|
||||
Assert.Equal(expectedBase, actualBase);
|
||||
Assert.Equal(stackSize, actualSize);
|
||||
}
|
||||
|
||||
Assert.False(backend.TryGetGuestThreadStackBounds(0xDEAD, out var missingBase, out var missingSize));
|
||||
Assert.Equal(0UL, missingBase);
|
||||
Assert.Equal(0UL, missingSize);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Reflection;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Kernel;
|
||||
|
||||
[Collection("GuestThreadFlowProfile")]
|
||||
public sealed class KernelHostWaitExceptionTests
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_3000_0000;
|
||||
private const ulong SemaphoreAddress = MemoryBase + 0x100;
|
||||
private const ulong NameAddress = MemoryBase + 0x200;
|
||||
private const ulong ValueAddress = MemoryBase + 0x300;
|
||||
private const ulong TimeoutAddress = MemoryBase + 0x400;
|
||||
|
||||
[Fact]
|
||||
public void SemaphoreHostWaitDeliversPendingExceptionBeforeReturning()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||
var createContext = new CpuContext(memory, Generation.Gen5);
|
||||
Assert.True(memory.TryWrite(NameAddress, "exception-sema\0"u8));
|
||||
createContext[CpuRegister.Rdi] = SemaphoreAddress;
|
||||
createContext[CpuRegister.Rsi] = NameAddress;
|
||||
createContext[CpuRegister.Rdx] = 1;
|
||||
createContext[CpuRegister.Rcx] = 0;
|
||||
createContext[CpuRegister.R8] = 1;
|
||||
createContext[CpuRegister.R9] = 0;
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelCreateSema(createContext));
|
||||
Assert.True(createContext.TryReadUInt64(SemaphoreAddress, out var handle));
|
||||
|
||||
WriteUInt32(memory, TimeoutAddress, 1_000_000);
|
||||
var waitContext = new CpuContext(memory, Generation.Gen5);
|
||||
waitContext[CpuRegister.Rdi] = handle;
|
||||
waitContext[CpuRegister.Rsi] = 1;
|
||||
waitContext[CpuRegister.Rdx] = TimeoutAddress;
|
||||
var signalContext = new CpuContext(memory, Generation.Gen5);
|
||||
var scheduler = DispatchProxy.Create<IGuestThreadScheduler, PendingExceptionScheduler>();
|
||||
var proxy = (PendingExceptionScheduler)(object)scheduler;
|
||||
proxy.DeliverException = context =>
|
||||
{
|
||||
Assert.Same(waitContext, context);
|
||||
Assert.Equal(
|
||||
0,
|
||||
KernelSemaphoreCompatExports.KernelSignalSema(
|
||||
signalContext,
|
||||
unchecked((uint)handle),
|
||||
1));
|
||||
};
|
||||
|
||||
var previousScheduler = GuestThreadExecution.Scheduler;
|
||||
try
|
||||
{
|
||||
GuestThreadExecution.Scheduler = scheduler;
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelWaitSema(waitContext));
|
||||
Assert.Equal(1, proxy.DeliverCalls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadExecution.Scheduler = previousScheduler;
|
||||
createContext[CpuRegister.Rdi] = handle;
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelDeleteSema(createContext));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SyncOnAddressHostWaitDeliversPendingExceptionBeforeReturning()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||
WriteUInt32(memory, ValueAddress, 7);
|
||||
WriteUInt32(memory, TimeoutAddress, 1_000_000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = ValueAddress;
|
||||
context[CpuRegister.Rsi] = 7;
|
||||
context[CpuRegister.Rdx] = TimeoutAddress;
|
||||
var scheduler = DispatchProxy.Create<IGuestThreadScheduler, PendingExceptionScheduler>();
|
||||
var proxy = (PendingExceptionScheduler)(object)scheduler;
|
||||
proxy.DeliverException = deliveredContext =>
|
||||
{
|
||||
Assert.Same(context, deliveredContext);
|
||||
WriteUInt32(memory, ValueAddress, 8);
|
||||
};
|
||||
|
||||
var previousScheduler = GuestThreadExecution.Scheduler;
|
||||
try
|
||||
{
|
||||
GuestThreadExecution.Scheduler = scheduler;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
KernelSyncOnAddressCompatExports.SyncOnAddressWait32(context));
|
||||
Assert.Equal(1, proxy.DeliverCalls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadExecution.Scheduler = previousScheduler;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
Assert.True(memory.TryWrite(address, bytes));
|
||||
}
|
||||
|
||||
public class PendingExceptionScheduler : DispatchProxy
|
||||
{
|
||||
private int _deliverCalls;
|
||||
|
||||
public Action<CpuContext>? DeliverException { get; set; }
|
||||
|
||||
public int DeliverCalls => Volatile.Read(ref _deliverCalls);
|
||||
|
||||
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
|
||||
{
|
||||
if (targetMethod?.Name == nameof(IGuestThreadScheduler.DeliverPendingGuestExceptionIfReady))
|
||||
{
|
||||
Interlocked.Increment(ref _deliverCalls);
|
||||
DeliverException!((CpuContext)args![0]!);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (targetMethod?.Name == nameof(IGuestThreadScheduler.WakeBlockedThreads))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw new NotSupportedException(targetMethod?.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Kernel;
|
||||
|
||||
public sealed class KernelSemaphoreCompatExportsTests
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private const ulong SemaphoreAddress = MemoryBase + 0x100;
|
||||
private const ulong NameAddress = MemoryBase + 0x200;
|
||||
|
||||
[Fact]
|
||||
public void KernelCreateSemaWritesPointerSizedHandleAndClearsUpperBits()
|
||||
{
|
||||
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
||||
Assert.True(context.TryWriteUInt64(SemaphoreAddress, 0xC0DE_C0DE_CAFE_BA00));
|
||||
Assert.True(context.Memory.TryWrite(NameAddress, "unity-worker\0"u8));
|
||||
|
||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = 1;
|
||||
context[CpuRegister.Rcx] = 0;
|
||||
context[CpuRegister.R8] = 1;
|
||||
context[CpuRegister.R9] = 0;
|
||||
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelCreateSema(context));
|
||||
Assert.True(context.TryReadUInt64(SemaphoreAddress, out var handle));
|
||||
Assert.NotEqual(0UL, handle);
|
||||
Assert.Equal(0UL, handle >> 32);
|
||||
|
||||
context[CpuRegister.Rdi] = handle;
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelDeleteSema(context));
|
||||
}
|
||||
}
|
||||
|
|
@ -145,6 +145,166 @@ public sealed class KernelSyncOnAddressCompatExportsTests
|
|||
waiter.Resume());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxWaitReturnsImmediatelyWhenValueDoesNotMatch()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 7);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 9;
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = 0;
|
||||
|
||||
RunAsGuest(0x809, () => Assert.Equal(
|
||||
0,
|
||||
KernelSyncOnAddressCompatExports.UmtxOp(ctx)));
|
||||
|
||||
Assert.False(GuestThreadExecution.TryConsumeCurrentThreadBlock(out _));
|
||||
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxWaitAndWakeShareTheAddressWaitRegistry()
|
||||
{
|
||||
var (memory, waitContext) = CreateContext();
|
||||
var wakeContext = new CpuContext(memory, Generation.Gen5);
|
||||
WriteUInt64(memory, ValueAddress, 0);
|
||||
|
||||
IGuestThreadBlockWaiter? stagedWaiter = null;
|
||||
waitContext[CpuRegister.Rdi] = ValueAddress;
|
||||
waitContext[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
waitContext[CpuRegister.Rdx] = 0;
|
||||
waitContext[CpuRegister.Rcx] = 0;
|
||||
waitContext[CpuRegister.R8] = 0;
|
||||
RunAsGuest(0x80A, () =>
|
||||
{
|
||||
Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(waitContext));
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock(
|
||||
out var reason,
|
||||
out _,
|
||||
out var hasContinuation,
|
||||
out _,
|
||||
out var waiter,
|
||||
out var deadline));
|
||||
Assert.Equal("_umtx_op.wait", reason);
|
||||
Assert.True(hasContinuation);
|
||||
Assert.Equal(0, deadline);
|
||||
stagedWaiter = Assert.IsAssignableFrom<IGuestThreadBlockWaiter>(waiter);
|
||||
});
|
||||
|
||||
wakeContext[CpuRegister.Rdi] = ValueAddress;
|
||||
wakeContext[CpuRegister.Rsi] = 3; // UMTX_OP_WAKE
|
||||
wakeContext[CpuRegister.Rdx] = 1;
|
||||
wakeContext[CpuRegister.Rcx] = 0;
|
||||
wakeContext[CpuRegister.R8] = 0;
|
||||
Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(wakeContext));
|
||||
|
||||
var resumed = Assert.IsAssignableFrom<IGuestThreadBlockWaiter>(stagedWaiter);
|
||||
Assert.True(resumed.TryWake());
|
||||
Assert.Equal(0, resumed.Resume());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxZeroTimespecReturnsMinusOneAndTimedOutErrno()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 12);
|
||||
WriteUInt64(memory, TimeoutAddress, 0);
|
||||
WriteUInt64(memory, TimeoutAddress + sizeof(long), 0);
|
||||
SetErrnoStorage(ctx);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 12;
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = TimeoutAddress;
|
||||
|
||||
Assert.Equal(-1, KernelSyncOnAddressCompatExports.UmtxOp(ctx));
|
||||
Assert.Equal(ulong.MaxValue, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(60, ReadInt32(memory, ctx.FsBase + 0x40)); // ETIMEDOUT
|
||||
Assert.False(GuestThreadExecution.TryConsumeCurrentThreadBlock(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxRejectsInvalidTimespecWithEinval()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 12);
|
||||
WriteUInt64(memory, TimeoutAddress, 0);
|
||||
WriteUInt64(memory, TimeoutAddress + sizeof(long), 1_000_000_000);
|
||||
SetErrnoStorage(ctx);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 12;
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = TimeoutAddress;
|
||||
|
||||
Assert.Equal(-1, KernelSyncOnAddressCompatExports.UmtxOp(ctx));
|
||||
Assert.Equal(22, ReadInt32(memory, ctx.FsBase + 0x40)); // EINVAL
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxLargeTimespecSaturatesWithoutOverflow()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 1);
|
||||
WriteUInt64(memory, TimeoutAddress, unchecked((ulong)long.MaxValue));
|
||||
WriteUInt64(memory, TimeoutAddress + sizeof(long), 999_999_999);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 2; // Already changed, so no host wait.
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = TimeoutAddress;
|
||||
|
||||
Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxTimedContinuationReturnsPosixTimeout()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 21);
|
||||
WriteUInt64(memory, TimeoutAddress, 0);
|
||||
WriteUInt64(memory, TimeoutAddress + sizeof(long), 1_000_000); // 1 ms
|
||||
SetErrnoStorage(ctx);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 21;
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = TimeoutAddress;
|
||||
|
||||
IGuestThreadBlockWaiter? stagedWaiter = null;
|
||||
RunAsGuest(0x80B, () =>
|
||||
{
|
||||
Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(ctx));
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock(
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out var waiter,
|
||||
out var deadline));
|
||||
Assert.True(deadline > 0);
|
||||
stagedWaiter = Assert.IsAssignableFrom<IGuestThreadBlockWaiter>(waiter);
|
||||
});
|
||||
|
||||
Thread.Sleep(10);
|
||||
var resumed = Assert.IsAssignableFrom<IGuestThreadBlockWaiter>(stagedWaiter);
|
||||
Assert.Equal(-1, resumed.Resume());
|
||||
Assert.Equal(60, ReadInt32(memory, ctx.FsBase + 0x40)); // ETIMEDOUT
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxExportIsRegisteredForGen5()
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
|
||||
Assert.True(manager.TryGetExport("04AjkP0jO9U", out var export));
|
||||
Assert.Equal("_umtx_op", export.Name);
|
||||
Assert.Equal("libKernel", export.LibraryName);
|
||||
}
|
||||
|
||||
private static (FakeCpuMemory Memory, CpuContext Context) CreateContext()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
|
|
@ -213,4 +373,16 @@ public sealed class KernelSyncOnAddressCompatExportsTests
|
|||
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
|
||||
Assert.True(memory.TryWrite(address, bytes));
|
||||
}
|
||||
|
||||
private static void SetErrnoStorage(CpuContext ctx)
|
||||
{
|
||||
ctx.FsBase = BaseAddress + 0x700;
|
||||
}
|
||||
|
||||
private static int ReadInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
return BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ public sealed class SemaphoreSignalProfileTests
|
|||
|
||||
[Theory]
|
||||
[InlineData(0, 2, true)]
|
||||
[InlineData(0, 0, false)]
|
||||
[InlineData(0, 0, true)]
|
||||
[InlineData(1, -1, false)]
|
||||
[InlineData(1, int.MaxValue, false)]
|
||||
public void SignalKeepsTokenSemanticsAndRecordsStagesWhenEnabled(uint initial, int count, bool succeeds)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Np;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Np;
|
||||
|
||||
public sealed class NpWebApi2ExportsTests
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private const int InvalidArgument = unchecked((int)0x80553402);
|
||||
|
||||
[Fact]
|
||||
public void PushEventRegisterCallback_ValidatesStateAndReturnsCallbackId()
|
||||
{
|
||||
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
||||
NpWebApi2Exports.ResetRuntimeState();
|
||||
try
|
||||
{
|
||||
context[CpuRegister.Rdi] = 1;
|
||||
context[CpuRegister.Rsi] = 0x1000;
|
||||
var libraryContextId = NpWebApi2Exports.NpWebApi2Initialize(context);
|
||||
Assert.True(libraryContextId > 0);
|
||||
|
||||
context[CpuRegister.Rdi] = unchecked((ulong)libraryContextId);
|
||||
context[CpuRegister.Rsi] = 0x1000_0000;
|
||||
var userContextId = NpWebApi2Exports.NpWebApi2CreateUserContext(context);
|
||||
Assert.True(userContextId > 0);
|
||||
|
||||
context[CpuRegister.Rdi] = unchecked((ulong)libraryContextId);
|
||||
var filterId = NpWebApi2Exports.NpWebApi2PushEventCreateFilter(context);
|
||||
Assert.True(filterId > 0);
|
||||
|
||||
context[CpuRegister.Rdi] = unchecked((ulong)userContextId);
|
||||
context[CpuRegister.Rsi] = unchecked((ulong)filterId);
|
||||
context[CpuRegister.Rdx] = 0x8000_1000;
|
||||
context[CpuRegister.Rcx] = 0x1234;
|
||||
Assert.True(NpWebApi2Exports.NpWebApi2PushEventRegisterCallback(context) > 0);
|
||||
|
||||
context[CpuRegister.Rdx] = 0;
|
||||
Assert.Equal(
|
||||
InvalidArgument,
|
||||
NpWebApi2Exports.NpWebApi2PushEventRegisterCallback(context));
|
||||
}
|
||||
finally
|
||||
{
|
||||
NpWebApi2Exports.ResetRuntimeState();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PushEventRegisterCallback_RegistersForGen5()
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
|
||||
Assert.True(manager.TryGetExport("fY3QqeNkF8k", out var export));
|
||||
Assert.Equal("sceNpWebApi2PushEventRegisterCallback", export.Name);
|
||||
Assert.Equal("libSceNpWebApi2", export.LibraryName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PushEventDeleteHandle_IsIdempotentAndRegisteredForGen5()
|
||||
{
|
||||
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = 1;
|
||||
context[CpuRegister.Rsi] = 1;
|
||||
Assert.Equal(0, NpWebApi2Exports.NpWebApi2PushEventDeleteHandle(context));
|
||||
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
Assert.True(manager.TryGetExport("fIATVMo4Y1w", out var export));
|
||||
Assert.Equal("sceNpWebApi2PushEventDeleteHandle", export.Name);
|
||||
Assert.Equal("libSceNpWebApi2", export.LibraryName);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ public sealed class PthreadSemaphoreSemanticsTests
|
|||
[InlineData("C36iRE0F5sE", "scePthreadSemWait")]
|
||||
[InlineData("aishVAiFaYM", "scePthreadSemPost")]
|
||||
[InlineData("H2a+IN9TP0E", "scePthreadSemTrywait")]
|
||||
[InlineData("fjN6NQHhK8k", "scePthreadSemTimedwait")]
|
||||
[InlineData("GEnUkDZoUwY", "scePthreadSemInit")]
|
||||
[InlineData("Vwc+L05e6oE", "scePthreadSemDestroy")]
|
||||
public void RegistryResolvesPthreadSemaphoreExports(string nid, string exportName)
|
||||
|
|
@ -68,6 +69,41 @@ public sealed class PthreadSemaphoreSemanticsTests
|
|||
DestroySemaphore(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PthreadSemTimedWaitTreatsSecondArgumentAsRelativeMicroseconds()
|
||||
{
|
||||
var context = CreateContext();
|
||||
InitializeSemaphore(context, initialCount: 1);
|
||||
|
||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
||||
context[CpuRegister.Rsi] = 250_000;
|
||||
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemTimedWait(context));
|
||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
||||
Assert.Equal(0, ReadSemaphoreValue(context));
|
||||
|
||||
DestroySemaphore(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PthreadSemTimedWaitWithZeroTimeoutReturnsTimedOutWhenEmpty()
|
||||
{
|
||||
var context = CreateContext();
|
||||
InitializeSemaphore(context, initialCount: 0);
|
||||
|
||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
||||
context[CpuRegister.Rsi] = 0;
|
||||
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT,
|
||||
KernelSemaphoreCompatExports.PthreadSemTimedWait(context));
|
||||
Assert.Equal(
|
||||
unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT),
|
||||
context[CpuRegister.Rax]);
|
||||
|
||||
DestroySemaphore(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PthreadSemInitRejectsNonPrivateFlag()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
// 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;
|
||||
|
||||
[CollectionDefinition(Name, DisableParallelization = true)]
|
||||
public sealed class PthreadStackBoundsCollection
|
||||
{
|
||||
public const string Name = "PthreadStackBounds";
|
||||
}
|
||||
|
||||
[Collection(PthreadStackBoundsCollection.Name)]
|
||||
public sealed class PthreadStackBoundsTests : IDisposable
|
||||
{
|
||||
private const ulong MemoryBase = 0x6_2000_0000;
|
||||
private const ulong ThreadHandle = 0x1234_5000;
|
||||
private const ulong AttrAddress = MemoryBase + 0x100;
|
||||
private const ulong OutStackAddress = MemoryBase + 0x200;
|
||||
private const ulong OutStackSizeAddress = MemoryBase + 0x208;
|
||||
private readonly IGuestThreadScheduler? _previousScheduler = GuestThreadExecution.Scheduler;
|
||||
|
||||
[Fact]
|
||||
public void PosixGetAttrUsesSchedulerBoundsForRemoteHighSlotThread()
|
||||
{
|
||||
const ulong stackBase = 0x0000_7FFF_A800_0000;
|
||||
const ulong stackSize = 0x20_0000;
|
||||
var scheduler = CreateScheduler(ThreadHandle, stackBase, stackSize);
|
||||
GuestThreadExecution.Scheduler = scheduler;
|
||||
|
||||
var context = CreateContext();
|
||||
context[CpuRegister.Rsp] = stackBase + stackSize - 0xEA0;
|
||||
context[CpuRegister.Rdi] = ThreadHandle;
|
||||
context[CpuRegister.Rsi] = AttrAddress;
|
||||
|
||||
Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGetNpPOSIX(context));
|
||||
AssertStackBounds(context, stackBase, stackSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentPrimaryThreadRetainsBoundedStackPointerFallback()
|
||||
{
|
||||
const ulong primaryThreadHandle = ThreadHandle + 1;
|
||||
var stackBase = OperatingSystem.IsWindows()
|
||||
? 0x0000_7FFF_F000_0000UL
|
||||
: 0x0000_6FFF_F000_0000UL;
|
||||
GuestThreadExecution.Scheduler = CreateScheduler();
|
||||
var previousThread = GuestThreadExecution.EnterGuestThread(primaryThreadHandle);
|
||||
try
|
||||
{
|
||||
var context = CreateContext();
|
||||
context[CpuRegister.Rsp] = stackBase + 0x1F_F160;
|
||||
context[CpuRegister.Rdi] = primaryThreadHandle;
|
||||
context[CpuRegister.Rsi] = AttrAddress + 0x20;
|
||||
|
||||
Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGet(context));
|
||||
AssertStackBounds(context, stackBase, 0x20_0000, AttrAddress + 0x20);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadExecution.RestoreGuestThread(previousThread);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidSchedulerBoundsDoNotReplaceDefaultAttributes()
|
||||
{
|
||||
const ulong invalidThreadHandle = ThreadHandle + 2;
|
||||
GuestThreadExecution.Scheduler = CreateScheduler(invalidThreadHandle, 0, 0x20_0000);
|
||||
var context = CreateContext();
|
||||
context[CpuRegister.Rdi] = invalidThreadHandle;
|
||||
context[CpuRegister.Rsi] = AttrAddress + 0x40;
|
||||
|
||||
Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGet(context));
|
||||
AssertStackBounds(context, 0, 0x10_0000, AttrAddress + 0x40);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GuestThreadExecution.Scheduler = _previousScheduler;
|
||||
}
|
||||
|
||||
private static CpuContext CreateContext() =>
|
||||
new(new FakeCpuMemory(MemoryBase, 0x4000), Generation.Gen5);
|
||||
|
||||
private static IGuestThreadScheduler CreateScheduler(
|
||||
ulong threadHandle = 0,
|
||||
ulong stackBase = 0,
|
||||
ulong stackSize = 0)
|
||||
{
|
||||
var scheduler = DispatchProxy.Create<IGuestThreadScheduler, StackBoundsSchedulerProxy>();
|
||||
var proxy = (StackBoundsSchedulerProxy)(object)scheduler;
|
||||
if (threadHandle != 0)
|
||||
{
|
||||
proxy.Bounds[threadHandle] = (stackBase, stackSize);
|
||||
}
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
private static void AssertStackBounds(
|
||||
CpuContext context,
|
||||
ulong expectedBase,
|
||||
ulong expectedSize,
|
||||
ulong attrAddress = AttrAddress)
|
||||
{
|
||||
context[CpuRegister.Rdi] = attrAddress;
|
||||
context[CpuRegister.Rsi] = OutStackAddress;
|
||||
context[CpuRegister.Rdx] = OutStackSizeAddress;
|
||||
Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGetstackPOSIX(context));
|
||||
Assert.True(context.TryReadUInt64(OutStackAddress, out var actualBase));
|
||||
Assert.True(context.TryReadUInt64(OutStackSizeAddress, out var actualSize));
|
||||
Assert.Equal(expectedBase, actualBase);
|
||||
Assert.Equal(expectedSize, actualSize);
|
||||
}
|
||||
|
||||
public class StackBoundsSchedulerProxy : DispatchProxy
|
||||
{
|
||||
public Dictionary<ulong, (ulong Base, ulong Size)> Bounds { get; } = [];
|
||||
|
||||
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
|
||||
{
|
||||
if (targetMethod?.Name == nameof(IGuestThreadScheduler.TryGetGuestThreadStackBounds))
|
||||
{
|
||||
var found = Bounds.TryGetValue((ulong)args![0]!, out var bounds);
|
||||
args[1] = found ? bounds.Base : 0UL;
|
||||
args[2] = found ? bounds.Size : 0UL;
|
||||
return found;
|
||||
}
|
||||
|
||||
throw new NotSupportedException(targetMethod?.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue