diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index 978828cc..79b6195f 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -1438,6 +1438,9 @@ public sealed partial class DirectExecutionBackend var expectedMutexTrylockBusy = string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) && result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; + var expectedSemaphoreTrywaitAgain = + string.Equals(nid, "H2a+IN9TP0E", StringComparison.Ordinal) && + result == OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN; var expectedNetAcceptWouldBlock = string.Equals(nid, "PIWqhn9oSxc", StringComparison.Ordinal) && resultValue == unchecked((int)0x80410123); @@ -1451,6 +1454,7 @@ public sealed partial class DirectExecutionBackend !expectedTimedWaitTimeout && !expectedEqueueTimeout && !expectedMutexTrylockBusy && + !expectedSemaphoreTrywaitAgain && !expectedNetAcceptWouldBlock && !expectedUserServiceNoEvent && !expectedPrivacyInvalidParameter) diff --git a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs index 49e338ac..4f00493d 100644 --- a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs @@ -428,6 +428,22 @@ public static class KernelSemaphoreCompatExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } + [SysAbiExport( + Nid = "GEnUkDZoUwY", + ExportName = "scePthreadSemInit", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadSemInit(CpuContext ctx) + { + // scePthreadSemInit(sem, flag, value, name) seems to only support private semaphores + if (ctx[CpuRegister.Rsi] != 0) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + return PosixSemInit(ctx); + } + [SysAbiExport( Nid = "YCV5dGGBcCo", ExportName = "sem_wait", @@ -446,6 +462,13 @@ public static class KernelSemaphoreCompatExports return KernelWaitSema(ctx); } + [SysAbiExport( + Nid = "C36iRE0F5sE", + ExportName = "scePthreadSemWait", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadSemWait(CpuContext ctx) => PosixSemWait(ctx); + [SysAbiExport( Nid = "WBWzsRifCEA", ExportName = "sem_trywait", @@ -463,6 +486,19 @@ public static class KernelSemaphoreCompatExports return KernelPollSema(ctx, handle, 1); } + [SysAbiExport( + Nid = "H2a+IN9TP0E", + ExportName = "scePthreadSemTrywait", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadSemTryWait(CpuContext ctx) + { + var result = PosixSemTryWait(ctx); + return result == (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY + ? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN) + : result; + } + [SysAbiExport( Nid = "w5IHyvahg-o", ExportName = "sem_timedwait", @@ -499,6 +535,13 @@ public static class KernelSemaphoreCompatExports return KernelSignalSema(ctx, handle, 1); } + [SysAbiExport( + Nid = "aishVAiFaYM", + ExportName = "scePthreadSemPost", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadSemPost(CpuContext ctx) => PosixSemPost(ctx); + [SysAbiExport( Nid = "Bq+LRV-N6Hk", ExportName = "sem_getvalue", @@ -549,6 +592,13 @@ public static class KernelSemaphoreCompatExports return result; } + [SysAbiExport( + Nid = "Vwc+L05e6oE", + ExportName = "scePthreadSemDestroy", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadSemDestroy(CpuContext ctx) => PosixSemDestroy(ctx); + private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle) { handle = 0; diff --git a/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs b/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs new file mode 100644 index 00000000..5dfab21c --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs @@ -0,0 +1,144 @@ +// 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.Pthread; + +public sealed class PthreadSemaphoreSemanticsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const ulong SemaphoreAddress = MemoryBase + 0x100; + private const ulong ValueAddress = MemoryBase + 0x200; + + [Theory] + [InlineData("C36iRE0F5sE", "scePthreadSemWait")] + [InlineData("aishVAiFaYM", "scePthreadSemPost")] + [InlineData("H2a+IN9TP0E", "scePthreadSemTrywait")] + [InlineData("GEnUkDZoUwY", "scePthreadSemInit")] + [InlineData("Vwc+L05e6oE", "scePthreadSemDestroy")] + public void RegistryResolvesPthreadSemaphoreExports(string nid, string exportName) + { + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports( + Generation.Gen4 | Generation.Gen5)); + + Assert.True(manager.TryGetExport(nid, out var export)); + Assert.Equal(exportName, export.Name); + Assert.Equal("libKernel", export.LibraryName); + } + + [Fact] + public void PthreadSemPostAndWaitShareSemaphoreState() + { + var context = CreateContext(); + InitializeSemaphore(context, initialCount: 0); + + context[CpuRegister.Rdi] = SemaphoreAddress; + Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemPost(context)); + Assert.Equal(0UL, context[CpuRegister.Rax]); + Assert.Equal(1, ReadSemaphoreValue(context)); + + context[CpuRegister.Rdi] = SemaphoreAddress; + Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemWait(context)); + Assert.Equal(0UL, context[CpuRegister.Rax]); + Assert.Equal(0, ReadSemaphoreValue(context)); + + DestroySemaphore(context); + } + + [Fact] + public void PthreadSemTryWaitConsumesAvailableTokenAndReturnsTryAgainWhenEmpty() + { + var context = CreateContext(); + InitializeSemaphore(context, initialCount: 1); + + context[CpuRegister.Rdi] = SemaphoreAddress; + Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemTryWait(context)); + Assert.Equal(0, ReadSemaphoreValue(context)); + + context[CpuRegister.Rdi] = SemaphoreAddress; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN, + KernelSemaphoreCompatExports.PthreadSemTryWait(context)); + Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN), + context[CpuRegister.Rax]); + + DestroySemaphore(context); + } + + [Fact] + public void PthreadSemInitRejectsNonPrivateFlag() + { + var context = CreateContext(); + context[CpuRegister.Rdi] = SemaphoreAddress; + context[CpuRegister.Rsi] = 1; + context[CpuRegister.Rdx] = 0; + context[CpuRegister.Rcx] = 0; + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, + KernelSemaphoreCompatExports.PthreadSemInit(context)); + Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT), + context[CpuRegister.Rax]); + } + + [Fact] + public void PthreadSemDestroyClearsSemaphoreAndRejectsSecondDestroy() + { + var context = CreateContext(); + InitializeSemaphore(context, initialCount: 0); + + context[CpuRegister.Rdi] = SemaphoreAddress; + Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemDestroy(context)); + Assert.True(context.TryReadUInt32(SemaphoreAddress, out var handle)); + Assert.Equal(0U, handle); + + context[CpuRegister.Rdi] = SemaphoreAddress; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, + KernelSemaphoreCompatExports.PthreadSemDestroy(context)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void PthreadSemOperationRejectsInvalidSemaphore(bool post) + { + var context = CreateContext(); + context[CpuRegister.Rdi] = SemaphoreAddress; + + var result = post + ? KernelSemaphoreCompatExports.PthreadSemPost(context) + : KernelSemaphoreCompatExports.PthreadSemWait(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result); + Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT), + context[CpuRegister.Rax]); + } + + private static CpuContext CreateContext() => new(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5); + + private static void InitializeSemaphore(CpuContext context, uint initialCount) + { + context[CpuRegister.Rdi] = SemaphoreAddress; + context[CpuRegister.Rsi] = 0; + context[CpuRegister.Rdx] = initialCount; + context[CpuRegister.Rcx] = 0; + Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemInit(context)); + } + + private static int ReadSemaphoreValue(CpuContext context) + { + context[CpuRegister.Rdi] = SemaphoreAddress; + context[CpuRegister.Rsi] = ValueAddress; + Assert.Equal(0, KernelSemaphoreCompatExports.PosixSemGetValue(context)); + Assert.True(context.TryReadUInt32(ValueAddress, out var value)); + return unchecked((int)value); + } + + private static void DestroySemaphore(CpuContext context) + { + context[CpuRegister.Rdi] = SemaphoreAddress; + Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemDestroy(context)); + } +}