[AGC/Vulkan] Extend PS5 runtime and rendering compatibility (#216)

* [Core] Add POSIX native execution and PS5 SELF support

Extend the native backend, guest TLS, fixed-address memory, and loader paths needed by PS5 titles on Windows, Linux, and macOS. Keep workstation GC so high-core-count hosts do not reserve over fixed guest image bases.

* [HLE] Expand PS5 service and media compatibility

Add the kernel, threading, save-data, networking, audio, video-codec, font, dialog, and service exports required by newer PS5 software. Preserve every SysAbi NID currently registered by main while adding the compatibility surface used by ASTRO BOT.

* [AGC/Vulkan] Extend Gen5 shader and presentation support

Expand PM4 handling, Gen5 shader translation, MRT and packed export support, guest image tracking, depth initialization, texture aliasing, and Vulkan presentation. Add the performance overlay and address-filtered diagnostics used to validate ASTRO BOT with original shaders.

* [Core] Align static TLS reservation across hosts

* [Pad] Align primary user ID with UserService

* [Gpu] Preserve runtime scalar buffers across renderer seam

* [AGC] Restore omitted command helper exports

* [Vulkan] Reuse primary views for promoted MRT targets

* [Vulkan] Preserve scratch storage bindings in compute dispatches
This commit is contained in:
Miguel Cruz
2026-07-15 19:02:34 -04:00
committed by GitHub
parent ad5a7d3799
commit 864cbb0fa0
85 changed files with 36299 additions and 9176 deletions
@@ -1,8 +1,8 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
@@ -34,45 +34,6 @@ public static class KernelEventFlagCompatExports
public object Gate { get; } = new();
}
private sealed class EventFlagWaiter : IGuestThreadBlockWaiter
{
public required CpuContext Ctx { get; init; }
public required EventFlagState State { get; init; }
public required ulong Pattern { get; init; }
public required uint WaitMode { get; init; }
public required ulong ResultAddress { get; init; }
public bool Timed { get; init; }
// Timed-wait completion state; unused when Timed is false.
public ulong TimeoutAddress { get; init; }
public long DeadlineTimestamp { get; init; }
public OrbisGen2Result? Result { get; set; }
// Untimed waits stash the prepared result here at wake and return it at resume.
private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK;
public int Resume() => Timed
? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp)
: (int)_blockedResult;
public bool TryWake()
{
if (Timed)
{
return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress);
}
if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult))
{
return false;
}
_blockedResult = preparedResult;
return true;
}
}
[SysAbiExport(
Nid = "BpFoboUJoZU",
ExportName = "sceKernelCreateEventFlag",
@@ -91,17 +52,17 @@ public static class KernelEventFlagCompatExports
optionAddress != 0 ||
!IsValidAttributes(attributes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
@@ -115,11 +76,11 @@ public static class KernelEventFlagCompatExports
if (!ctx.TryWriteUInt64(outAddress, handle))
{
_eventFlags.TryRemove(handle, out _);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -132,7 +93,7 @@ public static class KernelEventFlagCompatExports
var handle = ctx[CpuRegister.Rdi];
if (!_eventFlags.TryRemove(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -141,7 +102,7 @@ public static class KernelEventFlagCompatExports
}
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -156,7 +117,7 @@ public static class KernelEventFlagCompatExports
var returnRip = GetCurrentReturnRip();
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -167,7 +128,7 @@ public static class KernelEventFlagCompatExports
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -181,7 +142,7 @@ public static class KernelEventFlagCompatExports
var pattern = ctx[CpuRegister.Rsi];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -190,7 +151,7 @@ public static class KernelEventFlagCompatExports
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -207,29 +168,29 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (state.Gate)
{
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
ApplyClearMode(state, pattern, waitMode);
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -249,18 +210,18 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Monitor.Enter(state.Gate);
@@ -268,65 +229,64 @@ public static class KernelEventFlagCompatExports
{
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{
return ctx.SetReturn(immediateWaitResult);
return SetReturn(ctx, immediateWaitResult);
}
if (timeoutAddress != 0)
{
if (timeoutUsec == 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
TimeSpan.FromTicks((long)timeoutUsec * 10L));
var timedWaiter = new EventFlagWaiter
{
Ctx = ctx,
State = state,
Pattern = pattern,
WaitMode = waitMode,
ResultAddress = resultAddress,
Timed = true,
TimeoutAddress = timeoutAddress,
DeadlineTimestamp = deadline,
};
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
timedWaiter,
blockDeadlineTimestamp: deadline))
{
state.WaitingThreads++;
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
// Timed waits block on a deadline instead of returning TIMED_OUT
// immediately; a zero-microsecond timeout still degrades to an
// instant poll because the deadline is already in the past.
var deadline = timeoutAddress != 0
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
var hostDeadlineMs = timeoutAddress != 0
? Environment.TickCount64 + (timeoutUsec == 0
? 0L
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
: long.MaxValue;
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var managedThread = Environment.CurrentManagedThreadId;
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
var satisfied = false;
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
new EventFlagWaiter
() =>
{
Ctx = ctx,
State = state,
Pattern = pattern,
WaitMode = waitMode,
ResultAddress = resultAddress,
});
if (satisfied)
{
return (int)blockedWaitResult;
}
// Deadline expiry: report timeout with the current bits.
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
},
() =>
{
if (!TryPrepareBlockedWait(
ctx,
state,
pattern,
waitMode,
resultAddress,
out var preparedResult))
{
return false;
}
blockedWaitResult = preparedResult;
satisfied = true;
return true;
},
deadline);
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock)
@@ -334,7 +294,7 @@ public static class KernelEventFlagCompatExports
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
state.WaitingThreads++;
@@ -359,10 +319,21 @@ public static class KernelEventFlagCompatExports
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return ctx.SetReturn(pumpedWaitResult);
return SetReturn(ctx, pumpedWaitResult);
}
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
var remaining = hostDeadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
Monitor.Wait(state.Gate, (int)Math.Min(remaining, HostWaitPumpMilliseconds));
}
}
finally
@@ -376,7 +347,7 @@ public static class KernelEventFlagCompatExports
state.WaitingThreads++;
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
finally
{
@@ -396,24 +367,26 @@ public static class KernelEventFlagCompatExports
var waiterCountAddress = ctx[CpuRegister.Rdx];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
if (waiterCountAddress != 0 &&
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
state.Bits = setPattern;
state.WaitingThreads = 0;
Monitor.PulseAll(state.Gate);
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
TraceEventFlag(
$"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " +
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():X16}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static bool IsValidAttributes(uint attributes)
@@ -509,98 +482,90 @@ public static class KernelEventFlagCompatExports
}
}
private static bool TryCompleteBlockedTimedWait(
CpuContext ctx,
EventFlagState state,
EventFlagWaiter waiter,
ulong pattern,
uint waitMode,
ulong resultAddress)
{
lock (state.Gate)
{
if (waiter.Result is not null)
{
return true;
}
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_OK
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
ApplyClearMode(state, pattern, waitMode);
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
return true;
}
}
private static int CompleteBlockedTimedWait(
CpuContext ctx,
EventFlagState state,
EventFlagWaiter waiter,
ulong pattern,
uint waitMode,
ulong resultAddress,
ulong timeoutAddress,
long deadlineTimestamp)
{
lock (state.Gate)
{
if (waiter.Result is null)
{
if (IsSatisfied(state.Bits, pattern, waitMode))
{
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_OK
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
ApplyClearMode(state, pattern, waitMode);
}
}
else
{
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
}
}
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(
uint.MaxValue,
remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
}
return (int)waiter.Result.Value;
}
private static string GetEventFlagWakeKey(ulong handle) =>
$"event_flag:0x{handle:X16}";
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits);
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
{
var bytes = new byte[capacity];
Span<byte> current = stackalloc byte[1];
for (var index = 0; index < bytes.Length; index++)
{
if (!ctx.Memory.TryRead(address + (ulong)index, current))
{
value = string.Empty;
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
bytes[index] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static void TraceEventFlag(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
@@ -684,7 +649,7 @@ public static class KernelEventFlagCompatExports
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadByte(address, out var value))
if (TryReadByte(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X2}");
}
@@ -692,7 +657,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt32(address, out var value))
if (TryReadUInt32(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X8}");
}
@@ -700,7 +665,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt64(address, out var value))
if (TryReadUInt64(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X16}");
}
@@ -395,13 +395,13 @@ public static class KernelEventQueueCompatExports
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -450,7 +450,7 @@ public static class KernelEventQueueCompatExports
}
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -649,6 +649,60 @@ public static class KernelEventQueueCompatExports
return triggeredCount;
}
/// <summary>
/// Queues one event for every registration using <paramref name="filter"/>.
/// Unlike <see cref="TriggerRegisteredEvents"/>, this preserves distinct
/// event identifiers registered on the same queue. AGC driver completion
/// queues use this form because the driver, rather than a packet-provided
/// identifier, announces that the whole submission reached end-of-pipe.
/// </summary>
public static int TriggerRegisteredEventsDistinct(short filter)
{
HashSet<ulong>? 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,
registration.Ident,
registration.UserData));
(wakeHandles ??= []).Add(handle);
triggeredCount++;
}
}
}
if (wakeHandles is not null)
{
foreach (var handle in wakeHandles)
{
WakeEventQueue(handle);
}
}
return triggeredCount;
}
private static bool TriggerRegisteredEvent(
ulong handle,
ulong ident,
@@ -752,7 +806,7 @@ public static class KernelEventQueueCompatExports
ulong outCountAddress)
{
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -865,8 +919,29 @@ public static class KernelEventQueueCompatExports
return;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
}
@@ -62,4 +62,75 @@ public static class KernelExceptionCompatExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "il03nluKfMk",
ExportName = "sceKernelRaiseException",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int RaiseException(CpuContext ctx)
{
var targetThread = ctx[CpuRegister.Rdi];
var exceptionType = unchecked((int)ctx[CpuRegister.Rsi]);
if (targetThread == 0 || exceptionType is < 0 or >= 128)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ulong handler;
lock (_gate)
{
if (!_installedHandlers.TryGetValue(exceptionType, out handler))
{
// The kernel accepts a raise for a type with no process
// handler; there is simply no user callback to deliver.
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_IGNORE_GUEST_EXCEPTIONS"),
"1",
StringComparison.Ordinal))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
var scheduler = GuestThreadExecution.Scheduler;
string? error = null;
if (scheduler is null ||
!scheduler.TryRaiseGuestException(
ctx,
targetThread,
handler,
exceptionType,
out error))
{
Console.Error.WriteLine(
$"[LOADER][WARN] sceKernelRaiseException delivery failed: " +
$"target=0x{targetThread:X16} type=0x{exceptionType:X2} " +
$"error={error ?? "scheduler unavailable"}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_EXCEPTIONS"),
"1",
StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] guest_exception.raise " +
$"target=0x{targetThread:X16} type=0x{exceptionType:X2} " +
$"handler=0x{handler:X16}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
}
+55 -56
View File
@@ -13,8 +13,6 @@ public static class KernelExports
private static readonly object _coredumpGate = new();
private static ulong _coredumpHandler;
private static ulong _coredumpHandlerContext;
private const uint Gen4CompiledSdkVersion = 0x05000000;
private const uint Gen5CompiledSdkVersion = 0x09000000;
private readonly record struct CxaDestructorEntry(
ulong Function,
@@ -28,24 +26,7 @@ public static class KernelExports
LibraryName = "libKernel")]
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
{
var versionAddress = ctx[CpuRegister.Rdi];
if (versionAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var sdkVersion = ctx.TargetGeneration == Generation.Gen5
? Gen5CompiledSdkVersion
: Gen4CompiledSdkVersion;
if (!ctx.TryWriteUInt32(versionAddress, sdkVersion))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -71,20 +52,12 @@ public static class KernelExports
ExportName = "exit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
[SysAbiExport(
Nid = "L1SBTkC+Cvw",
ExportName = "abort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Abort(CpuContext ctx)
public static int Exit(CpuContext ctx)
{
// Route through the same graceful guest-entry-exit path as exit(): letting the call
// fall through to the host's native abort() does not unwind the guest thread cleanly.
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -199,7 +172,11 @@ public static class KernelExports
ExportName = "_exit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
public static int UnderscoreExit(CpuContext ctx)
{
_ = ctx;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ac86z8q7T8A",
@@ -218,12 +195,14 @@ public static class KernelExports
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCreate(CpuContext ctx)
=> PthreadCreateCore(ctx, ctx[CpuRegister.R8]);
private static int PthreadCreateCore(CpuContext ctx, ulong nameAddress)
{
var threadIdAddress = ctx[CpuRegister.Rdi];
var attrAddress = ctx[CpuRegister.Rsi];
var entryAddress = ctx[CpuRegister.Rdx];
var argument = ctx[CpuRegister.Rcx];
var nameAddress = ctx[CpuRegister.R8];
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
@@ -278,14 +257,20 @@ public static class KernelExports
ExportName = "pthread_create",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
public static int PosixPthreadCreate(CpuContext ctx)
{
return PthreadCreateCore(ctx, nameAddress: 0);
}
[SysAbiExport(
Nid = "Jmi+9w9u0E4",
ExportName = "pthread_create_name_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
public static int PosixPthreadCreateNameNp(CpuContext ctx)
{
return PthreadCreateCore(ctx, ctx[CpuRegister.R8]);
}
[SysAbiExport(
Nid = "3kg7rT0NQIs",
@@ -295,6 +280,9 @@ public static class KernelExports
public static int PthreadExit(CpuContext ctx)
{
var value = ctx[CpuRegister.Rdi];
// Run cleanup on the still-executable thread before unwinding it.
KernelPthreadExtendedCompatExports.RunThreadLocalDestructors(ctx);
KernelMemoryCompatExports.RunThreadDtors(ctx);
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -308,6 +296,8 @@ public static class KernelExports
public static int PosixPthreadExit(CpuContext ctx)
{
var value = ctx[CpuRegister.Rdi];
KernelPthreadExtendedCompatExports.RunThreadLocalDestructors(ctx);
KernelMemoryCompatExports.RunThreadDtors(ctx);
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -362,7 +352,10 @@ public static class KernelExports
ExportName = "pthread_join",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
public static int PosixPthreadJoin(CpuContext ctx)
{
return PthreadJoin(ctx);
}
[SysAbiExport(
Nid = "wuCroIGjt2g",
@@ -437,14 +430,21 @@ public static class KernelExports
private static string ReadCString(CpuContext ctx, ulong address, int maxLen)
{
Span<byte> buf = stackalloc byte[maxLen];
if (!ctx.Memory.TryRead(address, buf))
return $"<unreadable 0x{address:X16}>";
Span<byte> one = stackalloc byte[1];
var len = 0;
while (len < buf.Length)
{
if (!ctx.Memory.TryRead(address + (ulong)len, one))
return len == 0 ? $"<unreadable 0x{address:X16}>" : System.Text.Encoding.UTF8.GetString(buf[..len]);
int len = 0;
while (len < buf.Length && buf[len] != 0) len++;
if (one[0] == 0)
break;
try { return System.Text.Encoding.UTF8.GetString(buf.Slice(0, len)); }
catch { return System.Text.Encoding.ASCII.GetString(buf.Slice(0, len)); }
buf[len++] = one[0];
}
try { return System.Text.Encoding.UTF8.GetString(buf[..len]); }
catch { return System.Text.Encoding.ASCII.GetString(buf[..len]); }
}
private static bool ShouldTracePthread()
@@ -452,19 +452,18 @@ public static class KernelExports
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
}
private static int RequestProcessExit(CpuContext ctx, string syscallName)
[SysAbiExport(
Nid = "L1SBTkC+Cvw",
ExportName = "abort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Abort(CpuContext ctx)
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
// Route through the same graceful guest-entry-exit path as exit(): letting the call
// fall through to the host's native abort() does not unwind the guest thread cleanly.
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
// shadowing them breaks every C++ static-init guard in the game.
}
@@ -0,0 +1,662 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Kernel;
/// <summary>
/// Positional and scatter/gather file I/O, synchronization, renaming, fcntl,
/// polling and asynchronous I/O (AIO) that complement the base file exports.
/// These share the same guest FD table and path-resolution helpers via the
/// partial <see cref="KernelMemoryCompatExports"/> class.
/// </summary>
public static partial class KernelMemoryCompatExports
{
// fcntl commands (FreeBSD numbering used by the PS4/PS5 libc).
private const int F_DUPFD = 0;
private const int F_GETFD = 1;
private const int F_SETFD = 2;
private const int F_GETFL = 3;
private const int F_SETFL = 4;
// poll event bits.
private const short POLLIN = 0x0001;
private const short POLLOUT = 0x0004;
// AIO request state (SCE_KERNEL_AIO_STATE_*).
private const uint AioStateCompleted = 3;
private const int SizeofAioRwRequest = 40;
private static long _nextAioSubmitId = 1;
private static readonly ConcurrentDictionary<uint, int> _aioResults = new();
private static FileStream? GetOpenFile(int fd)
{
lock (_fdGate)
{
return _openFiles.TryGetValue(fd, out var stream) ? stream : null;
}
}
// ---- Positional read/write (do not move the file offset) ----
[SysAbiExport(Nid = "ezv-RSBNKqI", ExportName = "pread",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixPread(CpuContext ctx) => KernelPreadCore(ctx);
[SysAbiExport(Nid = "+r3rMFwItV4", ExportName = "sceKernelPread",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelPread(CpuContext ctx) => KernelPreadCore(ctx);
private static int KernelPreadCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var bufferAddress = ctx[CpuRegister.Rsi];
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
var offset = unchecked((long)ctx[CpuRegister.Rcx]);
if (requested < 0 || (requested > 0 && bufferAddress == 0) || offset < 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var stream = GetOpenFile(fd);
if (stream is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (requested == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var buffer = GC.AllocateUninitializedArray<byte>(requested);
int read;
try
{
read = RandomAccess.Read(stream.SafeFileHandle, buffer, offset);
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (read > 0 && !ctx.Memory.TryWrite(bufferAddress, buffer.AsSpan(0, read)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((ulong)read);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "C2kJ-byS5rM", ExportName = "pwrite",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixPwrite(CpuContext ctx) => KernelPwriteCore(ctx);
[SysAbiExport(Nid = "nKWi-N2HBV4", ExportName = "sceKernelPwrite",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelPwrite(CpuContext ctx) => KernelPwriteCore(ctx);
private static int KernelPwriteCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var bufferAddress = ctx[CpuRegister.Rsi];
var requested = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
var offset = unchecked((long)ctx[CpuRegister.Rcx]);
if (requested < 0 || (requested > 0 && bufferAddress == 0) || offset < 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var stream = GetOpenFile(fd);
if (stream is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (requested == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var payload = GC.AllocateUninitializedArray<byte>(requested);
if (!ctx.Memory.TryRead(bufferAddress, payload))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
try
{
RandomAccess.Write(stream.SafeFileHandle, payload, offset);
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Synchronization ----
[SysAbiExport(Nid = "juWbTNM+8hw", ExportName = "fsync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFsync(CpuContext ctx) => KernelFsyncCore(ctx);
[SysAbiExport(Nid = "fTx66l5iWIA", ExportName = "sceKernelFsync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelFsync(CpuContext ctx) => KernelFsyncCore(ctx);
[SysAbiExport(Nid = "KIbJFQ0I1Cg", ExportName = "fdatasync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFdatasync(CpuContext ctx) => KernelFsyncCore(ctx);
[SysAbiExport(Nid = "30Rh4ixbKy4", ExportName = "sceKernelFdatasync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelFdatasync(CpuContext ctx) => KernelFsyncCore(ctx);
private static int KernelFsyncCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var stream = GetOpenFile(fd);
if (stream is null)
{
if (fd is 0 or 1 or 2)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
try
{
stream.Flush(flushToDisk: true);
}
catch (IOException)
{
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "Y2OqwJQ3lr8", ExportName = "sync",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixSync(CpuContext ctx)
{
lock (_fdGate)
{
foreach (var stream in _openFiles.Values)
{
try { stream.Flush(flushToDisk: true); } catch (IOException) { }
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Truncation ----
[SysAbiExport(Nid = "ih4CD9-gghM", ExportName = "ftruncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx);
[SysAbiExport(Nid = "VW3TVZiM4-E", ExportName = "sceKernelFtruncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelFtruncate(CpuContext ctx) => KernelFtruncateCore(ctx);
private static int KernelFtruncateCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var length = unchecked((long)ctx[CpuRegister.Rsi]);
if (length < 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var stream = GetOpenFile(fd);
if (stream is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
try
{
stream.SetLength(length);
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "ayrtszI7GBg", ExportName = "truncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixTruncate(CpuContext ctx) => KernelTruncateCore(ctx);
[SysAbiExport(Nid = "WlyEA-sLDf0", ExportName = "sceKernelTruncate",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelTruncate(CpuContext ctx) => KernelTruncateCore(ctx);
private static int KernelTruncateCore(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var length = unchecked((long)ctx[CpuRegister.Rsi]);
if (length < 0 || !TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (IsReadOnlyGuestMutationPath(guestPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
var hostPath = ResolveGuestPath(guestPath);
try
{
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
stream.SetLength(length);
}
catch (FileNotFoundException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Rename ----
[SysAbiExport(Nid = "NN01qLRhiqU", ExportName = "rename",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixRename(CpuContext ctx) => KernelRenameCore(ctx);
[SysAbiExport(Nid = "52NcYU9+lEo", ExportName = "sceKernelRename",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelRename(CpuContext ctx) => KernelRenameCore(ctx);
private static int KernelRenameCore(CpuContext ctx)
{
if (!TryReadNullTerminatedUtf8(ctx, ctx[CpuRegister.Rdi], MaxGuestStringLength, out var fromGuest) ||
!TryReadNullTerminatedUtf8(ctx, ctx[CpuRegister.Rsi], MaxGuestStringLength, out var toGuest))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (IsReadOnlyGuestMutationPath(fromGuest) || IsReadOnlyGuestMutationPath(toGuest))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
var fromHost = ResolveGuestPath(fromGuest);
var toHost = ResolveGuestPath(toGuest);
try
{
if (Directory.Exists(fromHost))
{
Directory.Move(fromHost, toHost);
}
else
{
File.Move(fromHost, toHost, overwrite: true);
}
}
catch (FileNotFoundException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
catch (DirectoryNotFoundException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
InvalidateNegativeStatCacheForPathAndAncestors(toGuest);
AddNegativeStatCacheForGuestPath(fromGuest);
InvalidateAprFileSizeCache(fromHost);
InvalidateAprFileSizeCache(toHost);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Descriptor duplication ----
[SysAbiExport(Nid = "iiQjzvfWDq0", ExportName = "dup",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixDup(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_fdGate)
{
if (!_openFiles.TryGetValue(fd, out var stream))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
// POSIX dup shares the open file description (and offset), which is
// exactly the shared FileStream reference.
var newFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
_openFiles[newFd] = stream;
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "wdUufa9g-D8", ExportName = "dup2",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixDup2(CpuContext ctx)
{
var oldFd = unchecked((int)ctx[CpuRegister.Rdi]);
var newFd = unchecked((int)ctx[CpuRegister.Rsi]);
lock (_fdGate)
{
if (!_openFiles.TryGetValue(oldFd, out var stream))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (oldFd == newFd)
{
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// If newFd names an open file, dup2 closes it first.
if (_openFiles.TryGetValue(newFd, out var existing) && !ReferenceEquals(existing, stream))
{
try { existing.Dispose(); } catch (IOException) { }
}
_openFiles[newFd] = stream;
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- fcntl ----
[SysAbiExport(Nid = "8nY19bKoiZk", ExportName = "fcntl",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixFcntl(CpuContext ctx) => KernelFcntlCore(ctx);
[SysAbiExport(Nid = "SoZkxZkCHaw", ExportName = "sceKernelFcntl",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelFcntl(CpuContext ctx) => KernelFcntlCore(ctx);
private static int KernelFcntlCore(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var command = unchecked((int)ctx[CpuRegister.Rsi]);
var argument = unchecked((int)ctx[CpuRegister.Rdx]);
switch (command)
{
case F_DUPFD:
lock (_fdGate)
{
if (!_openFiles.TryGetValue(fd, out var stream))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var newFd = Math.Max((int)Interlocked.Increment(ref _nextFileDescriptor), argument);
_openFiles[newFd] = stream;
ctx[CpuRegister.Rax] = unchecked((ulong)newFd);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
case F_GETFD:
case F_GETFL:
// No close-on-exec / status flags are tracked; report cleared.
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
case F_SETFD:
case F_SETFL:
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
default:
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
// ---- poll / select ----
// Regular files are always ready for both read and write, so report every
// requested descriptor as immediately ready.
[SysAbiExport(Nid = "ku7D4q1Y9PI", ExportName = "poll",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixPoll(CpuContext ctx)
{
var fdsAddress = ctx[CpuRegister.Rdi];
var count = unchecked((uint)ctx[CpuRegister.Rsi]);
if (fdsAddress == 0 || count == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// struct pollfd { int fd; short events; short revents; } == 8 bytes.
var ready = 0;
Span<byte> buffer = stackalloc byte[8];
for (uint i = 0; i < count && i < 4096; i++)
{
var entry = fdsAddress + i * 8;
if (!ctx.Memory.TryRead(entry, buffer))
{
break;
}
var events = BinaryPrimitives.ReadInt16LittleEndian(buffer[4..]);
var revents = (short)(events & (POLLIN | POLLOUT));
BinaryPrimitives.WriteInt16LittleEndian(buffer[6..], revents);
if (revents != 0)
{
ready++;
}
_ = ctx.Memory.TryWrite(entry, buffer);
}
ctx[CpuRegister.Rax] = unchecked((ulong)ready);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "T8fER+tIGgk", ExportName = "select",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int PosixSelect(CpuContext ctx)
{
// nfds in Rdi; the fd_sets are left as-is (all reported ready) and the
// ready count returned is nfds so callers proceed without blocking.
var nfds = unchecked((int)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = unchecked((ulong)Math.Max(nfds, 0));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ---- Asynchronous I/O (executed synchronously) ----
[SysAbiExport(Nid = "HgX7+AORI58", ExportName = "sceKernelAioSubmitReadCommands",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioSubmitReadCommands(CpuContext ctx) => KernelAioSubmit(ctx, write: false);
[SysAbiExport(Nid = "lXT0m3P-vs4", ExportName = "sceKernelAioSubmitReadCommandsMultiple",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioSubmitReadCommandsMultiple(CpuContext ctx) => KernelAioSubmit(ctx, write: false);
[SysAbiExport(Nid = "XQ8C8y+de+E", ExportName = "sceKernelAioSubmitWriteCommands",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioSubmitWriteCommands(CpuContext ctx) => KernelAioSubmit(ctx, write: true);
private static int KernelAioSubmit(CpuContext ctx, bool write)
{
var requestsAddress = ctx[CpuRegister.Rdi];
var count = unchecked((int)ctx[CpuRegister.Rsi]);
var outIdAddress = ctx[CpuRegister.Rcx];
if (requestsAddress == 0 || count <= 0 || count > 0x10000)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
// Each SceKernelAioRWRequest: offset(8) nbyte(8) buf(8) result(8) fd(4).
Span<byte> request = stackalloc byte[SizeofAioRwRequest];
Span<byte> result = stackalloc byte[16];
for (var i = 0; i < count; i++)
{
var entry = requestsAddress + (ulong)(i * SizeofAioRwRequest);
if (!ctx.Memory.TryRead(entry, request))
{
break;
}
var offset = BinaryPrimitives.ReadInt64LittleEndian(request[0..]);
var nbyte = (int)Math.Min(BinaryPrimitives.ReadUInt64LittleEndian(request[8..]), int.MaxValue);
var buf = BinaryPrimitives.ReadUInt64LittleEndian(request[16..]);
var resultPtr = BinaryPrimitives.ReadUInt64LittleEndian(request[24..]);
var fd = BinaryPrimitives.ReadInt32LittleEndian(request[32..]);
long transferred = KernelAioTransfer(ctx, fd, offset, nbyte, buf, write);
if (resultPtr != 0)
{
BinaryPrimitives.WriteInt64LittleEndian(result[0..], transferred);
BinaryPrimitives.WriteUInt32LittleEndian(result[8..], AioStateCompleted);
_ = ctx.Memory.TryWrite(resultPtr, result);
}
}
var submitId = unchecked((uint)Interlocked.Increment(ref _nextAioSubmitId));
_aioResults[submitId] = 0;
if (outIdAddress != 0)
{
Span<byte> idBuffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(idBuffer, submitId);
_ = ctx.Memory.TryWrite(outIdAddress, idBuffer);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static long KernelAioTransfer(CpuContext ctx, int fd, long offset, int nbyte, ulong buf, bool write)
{
if (nbyte <= 0 || buf == 0 || offset < 0)
{
return 0;
}
var stream = GetOpenFile(fd);
if (stream is null)
{
return -1;
}
var scratch = GC.AllocateUninitializedArray<byte>(nbyte);
try
{
if (write)
{
if (!ctx.Memory.TryRead(buf, scratch))
{
return -1;
}
RandomAccess.Write(stream.SafeFileHandle, scratch, offset);
return nbyte;
}
var read = RandomAccess.Read(stream.SafeFileHandle, scratch, offset);
if (read > 0 && !ctx.Memory.TryWrite(buf, scratch.AsSpan(0, read)))
{
return -1;
}
return read;
}
catch (IOException)
{
return -1;
}
}
[SysAbiExport(Nid = "o7O4z3jwKzo", ExportName = "sceKernelAioPollRequests",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioPollRequests(CpuContext ctx) => KernelAioComplete(ctx);
[SysAbiExport(Nid = "lgK+oIWkJyA", ExportName = "sceKernelAioWaitRequests",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioWaitRequests(CpuContext ctx) => KernelAioComplete(ctx);
private static int KernelAioComplete(CpuContext ctx)
{
// Submission already performed the I/O synchronously, so every request
// reports completed. Rsi points at the state-out array, Rdx = count.
var statesAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (statesAddress != 0 && count > 0 && count <= 0x10000)
{
Span<byte> state = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(state, AioStateCompleted);
for (var i = 0; i < count; i++)
{
_ = ctx.Memory.TryWrite(statesAddress + (ulong)(i * sizeof(uint)), state);
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "fR521KIGgb8", ExportName = "sceKernelAioCancelRequest",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioCancelRequest(CpuContext ctx)
{
// Already completed; report the completed state if an out pointer given.
var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress != 0)
{
Span<byte> state = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(state, AioStateCompleted);
_ = ctx.Memory.TryWrite(stateAddress, state);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(Nid = "5TgME6AYty4", ExportName = "sceKernelAioDeleteRequest",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libKernel")]
public static int KernelAioDeleteRequest(CpuContext ctx)
{
var submitId = unchecked((uint)ctx[CpuRegister.Rdi]);
_aioResults.TryRemove(submitId, out _);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
File diff suppressed because it is too large Load Diff
@@ -9,8 +9,16 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelModuleRegistry
{
public enum ModuleStartState
{
NotStarted,
Starting,
Started,
}
private static readonly object _gate = new();
private static readonly Dictionary<int, ModuleEntry> _modulesByHandle = new();
private static readonly Dictionary<int, Dictionary<string, ulong>> _symbolsByHandle = new();
private static readonly Dictionary<string, int> _handleByPath = new(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<string, int> _handleByName = new(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary<int, int> _sysmoduleHandleById = new();
@@ -32,6 +40,11 @@ public static class KernelModuleRegistry
ulong BaseAddress,
ulong EndAddress,
ulong EntryPoint,
ulong InitEntryPoint,
ulong EhFrameHeaderAddress,
ulong EhFrameAddress,
ulong EhFrameSize,
ModuleStartState StartState,
bool IsMain,
bool IsSystemModule);
@@ -40,6 +53,7 @@ public static class KernelModuleRegistry
lock (_gate)
{
_modulesByHandle.Clear();
_symbolsByHandle.Clear();
_handleByPath.Clear();
_handleByName.Clear();
_sysmoduleHandleById.Clear();
@@ -52,6 +66,10 @@ public static class KernelModuleRegistry
ulong baseAddress,
ulong size,
ulong entryPoint,
ulong initEntryPoint,
ulong ehFrameHeaderAddress,
ulong ehFrameAddress,
ulong ehFrameSize,
bool isMain,
bool isSystemModule = false)
{
@@ -67,6 +85,10 @@ public static class KernelModuleRegistry
BaseAddress = baseAddress,
EndAddress = ComputeEnd(baseAddress, size),
EntryPoint = entryPoint,
InitEntryPoint = initEntryPoint,
EhFrameHeaderAddress = ehFrameHeaderAddress,
EhFrameAddress = ehFrameAddress,
EhFrameSize = ehFrameSize,
IsMain = existing.IsMain || isMain,
IsSystemModule = existing.IsSystemModule || isSystemModule,
};
@@ -84,6 +106,11 @@ public static class KernelModuleRegistry
BaseAddress: baseAddress,
EndAddress: ComputeEnd(baseAddress, size),
EntryPoint: entryPoint,
InitEntryPoint: initEntryPoint,
EhFrameHeaderAddress: ehFrameHeaderAddress,
EhFrameAddress: ehFrameAddress,
EhFrameSize: ehFrameSize,
StartState: ModuleStartState.NotStarted,
IsMain: isMain,
IsSystemModule: isSystemModule);
_modulesByHandle[handle] = entry;
@@ -119,6 +146,11 @@ public static class KernelModuleRegistry
BaseAddress: 0,
EndAddress: 0,
EntryPoint: 0,
InitEntryPoint: 0,
EhFrameHeaderAddress: 0,
EhFrameAddress: 0,
EhFrameSize: 0,
StartState: ModuleStartState.Started,
IsMain: false,
IsSystemModule: isSystemModule);
_modulesByHandle[handle] = entry;
@@ -203,6 +235,97 @@ public static class KernelModuleRegistry
}
}
/// <summary>
/// Atomically claims a module initializer. A module can be observed while
/// it is starting (for recursive loader calls), but its DT_INIT routine is
/// executed at most once after a successful start.
/// </summary>
public static bool TryBeginModuleStart(int handle, out ModuleEntry module)
{
lock (_gate)
{
if (!_modulesByHandle.TryGetValue(handle, out module))
{
return false;
}
if (module.StartState != ModuleStartState.NotStarted)
{
return false;
}
if (module.InitEntryPoint < 0x10000)
{
module = module with { StartState = ModuleStartState.Started };
_modulesByHandle[handle] = module;
return false;
}
module = module with { StartState = ModuleStartState.Starting };
_modulesByHandle[handle] = module;
return true;
}
}
public static void CompleteModuleStart(int handle, bool succeeded)
{
lock (_gate)
{
if (!_modulesByHandle.TryGetValue(handle, out var module) ||
module.StartState != ModuleStartState.Starting)
{
return;
}
_modulesByHandle[handle] = module with
{
StartState = succeeded ? ModuleStartState.Started : ModuleStartState.NotStarted,
};
}
}
public static void RegisterModuleSymbols(int handle, IReadOnlyDictionary<string, ulong> symbols)
{
ArgumentNullException.ThrowIfNull(symbols);
lock (_gate)
{
if (!_modulesByHandle.ContainsKey(handle))
{
return;
}
if (!_symbolsByHandle.TryGetValue(handle, out var destination))
{
destination = new Dictionary<string, ulong>(StringComparer.Ordinal);
_symbolsByHandle[handle] = destination;
}
foreach (var (name, address) in symbols)
{
if (!string.IsNullOrWhiteSpace(name) && address >= 0x10000)
{
destination.TryAdd(name, address);
}
}
}
}
public static bool TryResolveModuleSymbol(int handle, string symbolName, out ulong address)
{
address = 0;
if (string.IsNullOrWhiteSpace(symbolName))
{
return false;
}
lock (_gate)
{
return _symbolsByHandle.TryGetValue(handle, out var symbols) &&
symbols.TryGetValue(symbolName, out address) &&
address >= 0x10000;
}
}
public static bool TryFindByPathOrName(string? modulePathOrName, out ModuleEntry module)
{
module = default;
File diff suppressed because it is too large Load Diff
@@ -2,8 +2,10 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -15,6 +17,8 @@ public static class KernelPthreadExtendedCompatExports
private const int DefaultDetachState = 0;
private const ulong DefaultGuardSize = 0x1000UL;
private const ulong DefaultStackSize = 0x1_00000UL;
private const ulong NativeGuestStackSize = 0x20_0000UL;
private const ulong NativeGuestStackStride = 0x100_0000UL;
private const int DefaultInheritSched = 4;
private const int DefaultSchedPolicy = 1;
private const int DefaultSchedPriority = DefaultThreadPriority;
@@ -223,6 +227,13 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "+U1R4WtXvoc",
ExportName = "pthread_detach",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadDetach(CpuContext ctx) => PthreadDetach(ctx);
[SysAbiExport(
Nid = "How7B8Oet6k",
ExportName = "scePthreadGetname",
@@ -273,6 +284,7 @@ public static class KernelPthreadExtendedCompatExports
state.Attributes = state.Attributes with { AffinityMask = mask };
}
_ = GuestThreadExecution.Scheduler?.TrySetGuestThreadAffinity(thread, mask);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -326,7 +338,7 @@ public static class KernelPthreadExtendedCompatExports
priority = GetOrCreateThreadStateLocked(thread).Priority;
}
if (!ctx.TryWriteInt32(outPriorityAddress, priority))
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -354,6 +366,9 @@ public static class KernelPthreadExtendedCompatExports
GetOrCreateThreadStateLocked(thread).Priority = priority;
}
// Apply to the live scheduler thread so runtime priority changes take
// effect, not just the local bookkeeping snapshot.
_ = GuestThreadExecution.Scheduler?.TrySetGuestThreadPriority(thread, priority);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -373,7 +388,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -408,9 +423,9 @@ public static class KernelPthreadExtendedCompatExports
public static int PthreadGetschedparam(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var outPolicyAddress = ctx[CpuRegister.Rsi];
var outSchedParamAddress = ctx[CpuRegister.Rdx];
if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
var policyAddress = ctx[CpuRegister.Rsi];
var schedParamAddress = ctx[CpuRegister.Rdx];
if (thread == 0 || policyAddress == 0 || schedParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
@@ -424,8 +439,8 @@ public static class KernelPthreadExtendedCompatExports
priority = state.Priority;
}
if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
!ctx.TryWriteInt32(outSchedParamAddress, priority))
if (!TryWriteInt32(ctx, policyAddress, policy) ||
!TryWriteInt32(ctx, schedParamAddress, priority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -528,6 +543,22 @@ public static class KernelPthreadExtendedCompatExports
lock (_stateGate)
{
var threadState = GetOrCreateThreadStateLocked(thread);
// The native executor maps guest pthread stacks itself, after the
// kernel-facing thread object has been created. Report that live
// mapping when a thread asks for its own attributes. IL2CPP's
// conservative collector uses these two fields to register the stack;
// returning the default null address lets it recycle objects that are
// still reachable only from guest registers/stack frames.
if (thread == KernelPthreadState.GetCurrentThreadHandle() &&
TryInferNativeGuestStack(ctx[CpuRegister.Rsp], out var stackAddress))
{
threadState.Attributes = threadState.Attributes with
{
StackAddress = stackAddress,
StackSize = NativeGuestStackSize,
};
}
_attrStates[outAttrAddress] = threadState.Attributes;
}
@@ -535,6 +566,28 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryInferNativeGuestStack(ulong stackPointer, out ulong stackAddress)
{
stackAddress = 0;
var candidate = stackPointer & ~(NativeGuestStackStride - 1);
if (stackPointer - candidate >= NativeGuestStackSize)
{
return false;
}
var highestStack = OperatingSystem.IsWindows()
? 0x00007FFF_F000_0000UL
: 0x00006FFF_F000_0000UL;
var lowestStack = highestStack - (63 * NativeGuestStackStride);
if (candidate < lowestStack || candidate > highestStack)
{
return false;
}
stackAddress = candidate;
return true;
}
[SysAbiExport(
Nid = "8+s5BzZjxSg",
ExportName = "scePthreadAttrGetaffinity",
@@ -584,7 +637,7 @@ public static class KernelPthreadExtendedCompatExports
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -827,7 +880,7 @@ public static class KernelPthreadExtendedCompatExports
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -850,7 +903,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1207,7 +1260,7 @@ public static class KernelPthreadExtendedCompatExports
}
}
if (!ctx.TryWriteInt32(outKeyAddress, key))
if (!TryWriteInt32(ctx, outKeyAddress, key))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1315,6 +1368,72 @@ public static class KernelPthreadExtendedCompatExports
LibraryName = "libKernel")]
public static int OrbisPthreadGetspecific(CpuContext ctx) => PosixPthreadGetspecific(ctx);
private const int PthreadDestructorIterations = 4;
/// <summary>
/// Runs the current thread's pthread TLS-key destructors, as POSIX
/// requires on thread exit. Each key holding a non-null value with a
/// registered destructor has its value cleared first and the destructor
/// then invoked with the previous value; this repeats up to
/// PTHREAD_DESTRUCTOR_ITERATIONS times so destructors that set new
/// thread-local values are themselves cleaned up. Called on the exiting
/// guest thread while it is still executable.
/// </summary>
public static void RunThreadLocalDestructors(CpuContext ctx)
{
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return;
}
var threadHandle = KernelPthreadState.GetCurrentThreadHandle();
if (!_threadLocalSpecific.TryGetValue(threadHandle, out var values))
{
return;
}
for (var iteration = 0; iteration < PthreadDestructorIterations; iteration++)
{
var ranAny = false;
foreach (var entry in values)
{
var value = entry.Value;
if (value == 0 ||
!_tlsKeys.TryGetValue(entry.Key, out var keyState) ||
keyState.Destructor == 0)
{
continue;
}
// Clear before invoking, per POSIX, so a destructor that
// re-sets the key is handled on the next iteration.
if (!values.TryUpdate(entry.Key, 0, value))
{
continue;
}
ranAny = true;
_ = scheduler.TryCallGuestFunction(
ctx,
keyState.Destructor,
value,
0,
0,
0,
"pthread_tls_destructor",
out _);
}
if (!ranAny)
{
break;
}
}
_threadLocalSpecific.TryRemove(threadHandle, out _);
}
private static int PthreadRwlockLockCore(CpuContext ctx, ulong rwlockAddress, bool write)
{
if (rwlockAddress == 0)
@@ -1680,4 +1799,24 @@ public static class KernelPthreadExtendedCompatExports
payload[^1] = 0;
return ctx.Memory.TryWrite(address, payload);
}
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
@@ -13,9 +14,6 @@ public static class KernelSemaphoreCompatExports
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
private static int _nextSemaphoreHandle = 1;
[ThreadStatic]
private static int _semaPollBackoffCount;
private sealed class KernelSemaphoreState
{
public required string Name { get; init; }
@@ -25,35 +23,9 @@ public static class KernelSemaphoreCompatExports
public required int MaxCount { get; init; }
public int Count { get; set; }
public int WaitingThreads { get; set; }
public int CancelEpoch { get; set; }
public bool Deleted { get; set; }
public object Gate { get; } = new();
}
private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter
{
public required KernelSemaphoreState Semaphore { get; init; }
public required int NeedCount { get; init; }
public required int CancelEpochAtBlock { get; init; }
public bool Timed { get; init; }
// Timed-wait completion state; unused when Timed is false.
public CpuContext? Ctx { get; init; }
public ulong TimeoutAddress { get; init; }
public long DeadlineTimestamp { get; init; }
// Written and read only under the owning semaphore's Gate.
public int? Result { get; set; }
public int Resume() => Timed
? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp)
: CompleteBlockedSemaWait(Semaphore, this);
public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this);
}
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
[SysAbiExport(
Nid = "188x57JYp0g",
ExportName = "sceKernelCreateSema",
@@ -76,12 +48,12 @@ public static class KernelSemaphoreCompatExports
initialCount > maxCount ||
optionAddress != 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
@@ -90,7 +62,7 @@ public static class KernelSemaphoreCompatExports
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
}
var state = new KernelSemaphoreState
_semaphores[handle] = new KernelSemaphoreState
{
Name = name,
WakeKey = GetSemaphoreWakeKey(handle),
@@ -98,28 +70,15 @@ public static class KernelSemaphoreCompatExports
MaxCount = maxCount,
Count = initialCount,
};
_semaphores[handle] = state;
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
// Handles are sequential and guest-predictable, so a hostile guest can
// race a WaitSema onto the handle between publication above and this
// rollback. Strand-proof that waiter exactly like DeleteSema does.
lock (state.Gate)
{
state.Deleted = true;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (_traceSema)
{
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -135,125 +94,144 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var pollTimedOut = false;
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
if (_traceSema)
if (timeoutAddress != 0)
{
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
_ = TryWriteUInt32(ctx, timeoutAddress, timeoutUsec);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
semaphore.WaitingThreads++;
}
// Block cooperatively: the wake predicate atomically acquires the
// tokens (so a wake commits the acquisition), while the resume
// handler distinguishes a real acquisition from a deadline expiry.
var acquired = false;
var deadline = timeoutAddress != 0
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
bool WakePredicate()
{
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
acquired = true;
return true;
}
return false;
}
}
int ResumeWait()
{
if (timeoutAddress != 0)
{
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (timeoutMicros == 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
if (_traceSema)
{
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
pollTimedOut = true;
}
else
{
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
var timedWaiter = new SemaphoreWaiter
{
Semaphore = semaphore,
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
Timed = true,
Ctx = ctx,
TimeoutAddress = timeoutAddress,
DeadlineTimestamp = deadline,
};
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
semaphore.WakeKey,
timedWaiter,
blockDeadlineTimestamp: deadline))
{
semaphore.WaitingThreads++;
if (_traceSema)
{
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Host-owned threads cannot park in the guest scheduler; degrade to the
// immediate-timeout poll the callers already tolerate.
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
if (_traceSema)
{
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
pollTimedOut = true;
}
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
if (!pollTimedOut)
if (acquired)
{
var waiter = new SemaphoreWaiter
{
Semaphore = semaphore,
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
};
if (!GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
semaphore.WakeKey,
waiter))
{
if (_traceSema)
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
semaphore.WaitingThreads++;
if (_traceSema)
{
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"wait-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
lock (semaphore.Gate)
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
}
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
if ((++_semaPollBackoffCount & 255) == 0)
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
ResumeWait,
WakePredicate,
deadline))
{
Thread.Sleep(0);
}
else
{
Thread.Yield();
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
// Not a guest thread (or no scheduler): fall back to a host-thread
// wait so the semantics still hold on non-cooperative callers.
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
}
private static int WaitSemaphoreOnHostThread(
CpuContext ctx,
KernelSemaphoreState semaphore,
uint handle,
int needCount,
ulong timeoutAddress,
uint timeoutUsec)
{
var deadlineMs = timeoutAddress != 0
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
: long.MaxValue;
lock (semaphore.Gate)
{
TraceSemaphore(
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
while (semaphore.Count < needCount)
{
var remaining = deadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
}
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore(
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
private static string GetSemaphoreWakeKey(uint handle) => $"sceKernelWaitSema:{handle:X8}";
[SysAbiExport(
Nid = "12wOHk8ywb0",
ExportName = "sceKernelPollSema",
@@ -263,31 +241,25 @@ public static class KernelSemaphoreCompatExports
{
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count < needCount)
{
if (_traceSema)
{
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
semaphore.Count -= needCount;
if (_traceSema)
{
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -300,34 +272,31 @@ public static class KernelSemaphoreCompatExports
{
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (signalCount <= 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count > semaphore.MaxCount - signalCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
semaphore.Count += signalCount;
if (_traceSema)
{
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
// Wake host-thread waiters parked in the fallback path.
Monitor.PulseAll(semaphore.Gate);
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
}
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
// whose needCount exceeds the remaining count stays parked while a smaller
// waiter can proceed.
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
// Wake cooperatively-blocked guest threads; their wake predicate
// acquires the tokens atomically, so this respects the new count.
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -339,35 +308,29 @@ public static class KernelSemaphoreCompatExports
{
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (setCount > semaphore.MaxCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.CancelEpoch++;
// WaitingThreads is NOT zeroed here: each canceled waiter decrements it
// exactly once in its wake handler. Zeroing here as well would double-count
// and silently absorb the increment of a waiter that parks between this
// gate release and the wake-all below.
if (_traceSema)
{
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
}
semaphore.WaitingThreads = 0;
Monitor.PulseAll(semaphore.Gate);
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -380,138 +343,238 @@ public static class KernelSemaphoreCompatExports
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
if (!_semaphores.TryRemove(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
// Delete succeeds even with blocked waiters; they wake with the deleted
// result (the SCE kernel wakes waiters with the EACCES-class code).
lock (semaphore.Gate)
{
semaphore.Deleted = true;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
if (_traceSema)
{
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
// Wake handler: runs under the scheduler's guest-thread gate (lock order:
// scheduler gate -> semaphore gate). Returns true iff the waiter has a final
// result and should be re-readied; false leaves it parked.
private static bool TryConsumeBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
[SysAbiExport(
Nid = "pDuPEf3m4fI",
ExportName = "sem_init",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemInit(CpuContext ctx)
{
lock (semaphore.Gate)
var semaphoreAddress = ctx[CpuRegister.Rdi];
var initialCountValue = ctx[CpuRegister.Rdx];
if (semaphoreAddress == 0 || initialCountValue > int.MaxValue)
{
return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
if (handle == 0)
{
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
}
var initialCount = unchecked((int)initialCountValue);
_semaphores[handle] = new KernelSemaphoreState
{
Name = $"posix@0x{semaphoreAddress:X16}",
WakeKey = GetSemaphoreWakeKey(handle),
InitialCount = initialCount,
MaxCount = int.MaxValue,
Count = initialCount,
};
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSemaphore($"posix-init address=0x{semaphoreAddress:X16} handle=0x{handle:X8} count={initialCount}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static bool TryConsumeBlockedSemaWaitLocked(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
[SysAbiExport(
Nid = "YCV5dGGBcCo",
ExportName = "sem_wait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemWait(CpuContext ctx)
{
if (waiter.Result is not null)
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
{
return true;
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (semaphore.Deleted)
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
}
return true;
}
if (semaphore.CancelEpoch != waiter.CancelEpochAtBlock)
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
}
return true;
}
if (semaphore.Count >= waiter.NeedCount)
{
semaphore.Count -= waiter.NeedCount;
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
return true;
}
return false;
ctx[CpuRegister.Rdi] = handle;
ctx[CpuRegister.Rsi] = 1;
ctx[CpuRegister.Rdx] = 0;
return KernelWaitSema(ctx);
}
// Resume handler: runs on the woken guest thread outside the scheduler gate;
// its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
[SysAbiExport(
Nid = "WBWzsRifCEA",
ExportName = "sem_trywait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemTryWait(CpuContext ctx)
{
lock (semaphore.Gate)
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
// Nothing readies a parked semaphore waiter without the wake handler
// resolving it, so reaching here means the scheduler contract changed.
Console.Error.WriteLine(
$"[LOADER][GAP] sema.resume-no-outcome name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count}");
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
}
return waiter.Result!.Value;
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rdi] = handle;
ctx[CpuRegister.Rsi] = 1;
return KernelPollSema(ctx, handle, 1);
}
private static int CompleteBlockedTimedSemaWait(
CpuContext ctx,
KernelSemaphoreState semaphore,
SemaphoreWaiter waiter,
ulong timeoutAddress,
long deadlineTimestamp)
[SysAbiExport(
Nid = "w5IHyvahg-o",
ExportName = "sem_timedwait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemTimedWait(CpuContext ctx)
{
int result;
lock (semaphore.Gate)
var timeoutAddress = ctx[CpuRegister.Rsi];
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
}
result = waiter.Result!.Value;
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rdi] = handle;
ctx[CpuRegister.Rsi] = 1;
ctx[CpuRegister.Rdx] = timeoutAddress;
return KernelWaitSema(ctx);
}
[SysAbiExport(
Nid = "IKP8typ0QUk",
ExportName = "sem_post",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemPost(CpuContext ctx)
{
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rdi] = handle;
ctx[CpuRegister.Rsi] = 1;
return KernelSignalSema(ctx, handle, 1);
}
[SysAbiExport(
Nid = "Bq+LRV-N6Hk",
ExportName = "sem_getvalue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemGetValue(CpuContext ctx)
{
var semaphoreAddress = ctx[CpuRegister.Rdi];
var valueAddress = ctx[CpuRegister.Rsi];
if (valueAddress == 0 ||
!TryGetPosixSemaphoreHandle(ctx, semaphoreAddress, out var handle) ||
!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
int count;
lock (semaphore.Gate)
{
count = semaphore.Count;
}
return TryWriteUInt32(ctx, valueAddress, unchecked((uint)count))
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "cDW233RAwWo",
ExportName = "sem_destroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixSemDestroy(CpuContext ctx)
{
var semaphoreAddress = ctx[CpuRegister.Rdi];
if (!TryGetPosixSemaphoreHandle(ctx, semaphoreAddress, out var handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rdi] = handle;
var result = KernelDeleteSema(ctx);
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteUInt32(ctx, semaphoreAddress, 0);
}
return result;
}
private static bool TryGetPosixSemaphoreHandle(CpuContext ctx, ulong semaphoreAddress, out uint handle)
{
handle = 0;
return semaphoreAddress != 0 &&
TryReadUInt32(ctx, semaphoreAddress, out handle) &&
handle != 0;
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0 || maxLength <= 0)
{
return false;
}
var bytes = new byte[Math.Min(maxLength, 4096)];
Span<byte> current = stackalloc byte[1];
for (var i = 0; i < bytes.Length; i++)
{
if (!ctx.Memory.TryRead(address + (ulong)i, current))
{
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, i);
return true;
}
bytes[i] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
// Call sites must check this before building the interpolated message; the trace
// strings would otherwise be allocated on every semaphore op even with tracing off.
private static readonly bool _traceSema =
@@ -521,4 +584,10 @@ public static class KernelSemaphoreCompatExports
{
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
}
private static string FormatCallSite(CpuContext ctx)
{
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnAddress);
return $"guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{returnAddress:X16}";
}
}