From 2df6f5b0a0fac5d9c718680fa0d3c963e913115b Mon Sep 17 00:00:00 2001 From: Berk <12572227+par274@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:43:21 +0300 Subject: [PATCH] Unity fixes 1016p1 (#1043) * [HLE] Let schedulers report guest thread stack bounds Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Core] Report the mapped stack of each guest thread Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Validate pthread stack bounds before reporting them Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Report every guest thread's stack in pthread attributes Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Publish semaphore handles as 64-bit values Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Accept zero-count semaphore signals Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Route semaphore waits through one core Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Poll host semaphore waits and deliver guest exceptions Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Add scePthreadSemTimedwait Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Remove every pending copy of a deleted event Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Block timed event queue waits in the guest scheduler Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Refresh pending user data when an event is re-added Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Keep every end-of-pipe interrupt event Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Track deadlines and POSIX results on sync waiters Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Poll host sync waits in bounded slices Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Share the sync-on-address wait and wake cores Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Kernel] Add _umtx_op wait and wake Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Np] Track WebApi2 user contexts and push filters Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Np] Add sceNpWebApi2PushEventDeleteHandle Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Np] Add sceNpWebApi2PushEventRegisterCallback Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Tests] Cover kernel sync, pthread stack bounds and NpWebApi2 Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [GPU] Accept HTile-backed compressed stencil targets Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Tests] Cover HTile-backed compressed stencil targets Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [GPU] Accept stencil targets with or without HTile backing * [Tests] Cover stencil compression with and without HTile * [ShaderCompiler] Treat V_CMPX as an EXEC write in every encoding * [Tests] Cover SDWA V_CMPX in the EXEC-full analysis * [GPU] Give images first found without metadata the DCC metadata of a texture request Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Tests] Cover DCC metadata adopted from a texture request Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [GPU] Number command stream submissions in admission order Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [GPU] Queue control barriers behind accepted command streams Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [GPU] Cancel pending control barriers when the command stream fails Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [GPU] Cancel pending control barriers when the command stream is discarded Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [GPU] Run relay work after accepted command streams Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [VideoOut] Order relay work behind the presenter command stream Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [GPU] Let backends run work after pending command streams Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [VideoOut] Run state changes after pending command streams Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Metal] Run state changes after pending command streams Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [VideoOut] Close ports after pending command streams Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [VideoOut] Unregister buffers after pending command streams Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> * [Tests] Cover ordered video-out state changes Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> --------- Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com> --- .../Cpu/Native/DirectExecutionBackend.cs | 27 ++ src/SharpEmu.HLE/GuestThreadExecution.cs | 15 + .../Gpu/GpuCommands/CommandStreamQueue.cs | 162 +++++++-- .../Gpu/GpuCommands/CommandSubmission.cs | 2 + src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs | 5 + .../Gpu/Images/GuestImageCache.cs | 11 + .../ImageRequestBuilders.DepthTarget.cs | 4 +- .../Gpu/Metal/MetalGuestGpuBackend.cs | 51 +++ .../Gpu/Scheduling/GpuWorkerRelay.cs | 46 ++- .../Gpu/Vulkan/VulkanGuestGpuBackend.cs | 3 + .../Kernel/KernelEventQueueCompatExports.cs | 169 ++++----- .../KernelPthreadExtendedCompatExports.cs | 38 +- .../Kernel/KernelSemaphoreCompatExports.cs | 129 +++++-- .../KernelSyncOnAddressCompatExports.cs | 325 +++++++++++++++--- src/SharpEmu.Libs/Np/NpWebApi2Exports.cs | 77 +++++ src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 59 ++-- .../VulkanVideoPresenter.CommandStream.cs | 20 ++ .../VulkanVideoPresenter.Scheduler.cs | 2 +- .../VideoOut/VulkanVideoPresenter.cs | 2 + .../Ir/Gen5ExecFullAnalysis.cs | 12 +- .../Cpu/GuestThreadStackBoundsTests.cs | 51 +++ .../GpuCommands/CommandStreamQueueTests.cs | 77 +++++ .../Images/GuestImageCacheTests.Metadata.cs | 35 ++ .../Gpu/Images/ImageRequestBuildersTests.cs | 32 ++ .../Gpu/Scheduling/GpuWorkerRelayTests.cs | 114 +++++- .../Kernel/KernelHostWaitExceptionTests.cs | 135 ++++++++ .../KernelSemaphoreCompatExportsTests.cs | 38 ++ .../KernelSyncOnAddressCompatExportsTests.cs | 172 +++++++++ .../Kernel/SemaphoreSignalProfileTests.cs | 3 +- .../Np/NpWebApi2ExportsTests.cs | 78 +++++ .../Pthread/PthreadSemaphoreSemanticsTests.cs | 36 ++ .../Pthread/PthreadStackBoundsTests.cs | 138 ++++++++ .../Gen5ExecFullAnalysisTests.cs | 25 ++ 33 files changed, 1871 insertions(+), 222 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Cpu/GuestThreadStackBoundsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Kernel/KernelHostWaitExceptionTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Kernel/KernelSemaphoreCompatExportsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Np/NpWebApi2ExportsTests.cs create mode 100644 tests/SharpEmu.Libs.Tests/Pthread/PthreadStackBoundsTests.cs diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 71e8669..3c36b57 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -3978,6 +3978,33 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I public bool SupportsGuestContextTransfer => true; + public bool TryGetGuestThreadStackBounds( + ulong threadHandle, + out ulong stackBase, + out ulong stackSize) + { + stackBase = 0; + stackSize = 0; + if (threadHandle == 0) + { + return false; + } + + lock (_guestThreadGate) + { + if (!_guestThreads.TryGetValue(threadHandle, out var thread) || + thread.StackBase == 0 || + thread.StackSize == 0) + { + return false; + } + + stackBase = thread.StackBase; + stackSize = thread.StackSize; + return true; + } + } + public void RegisterGuestThreadContext(ulong threadHandle, CpuContext context) { if (threadHandle == 0) diff --git a/src/SharpEmu.HLE/GuestThreadExecution.cs b/src/SharpEmu.HLE/GuestThreadExecution.cs index 4879358..2a1f013 100644 --- a/src/SharpEmu.HLE/GuestThreadExecution.cs +++ b/src/SharpEmu.HLE/GuestThreadExecution.cs @@ -50,6 +50,21 @@ public interface IGuestThreadScheduler /// void RegisterGuestThreadContext(ulong threadHandle, CpuContext context); + /// + /// Returns the exact mapped guest stack owned by a scheduler thread. The + /// default keeps alternate/test schedulers source-compatible when they do + /// not own guest stack mappings. + /// + bool TryGetGuestThreadStackBounds( + ulong threadHandle, + out ulong stackBase, + out ulong stackSize) + { + stackBase = 0; + stackSize = 0; + return false; + } + bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error); bool TryJoinThread( diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/CommandStreamQueue.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/CommandStreamQueue.cs index d0a0de6..1db6ec3 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/CommandStreamQueue.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/CommandStreamQueue.cs @@ -26,6 +26,8 @@ public enum SliceResult // Submissions of every queue; one thread runs the slices, other threads enqueue and wait. public sealed class CommandStreamQueue { + private sealed record ControlBarrier(ulong CutoffOrdinal, Action Work, Action Cancel); + public const int ComputeQueueCount = 56; public const int QueueCount = 1 + ComputeQueueCount; public const int AllBlockedRetryMilliseconds = 100; @@ -37,8 +39,10 @@ public sealed class CommandStreamQueue private readonly object _gate = new(); private readonly LinkedList[] _queues = new LinkedList[QueueCount]; private readonly GpuCommandInterpreter?[] _interpreters = new GpuCommandInterpreter?[QueueCount]; + private readonly Queue _controlBarriers = new(); private int _nextQueue; private int _submissionCount; + private ulong _admissionOrdinal; private bool _accepting = true; private bool _stopping; private bool _processing; @@ -91,7 +95,7 @@ public sealed class CommandStreamQueue { lock (_gate) { - return _submissionCount != 0; + return _submissionCount != 0 || _controlBarriers.Count != 0; } } } @@ -113,7 +117,7 @@ public sealed class CommandStreamQueue { lock (_gate) { - return SelectQueueLocked() >= 0; + return HasRunnableWorkLocked(); } } } @@ -200,6 +204,7 @@ public sealed class CommandStreamQueue throw _host.Fatal($"The command stream no longer accepts submissions: queue={submission.QueueId} address=0x{submission.Address:X16}."); } + submission.AdmissionOrdinal = ++_admissionOrdinal; _queues[submission.QueueId].AddLast(submission); _submissionCount++; if (submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary)) @@ -208,6 +213,23 @@ public sealed class CommandStreamQueue Monitor.PulseAll(_gate); } + public bool TryEnqueueControlBarrier(Action work, Action cancel) + { + ArgumentNullException.ThrowIfNull(work); + ArgumentNullException.ThrowIfNull(cancel); + lock (_gate) + { + if (!_accepting) + { + return false; + } + + _controlBarriers.Enqueue(new ControlBarrier(_admissionOrdinal, work, cancel)); + Monitor.PulseAll(_gate); + return true; + } + } + public IdleOutcome Done() { if (_processingThread == Thread.CurrentThread) @@ -257,7 +279,8 @@ public sealed class CommandStreamQueue { lock (_gate) { - while (_outcome == IdleOutcome.Completed && (_processing || _submissionCount != 0)) + while (_outcome == IdleOutcome.Completed && + (_processing || _submissionCount != 0 || _controlBarriers.Count != 0)) { Monitor.Wait(_gate); } @@ -279,13 +302,13 @@ public sealed class CommandStreamQueue { lock (_gate) { - if (SelectQueueLocked() >= 0 || _stopping) + if (HasRunnableWorkLocked() || _stopping) { - return SelectQueueLocked() >= 0; + return HasRunnableWorkLocked(); } Monitor.Wait(_gate, timeoutMilliseconds); - return SelectQueueLocked() >= 0; + return HasRunnableWorkLocked(); } } @@ -295,7 +318,7 @@ public sealed class CommandStreamQueue lock (_gate) { var deadline = Environment.TickCount64 + intervalMilliseconds; - while (SelectQueueLocked() < 0) + while (!HasRunnableWorkLocked()) { var remaining = deadline - Environment.TickCount64; if (remaining <= 0) @@ -313,10 +336,15 @@ public sealed class CommandStreamQueue // Publishes a failure: nothing runs any more and every waiter is released with Failed. public void Fail() { + Action[] cancellations; lock (_gate) { FailLocked(); + cancellations = CancelControlBarriersLocked(); } + + foreach (var cancel in cancellations) + cancel(); } private void FailLocked() @@ -364,7 +392,8 @@ public sealed class CommandStreamQueue // Runs one slice of the next unblocked submission on the calling thread. public SliceResult ProcessOne() { - CommandSubmission submission; + CommandSubmission? submission = null; + ControlBarrier? control = null; lock (_gate) { if (_processing) @@ -372,35 +401,73 @@ public sealed class CommandStreamQueue throw _host.Fatal("The command stream queue is already processing a slice."); } - var selected = SelectQueueLocked(); - if (selected < 0) + if (IsControlBarrierReadyLocked()) { - return _submissionCount != 0 ? SliceResult.AllBlocked : SliceResult.NoWork; + control = _controlBarriers.Dequeue(); + } + else + { + var selected = SelectQueueLocked(); + if (selected < 0) + { + return _submissionCount != 0 ? SliceResult.AllBlocked : SliceResult.NoWork; + } + + var queue = _queues[selected]; + submission = queue.First!.Value; + queue.RemoveFirst(); + _submissionCount--; + _nextQueue = (selected + 1) % QueueCount; } - var queue = _queues[selected]; - submission = queue.First!.Value; - queue.RemoveFirst(); - _submissionCount--; - _nextQueue = (selected + 1) % QueueCount; _processing = true; _processingThread = Thread.CurrentThread; - if (!submission.Started && submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary)) + if (submission is { Started: false } && + submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary)) SubmissionFlowProfile.Record(SubmissionFlowProfile.EventKind.ProcessingStarted, submission.QueueId, submission.SubmissionId, submission.Address, submission.DwordCount, _submissionCount); } + if (control is not null) + { + try + { + control.Work(); + } + catch (Exception exception) + { + try + { + Console.Error.WriteLine($"[GPU][ERROR] Command stream control barrier failed. {exception}"); + } + catch (Exception) + { + } + + Fail(); + throw; + } + + lock (_gate) + { + _processing = false; + Monitor.PulseAll(_gate); + } + + return SliceResult.Completed; + } + bool complete; try { - complete = RunSlice(submission); + complete = RunSlice(submission!); } catch (Exception exception) { try { // Report the cause before another submitting thread observes the failed queue. - Console.Error.WriteLine($"[GPU][ERROR] Command stream slice failed: queue={submission.QueueId} address=0x{submission.Address:X16}. {exception}"); + Console.Error.WriteLine($"[GPU][ERROR] Command stream slice failed: queue={submission!.QueueId} address=0x{submission.Address:X16}. {exception}"); } catch (Exception) { @@ -416,7 +483,7 @@ public sealed class CommandStreamQueue SliceResult result; if (!complete) { - submission.Blocked = true; + submission!.Blocked = true; submission.BlockedSinceTicks = System.Diagnostics.Stopwatch.GetTimestamp(); submission.BlockedPacketAddress = submission.Commands.CurrentPacketAddress; _queues[submission.QueueId].AddFirst(submission); @@ -597,12 +664,17 @@ public sealed class CommandStreamQueue // Drops every submission without stopping admission; the presenter uses it after device loss. public void DiscardAll() { + Action[] cancellations; lock (_gate) { _outcome = IdleOutcome.Failed; DropAllLocked(); + cancellations = CancelControlBarriersLocked(); Monitor.PulseAll(_gate); } + + foreach (var cancel in cancellations) + cancel(); } // A diagnostic view of the blocked heads: their count, the oldest age and one sample. @@ -669,6 +741,20 @@ public sealed class CommandStreamQueue private int SelectQueueLocked() { + if (_controlBarriers.TryPeek(out var control)) + { + for (var offset = 0; offset < QueueCount; offset++) + { + var queueId = (_nextQueue + offset) % QueueCount; + if (_queues[queueId].First is { } head && + head.Value.AdmissionOrdinal <= control.CutoffOrdinal && + !head.Value.Blocked) + { + return queueId; + } + } + } + for (var offset = 0; offset < QueueCount; offset++) { var queueId = (_nextQueue + offset) % QueueCount; @@ -681,6 +767,42 @@ public sealed class CommandStreamQueue return -1; } + private bool HasRunnableWorkLocked() => IsControlBarrierReadyLocked() || SelectQueueLocked() >= 0; + + private bool IsControlBarrierReadyLocked() + { + if (!_controlBarriers.TryPeek(out var control)) + { + return false; + } + + foreach (var queue in _queues) + { + if (queue.First is { } head && head.Value.AdmissionOrdinal <= control.CutoffOrdinal) + { + return false; + } + } + + return true; + } + + private Action[] CancelControlBarriersLocked() + { + if (_controlBarriers.Count == 0) + { + return []; + } + + var cancellations = new Action[_controlBarriers.Count]; + for (var index = 0; index < cancellations.Length; index++) + { + cancellations[index] = _controlBarriers.Dequeue().Cancel; + } + + return cancellations; + } + private void DropAllLocked() { foreach (var queue in _queues) diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/CommandSubmission.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/CommandSubmission.cs index 701022d..5ec2b47 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/CommandSubmission.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/CommandSubmission.cs @@ -101,6 +101,8 @@ public sealed class CommandSubmission // The submitter's own sequence number, kept for diagnostics and the translation state. public ulong SubmissionId { get; } + internal ulong AdmissionOrdinal { get; set; } + // Snapshots captured at submit time; the processor never reads them. public object? GeometrySnapshots { get; } diff --git a/src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs b/src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs index 96120cf..c8b3160 100644 --- a/src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs +++ b/src/SharpEmu.Libs/Gpu/IGuestGpuBackend.cs @@ -57,6 +57,11 @@ internal interface IGuestGpuBackend // Marks the frame boundary; off the worker it first waits for the accepted submissions. IdleOutcome SubmitDone(ICpuMemory memory); + void RunAfterPendingCommandStreams(Action work) + { + work(); + } + /// Registers a display buffer with its guest texture format tag. void RegisterKnownDisplayBuffer(ulong address, uint guestFormat); diff --git a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs index 94728e9..8d50e0a 100644 --- a/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs +++ b/src/SharpEmu.Libs/Gpu/Images/GuestImageCache.cs @@ -126,6 +126,17 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma var found = LookUpImage(ref request, exactFormat); if (request.Role is ImageRole.Texture or ImageRole.StorageImage) { + if (request.Description.Metadata.Kind == MetadataKind.Dcc) + { + using var held = _lock.Hold(); + var image = _slots[found]; + if (image.Description.Metadata.Kind == MetadataKind.None) + { + image.Description.Metadata.Kind = MetadataKind.Dcc; + image.Description.Metadata.Range = request.Description.Metadata.Range; + } + } + ref readonly var description = ref _slots[found].Description; if (description.DccSliceSize is var sliceSize and not 0) { diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs index c0b7dc3..8a0c357 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs @@ -58,8 +58,6 @@ public static partial class ImageRequestBuilders return Format.Undefined; } - private static bool HtileStencilCompatible(bool hasStencil, bool hasHtile, bool htileStencilDisabled) => !hasStencil || !hasHtile || htileStencilDisabled; - // Builds the request for the bound depth target. Null when no depth or stencil state is active. public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) => DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false); @@ -127,7 +125,7 @@ public static partial class ImageRequestBuilders if (hasStencil) { - if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt || !HtileStencilCompatible(hasStencil, hasHtile, depthWords.HtileStencilDisabled) || + if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt || stencilAddress == 0 || (!copyMode && !depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (stencilAddress & 0xFFFF) != 0) { throw SubmissionScheduler.Fatal( diff --git a/src/SharpEmu.Libs/Gpu/Metal/MetalGuestGpuBackend.cs b/src/SharpEmu.Libs/Gpu/Metal/MetalGuestGpuBackend.cs index 0ba5ab0..9c67fdb 100644 --- a/src/SharpEmu.Libs/Gpu/Metal/MetalGuestGpuBackend.cs +++ b/src/SharpEmu.Libs/Gpu/Metal/MetalGuestGpuBackend.cs @@ -124,6 +124,57 @@ internal sealed class MetalGuestGpuBackend : IGuestGpuBackend, IGuestImageSnapsh public IdleOutcome SubmitDone(ICpuMemory memory) => EnsureCommandStream(memory).Done(); + public void RunAfterPendingCommandStreams(Action work) + { + ArgumentNullException.ThrowIfNull(work); + + CommandStreamQueue? commandStream; + CommandStreamWorker? worker; + lock (_commandStreamGate) + { + commandStream = _commandStream; + worker = _commandStreamWorker; + } + + if (commandStream is null || worker?.IsWorkerThread == true) + { + work(); + return; + } + + using var done = new SemaphoreSlim(0); + System.Runtime.ExceptionServices.ExceptionDispatchInfo? failure = null; + if (!commandStream.TryEnqueueControlBarrier( + () => + { + try + { + work(); + } + catch (Exception exception) + { + failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception); + } + finally + { + done.Release(); + } + }, + () => + { + failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture( + new OperationCanceledException("The command stream stopped before the video-out state change could run.")); + done.Release(); + })) + { + work(); + return; + } + + done.Wait(); + failure?.Throw(); + } + public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) => MetalVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat); diff --git a/src/SharpEmu.Libs/Gpu/Scheduling/GpuWorkerRelay.cs b/src/SharpEmu.Libs/Gpu/Scheduling/GpuWorkerRelay.cs index 19e67f0..028e46b 100644 --- a/src/SharpEmu.Libs/Gpu/Scheduling/GpuWorkerRelay.cs +++ b/src/SharpEmu.Libs/Gpu/Scheduling/GpuWorkerRelay.cs @@ -14,16 +14,60 @@ public sealed class GpuWorkerRelay : IGpuQueueRelay private readonly object _gate = new(); private readonly Queue _commands = new(); private readonly Action _wake; + private readonly Func? _tryEnqueueAfterPendingWork; private int _pendingCount; private bool _accepting = true; - public GpuWorkerRelay(Action wake) + public GpuWorkerRelay(Action wake, Func? tryEnqueueAfterPendingWork = null) { _wake = wake; + _tryEnqueueAfterPendingWork = tryEnqueueAfterPendingWork; } public bool TryRunAfterPendingWork(Action work) => TryRunOnGpuQueue(work); + public bool TryRunAfterAcceptedCommandStreams(Action work) + { + ArgumentNullException.ThrowIfNull(work); + if (IsGpuQueueThread || _tryEnqueueAfterPendingWork is null) + { + return TryRunOnGpuQueue(work); + } + + using var done = new SemaphoreSlim(0); + System.Runtime.ExceptionServices.ExceptionDispatchInfo? failure = null; + if (!_tryEnqueueAfterPendingWork( + () => + { + try + { + work(); + } + catch (Exception exception) + { + failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception); + } + finally + { + done.Release(); + } + }, + () => + { + failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture( + new OperationCanceledException( + "The command stream stopped before the ordered GPU worker action could run.")); + done.Release(); + })) + { + return false; + } + + done.Wait(); + failure?.Throw(); + return true; + } + public bool IsGpuQueueThread => _boundWorker == this; public bool HasPendingCommands => Volatile.Read(ref _pendingCount) != 0; diff --git a/src/SharpEmu.Libs/Gpu/Vulkan/VulkanGuestGpuBackend.cs b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanGuestGpuBackend.cs index 2d8eec6..5ca67cc 100644 --- a/src/SharpEmu.Libs/Gpu/Vulkan/VulkanGuestGpuBackend.cs +++ b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanGuestGpuBackend.cs @@ -66,6 +66,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend public IdleOutcome SubmitDone(ICpuMemory memory) => VulkanVideoPresenter.SubmitDone(memory); + public void RunAfterPendingCommandStreams(Action work) => + VulkanVideoPresenter.RunAfterPendingCommandStreams(work); + public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) => VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat); diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index 0b5d967..061afd8 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -161,20 +161,43 @@ public static class KernelEventQueueCompatExports } public bool Remove(ulong ident, short filter) + => RemoveAll(ident, filter) != 0; + + public int RemoveAll(ulong ident, short filter) { - var index = FindIndex(ident, filter); - if (index < 0) + var originalCount = Count; + var writeIndex = 0; + for (var readIndex = 0; readIndex < originalCount; readIndex++) { - return false; + var candidate = this[readIndex]; + if (candidate.Ident == ident && candidate.Filter == filter) + { + continue; + } + + this[writeIndex++] = candidate; } - for (var i = index; i + 1 < Count; i++) + Count = writeIndex; + return originalCount - writeIndex; + } + + public int UpdateUserData(ulong ident, short filter, ulong userData) + { + var updatedCount = 0; + for (var index = 0; index < Count; index++) { - this[i] = this[i + 1]; + var candidate = this[index]; + if (candidate.Ident != ident || candidate.Filter != filter) + { + continue; + } + + this[index] = candidate with { UserData = userData }; + updatedCount++; } - Count--; - return true; + return updatedCount; } } @@ -824,52 +847,55 @@ public static class KernelEventQueueCompatExports } var waiterId = Interlocked.Increment(ref _nextEventQueueWaiterId); - if (timeoutAddress == 0) - { - var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock( - ctx, - "sceKernelWaitEqueue", - state.WakeKey, - new EqueueWaiter - { - Ctx = ctx, - State = state, - EventsAddress = eventsAddress, - EventCapacity = eventCapacity, - OutCountAddress = outCountAddress, - WaiterId = waiterId, - }); - if (requestedBlock) + var blockDeadlineTimestamp = timeoutAddress == 0 + ? 0 + : GuestThreadExecution.ComputeDeadlineTimestamp( + TimeSpan.FromMicroseconds(timeoutUsec)); + var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock( + ctx, + "sceKernelWaitEqueue", + state.WakeKey, + new EqueueWaiter { - var wakeAfterRegistration = false; - lock (_eventQueueGate) - { - wakeAfterRegistration = - !IsLiveEventQueueLocked(state) || - HasPendingEventsLocked(state.Handle); - } - - if (wakeAfterRegistration) - { - WakeEventQueue( - state, - _logEqueue - ? "source=post-registration-state-check" - : null); - } - - if (_logEqueue) - { - TraceEventQueue( - ctx, - "wait-block", - handle, - $"generation={state.Generation} waiter={waiterId} " + - $"capacity={eventCapacity} timeout=infinite " + - $"events=0x{eventsAddress:X16} out_count=0x{outCountAddress:X16}"); - } - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + Ctx = ctx, + State = state, + EventsAddress = eventsAddress, + EventCapacity = eventCapacity, + OutCountAddress = outCountAddress, + WaiterId = waiterId, + }, + blockDeadlineTimestamp); + if (requestedBlock) + { + var wakeAfterRegistration = false; + lock (_eventQueueGate) + { + wakeAfterRegistration = + !IsLiveEventQueueLocked(state) || + HasPendingEventsLocked(state.Handle); } + + if (wakeAfterRegistration) + { + WakeEventQueue( + state, + _logEqueue + ? "source=post-registration-state-check" + : null); + } + + if (_logEqueue) + { + TraceEventQueue( + ctx, + "wait-block", + handle, + $"generation={state.Generation} waiter={waiterId} " + + $"capacity={eventCapacity} timeout=" + + (timeoutAddress == 0 ? "infinite " : $"{timeoutUsec}_usec ") + + $"events=0x{eventsAddress:X16} out_count=0x{outCountAddress:X16}"); + } + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } if (timeoutAddress != 0) @@ -1141,9 +1167,10 @@ public static class KernelEventQueueCompatExports } var key = (ident, filter); + var isExistingRegistration = events.ContainsKey(key); var isNewReadRegistration = filter == KernelEventFilterRead && - !events.ContainsKey(key); + !isExistingRegistration; events[(ident, filter)] = new KernelEventRegistration( ident, filter, @@ -1152,6 +1179,11 @@ public static class KernelEventQueueCompatExports lowWater, unchecked((ulong)Interlocked.Increment( ref _nextEventRegistrationGeneration))); + if (isExistingRegistration && + _pendingEvents.TryGetValue(handle, out var pending)) + { + _ = pending.UpdateUserData(ident, filter, userData); + } if (isNewReadRegistration) { _readEventRegistrationCount++; @@ -1296,7 +1328,7 @@ public static class KernelEventQueueCompatExports if (_pendingEvents.TryGetValue(handle, out var pending)) { - _ = pending.Remove(ident, filter); + _ = pending.RemoveAll(ident, filter); } return true; @@ -1387,22 +1419,13 @@ public static class KernelEventQueueCompatExports _pendingEvents[handle] = queue; } - // Keep each interrupt as a separate event. Limit growth when the queue is not read. - var queuedEvent = new KernelQueuedEvent( + queue.AddLast(new KernelQueuedEvent( registration.Ident, registration.Filter, registration.Flags, 1, data, - registration.UserData); - if (CountPendingEvents(queue, registration.Ident, registration.Filter) < 256) - { - queue.AddLast(queuedEvent); - } - else - { - QueueOrUpdateEvent(queue, queuedEvent); - } + registration.UserData)); (wakeQueues ??= []).Add(state); triggeredCount++; @@ -1630,24 +1653,6 @@ public static class KernelEventQueueCompatExports } } - private static int CountPendingEvents( - KernelEventDeque queue, - ulong ident, - short filter) - { - var count = 0; - for (var i = 0; i < queue.Count; i++) - { - var pending = queue[i]; - if (pending.Ident == ident && pending.Filter == filter) - { - count++; - } - } - - return count; - } - private static void PollReadEvents(object? state) { _ = state; diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index 16b86ca..e423acd 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -548,23 +548,38 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } + ulong stackAddress = 0; + ulong stackSize = 0; + var hasStackBounds = false; + var scheduler = GuestThreadExecution.Scheduler; + if (scheduler is not null && + scheduler.TryGetGuestThreadStackBounds(thread, out stackAddress, out stackSize) && + IsValidStackBounds(stackAddress, stackSize)) + { + hasStackBounds = true; + } + else + { + stackAddress = 0; + stackSize = 0; + if (thread == KernelPthreadState.GetCurrentThreadHandle() && + TryInferNativeGuestStack(ctx[CpuRegister.Rsp], out stackAddress)) + { + stackSize = NativeGuestStackSize; + hasStackBounds = true; + } + } + 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)) + if (hasStackBounds) { threadState.Attributes = threadState.Attributes with { StackAddress = stackAddress, - StackSize = NativeGuestStackSize, + StackSize = stackSize, }; } _attrStates[outAttrAddress] = threadState.Attributes; @@ -574,6 +589,11 @@ public static class KernelPthreadExtendedCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + private static bool IsValidStackBounds(ulong stackAddress, ulong stackSize) => + stackAddress != 0 && + stackSize != 0 && + stackAddress <= ulong.MaxValue - stackSize; + private static bool TryInferNativeGuestStack(ulong stackPointer, out ulong stackAddress) { stackAddress = 0; diff --git a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs index 5dfe103..c17f8c5 100644 --- a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs @@ -73,7 +73,7 @@ public static class KernelSemaphoreCompatExports Count = initialCount, }; - if (!TryWriteUInt32(ctx, semaphoreAddress, handle)) + if (!ctx.TryWriteUInt64(semaphoreAddress, handle)) { _semaphores.TryRemove(handle, out _); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); @@ -113,6 +113,27 @@ public static class KernelSemaphoreCompatExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } + return WaitSemaphoreCore( + ctx, + semaphore, + handle, + needCount, + hasTimeout: timeoutAddress != 0, + timeoutAddress, + timeoutUsec, + "sceKernelWaitSema"); + } + + private static int WaitSemaphoreCore( + CpuContext ctx, + KernelSemaphoreState semaphore, + uint handle, + int needCount, + bool hasTimeout, + ulong timeoutAddress, + uint timeoutUsec, + string operationName) + { lock (semaphore.Gate) { if (semaphore.Count >= needCount) @@ -125,7 +146,7 @@ public static class KernelSemaphoreCompatExports if (_traceSema) { - TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}"); + TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")}"); } return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } @@ -137,7 +158,7 @@ public static class KernelSemaphoreCompatExports // 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 + var deadline = hasTimeout ? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec)) : 0; @@ -187,7 +208,7 @@ public static class KernelSemaphoreCompatExports if (GuestThreadExecution.RequestCurrentThreadBlock( ctx, - "sceKernelWaitSema", + operationName, GetSemaphoreWakeKey(handle), ResumeWait, WakePredicate, @@ -216,14 +237,21 @@ public static class KernelSemaphoreCompatExports if (_traceSema) { - 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)}"); + TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}"); } return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } // 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); + return WaitSemaphoreOnHostThread( + ctx, + semaphore, + handle, + needCount, + hasTimeout, + timeoutAddress, + timeoutUsec); } private static int WaitSemaphoreOnHostThread( @@ -231,46 +259,62 @@ public static class KernelSemaphoreCompatExports KernelSemaphoreState semaphore, uint handle, int needCount, + bool hasTimeout, ulong timeoutAddress, uint timeoutUsec) { - var deadlineMs = timeoutAddress != 0 + var deadlineMs = hasTimeout ? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L) : long.MaxValue; - lock (semaphore.Gate) + if (_traceSema) { - if (_traceSema) + 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)}"); + $"count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")} {FormatCallSite(ctx)}"); } - while (semaphore.Count < needCount) + } + + while (true) + { + lock (semaphore.Gate) { + if (semaphore.Count >= needCount) + { + semaphore.Count -= needCount; + semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); + if (_traceSema) + { + 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); + } + var remaining = deadlineMs - Environment.TickCount64; - if (timeoutAddress != 0 && remaining <= 0) + if (hasTimeout && remaining <= 0) { semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); - _ = TryWriteUInt32(ctx, timeoutAddress, 0); + if (timeoutAddress != 0) + { + _ = TryWriteUInt32(ctx, timeoutAddress, 0); + } return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); } - Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100)); + var waitMilliseconds = hasTimeout + ? (int)Math.Max(1L, Math.Min(remaining, 10L)) + : 10; + Monitor.Wait(semaphore.Gate, waitMilliseconds); } - semaphore.Count -= needCount; - semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); - if (_traceSema) - { - 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); + GuestThreadExecution.Scheduler?.DeliverPendingGuestExceptionIfReady(ctx); } } @@ -328,7 +372,7 @@ public static class KernelSemaphoreCompatExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); } - if (signalCount <= 0) + if (signalCount < 0) { signalTrace.Record(SemaphoreSignalProfile.Stage.Rejected, result: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); @@ -532,6 +576,35 @@ public static class KernelSemaphoreCompatExports : result; } + [SysAbiExport( + Nid = "fjN6NQHhK8k", + ExportName = "scePthreadSemTimedwait", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int PthreadSemTimedWait(CpuContext ctx) + { + var timeoutUsec = unchecked((uint)ctx[CpuRegister.Rsi]); + if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + if (!_semaphores.TryGetValue(handle, out var semaphore)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); + } + + return WaitSemaphoreCore( + ctx, + semaphore, + handle, + needCount: 1, + hasTimeout: true, + timeoutAddress: 0, + timeoutUsec, + "scePthreadSemTimedwait"); + } + [SysAbiExport( Nid = "w5IHyvahg-o", ExportName = "sem_timedwait", diff --git a/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs index 1ce1b3b..17d81da 100644 --- a/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs @@ -10,23 +10,44 @@ namespace SharpEmu.Libs.Kernel; // Wake selects a fixed number of registered waiters at the same address. public static class KernelSyncOnAddressCompatExports { + private const int UmtxOperationWait = 2; + private const int UmtxOperationWake = 3; + private const int PosixEperm = 1; + private const int PosixEacces = 13; + private const int PosixEfault = 14; + private const int PosixEinval = 22; + private const int PosixEagain = 35; + private const int PosixEtimedout = 60; + private const int PosixEcanceled = 85; + private const int PosixEother = 1062; + private sealed class SyncWaiter : IGuestThreadBlockWaiter { private readonly CpuContext _context; private readonly ulong _expected; private readonly bool _is64Bit; + private readonly bool _usePosixResult; + private readonly long _deadlineTimestamp; private readonly object _hostGate = new(); private readonly long _registeredTicks; private readonly int _registeredManagedThread; private int _wakeRequested; private int _resumeRecorded; - public SyncWaiter(CpuContext context, ulong address, ulong expected, bool is64Bit) + public SyncWaiter( + CpuContext context, + ulong address, + ulong expected, + bool is64Bit, + bool usePosixResult, + long deadlineTimestamp) { _context = context; Address = address; _expected = expected; _is64Bit = is64Bit; + _usePosixResult = usePosixResult; + _deadlineTimestamp = deadlineTimestamp; _registeredTicks = KernelSyncOnAddressProfile.Enabled ? Stopwatch.GetTimestamp() : 0L; @@ -50,13 +71,13 @@ public static class KernelSyncOnAddressCompatExports if (Volatile.Read(ref _wakeRequested) != 0) { RecordResume(explicitWake: true, valueChanged: false, timedOut: false, faulted: false); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + return Complete((int)OrbisGen2Result.ORBIS_GEN2_OK); } if (!TryReadValue(_context, Address, _is64Bit, out var value)) { RecordResume(explicitWake: false, valueChanged: false, timedOut: false, faulted: true); - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + return Complete((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } var valueChanged = value != _expected; @@ -65,15 +86,21 @@ public static class KernelSyncOnAddressCompatExports valueChanged, timedOut: !valueChanged, faulted: false); - return valueChanged + var result = valueChanged ? (int)OrbisGen2Result.ORBIS_GEN2_OK : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; + return Complete(result); } public bool TryWake() => Volatile.Read(ref _wakeRequested) != 0; public bool RequestWake() { + if (_deadlineTimestamp != 0 && Stopwatch.GetTimestamp() >= _deadlineTimestamp) + { + return false; + } + if (Interlocked.Exchange(ref _wakeRequested, 1) != 0) { return false; @@ -87,42 +114,50 @@ public static class KernelSyncOnAddressCompatExports return true; } - public int WaitOnHost(TimeSpan? timeout) + public int WaitOnHost() { - if (Volatile.Read(ref _wakeRequested) == 0) + while (Volatile.Read(ref _wakeRequested) == 0) { - lock (_hostGate) + if (!TryReadValue(_context, Address, _is64Bit, out var current) || + current != _expected) { - if (timeout is null) - { - while (Volatile.Read(ref _wakeRequested) == 0) - { - Monitor.Wait(_hostGate); - } - } - else - { - var deadline = Stopwatch.GetTimestamp() + - (long)Math.Ceiling(timeout.Value.TotalSeconds * Stopwatch.Frequency); - while (Volatile.Read(ref _wakeRequested) == 0) - { - var remainingTicks = deadline - Stopwatch.GetTimestamp(); - if (remainingTicks <= 0) - { - break; - } + break; + } - var remaining = TimeSpan.FromSeconds( - remainingTicks / (double)Stopwatch.Frequency); - Monitor.Wait(_hostGate, remaining); - } + var waitSlice = TimeSpan.FromMilliseconds(10); + if (_deadlineTimestamp != 0) + { + var remainingTicks = _deadlineTimestamp - Stopwatch.GetTimestamp(); + if (remainingTicks <= 0) + { + break; + } + + var remainingSeconds = remainingTicks / (double)Stopwatch.Frequency; + if (remainingSeconds < waitSlice.TotalSeconds) + { + waitSlice = TimeSpan.FromSeconds(remainingSeconds); } } + + lock (_hostGate) + { + if (Volatile.Read(ref _wakeRequested) == 0) + { + Monitor.Wait(_hostGate, waitSlice); + } + } + + GuestThreadExecution.Scheduler?.DeliverPendingGuestExceptionIfReady(_context); } return Resume(); } + private int Complete(int result) => _usePosixResult + ? SetPosixReturn(_context, result) + : result; + private void RecordResume( bool explicitWake, bool valueChanged, @@ -171,6 +206,80 @@ public static class KernelSyncOnAddressCompatExports LibraryName = "libKernel")] public static int SyncOnAddressWait64(CpuContext ctx) => Wait(ctx, is64Bit: true); + [SysAbiExport( + Nid = "04AjkP0jO9U", + ExportName = "_umtx_op", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libKernel")] + public static int UmtxOp(CpuContext ctx) + { + var address = ctx[CpuRegister.Rdi]; + var operation = unchecked((int)ctx[CpuRegister.Rsi]); + var value = ctx[CpuRegister.Rdx]; + var secondaryAddress = ctx[CpuRegister.Rcx]; + var timeoutAddress = ctx[CpuRegister.R8]; + + if (secondaryAddress != 0) + { + return SetPosixReturn( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + switch (operation) + { + case UmtxOperationWait: + { + TimeSpan? timeout = null; + long deadline = 0; + if (timeoutAddress != 0) + { + if (!ctx.TryReadUInt64(timeoutAddress, out var secondsBits) || + !ctx.TryReadUInt64(timeoutAddress + sizeof(long), out var nanosecondsBits)) + { + return SetPosixReturn( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + + var seconds = unchecked((long)secondsBits); + var nanoseconds = unchecked((long)nanosecondsBits); + if (seconds < 0 || nanoseconds < 0 || nanoseconds >= 1_000_000_000L) + { + return SetPosixReturn( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + timeout = CreateTimespecDuration(seconds, nanoseconds); + deadline = GuestThreadExecution.ComputeDeadlineTimestamp(timeout.Value); + } + + return WaitCore( + ctx, + is64Bit: true, + address, + value, + timeout, + deadline, + usePosixResult: true, + reason: "_umtx_op.wait"); + } + + case UmtxOperationWake: + return Wake( + ctx, + address, + unchecked((int)value), + usePosixResult: true); + + default: + return SetPosixReturn( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + } + [SysAbiExport( Nid = "q2y-wDIVWZA", ExportName = "sceKernelSyncOnAddressWake", @@ -180,14 +289,29 @@ public static class KernelSyncOnAddressCompatExports { var address = ctx[CpuRegister.Rdi]; var requested = unchecked((int)ctx[CpuRegister.Rsi]); + return Wake(ctx, address, requested, usePosixResult: false); + } + + private static int Wake( + CpuContext ctx, + ulong address, + int requested, + bool usePosixResult) + { if (address == 0 || (address & 3) != 0 || requested < 0) { - return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + return CompleteResult( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, + usePosixResult); } if (requested == 0) { - return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + return CompleteResult( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_OK, + usePosixResult); } List? selected = null; @@ -259,7 +383,10 @@ public static class KernelSyncOnAddressCompatExports GuestThreadExecution.CurrentGuestThreadHandle, Environment.CurrentManagedThreadId); - return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + return CompleteResult( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_OK, + usePosixResult); } private static void CaptureCallStack( @@ -304,15 +431,15 @@ public static class KernelSyncOnAddressCompatExports private static int Wait(CpuContext ctx, bool is64Bit) { var address = ctx[CpuRegister.Rdi]; + var expected = is64Bit + ? ctx[CpuRegister.Rsi] + : unchecked((uint)ctx[CpuRegister.Rsi]); var alignmentMask = is64Bit ? 7UL : 3UL; if (address == 0 || (address & alignmentMask) != 0) { return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - var expected = is64Bit - ? ctx[CpuRegister.Rsi] - : unchecked((uint)ctx[CpuRegister.Rsi]); if (!TryReadValue(ctx, address, is64Bit, out var current)) { return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); @@ -334,28 +461,92 @@ public static class KernelSyncOnAddressCompatExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } - if (timeoutMicroseconds == 0) - { - return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); - } - timeout = TimeSpan.FromTicks((long)timeoutMicroseconds * 10L); deadline = GuestThreadExecution.ComputeDeadlineTimestamp(timeout.Value); } - var waiter = new SyncWaiter(ctx, address, expected, is64Bit); + return WaitCore( + ctx, + is64Bit, + address, + expected, + timeout, + deadline, + usePosixResult: false, + reason: is64Bit + ? "sceKernelSyncOnAddressWait64" + : "sceKernelSyncOnAddressWait32"); + } + + private static int WaitCore( + CpuContext ctx, + bool is64Bit, + ulong address, + ulong expected, + TimeSpan? timeout, + long deadline, + bool usePosixResult, + string reason) + { + var alignmentMask = is64Bit ? 7UL : 3UL; + if (address == 0 || (address & alignmentMask) != 0) + { + return CompleteResult( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, + usePosixResult); + } + + if (!TryReadValue(ctx, address, is64Bit, out var current)) + { + return CompleteResult( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, + usePosixResult); + } + + if (current != expected) + { + KernelSyncOnAddressProfile.RecordImmediate(address); + return CompleteResult( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_OK, + usePosixResult); + } + + if (timeout == TimeSpan.Zero) + { + return CompleteResult( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT, + usePosixResult); + } + + var waiter = new SyncWaiter( + ctx, + address, + expected, + is64Bit, + usePosixResult, + deadline); Register(address, waiter); if (!TryReadValue(ctx, address, is64Bit, out current)) { Unregister(waiter); - return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + return CompleteResult( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, + usePosixResult); } if (current != expected || waiter.TryWake()) { Unregister(waiter); KernelSyncOnAddressProfile.RecordImmediate(address); - return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + return CompleteResult( + ctx, + (int)OrbisGen2Result.ORBIS_GEN2_OK, + usePosixResult); } var returnRip = 0UL; @@ -383,7 +574,7 @@ public static class KernelSyncOnAddressCompatExports if (GuestThreadExecution.RequestCurrentThreadBlock( ctx, - is64Bit ? "sceKernelSyncOnAddressWait64" : "sceKernelSyncOnAddressWait32", + reason, waiter.WakeKey, waiter, deadline)) @@ -393,7 +584,24 @@ public static class KernelSyncOnAddressCompatExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } - return SetReturn(ctx, waiter.WaitOnHost(timeout)); + return SetReturn(ctx, waiter.WaitOnHost()); + } + + private static TimeSpan CreateTimespecDuration(long seconds, long nanoseconds) + { + var subsecondTicks = (nanoseconds + 99L) / 100L; + if (seconds > TimeSpan.MaxValue.Ticks / TimeSpan.TicksPerSecond) + { + return TimeSpan.MaxValue; + } + + var secondTicks = seconds * TimeSpan.TicksPerSecond; + if (secondTicks > TimeSpan.MaxValue.Ticks - subsecondTicks) + { + return TimeSpan.MaxValue; + } + + return TimeSpan.FromTicks(secondTicks + subsecondTicks); } private static void Register(ulong address, SyncWaiter waiter) @@ -458,4 +666,31 @@ public static class KernelSyncOnAddressCompatExports ctx[CpuRegister.Rax] = unchecked((ulong)(long)result); return result; } + + private static int CompleteResult(CpuContext ctx, int result, bool usePosixResult) => + usePosixResult + ? SetPosixReturn(ctx, result) + : SetReturn(ctx, result); + + private static int SetPosixReturn(CpuContext ctx, int result) + { + if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK) + { + return SetReturn(ctx, 0); + } + + var errno = result switch + { + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => PosixEperm, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED => PosixEacces, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => PosixEfault, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => PosixEinval, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN => PosixEagain, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT => PosixEtimedout, + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED => PosixEcanceled, + _ => PosixEother, + }; + _ = KernelRuntimeCompatExports.TrySetErrno(ctx, errno); + return SetReturn(ctx, -1); + } } diff --git a/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs b/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs index e886b5e..04a354a 100644 --- a/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs +++ b/src/SharpEmu.Libs/Np/NpWebApi2Exports.cs @@ -12,9 +12,27 @@ public static class NpWebApi2Exports private static int _initialized; private static int _nextLibraryContextHandle; private static int _nextPushEventHandle; + private static int _nextPushEventCallbackHandle; private static int _nextUserContextHandle = 1000; private static readonly object _contextGate = new(); private static readonly HashSet _libraryContexts = []; + private static readonly HashSet _userContexts = []; + private static readonly HashSet _pushEventFilters = []; + + public static void ResetRuntimeState() + { + Interlocked.Exchange(ref _initialized, 0); + Interlocked.Exchange(ref _nextLibraryContextHandle, 0); + Interlocked.Exchange(ref _nextPushEventHandle, 0); + Interlocked.Exchange(ref _nextPushEventCallbackHandle, 0); + Interlocked.Exchange(ref _nextUserContextHandle, 1000); + lock (_contextGate) + { + _libraryContexts.Clear(); + _userContexts.Clear(); + _pushEventFilters.Clear(); + } + } [SysAbiExport( Nid = "+o9816YQhqQ", @@ -51,6 +69,11 @@ public static class NpWebApi2Exports } var filterHandle = Interlocked.Increment(ref _nextPushEventHandle); + lock (_contextGate) + { + _pushEventFilters.Add(filterHandle); + } + TraceNpWebApi2("push-event-create-filter", libraryContextId, (ulong)filterHandle); return ctx.SetReturn(filterHandle); } @@ -74,6 +97,20 @@ public static class NpWebApi2Exports return ctx.SetReturn(handle); } + [SysAbiExport( + Nid = "fIATVMo4Y1w", + ExportName = "sceNpWebApi2PushEventDeleteHandle", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNpWebApi2")] + public static int NpWebApi2PushEventDeleteHandle(CpuContext ctx) + { + TraceNpWebApi2( + "push-event-delete-handle", + unchecked((int)ctx[CpuRegister.Rdi]), + unchecked((uint)ctx[CpuRegister.Rsi])); + return ctx.SetReturn(0); + } + [SysAbiExport( Nid = "sk54bi6FtYM", ExportName = "sceNpWebApi2CreateUserContext", @@ -97,9 +134,34 @@ public static class NpWebApi2Exports } var userContextId = Interlocked.Increment(ref _nextUserContextHandle); + lock (_contextGate) + { + _userContexts.Add(userContextId); + } + return ctx.SetReturn(userContextId); } + [SysAbiExport( + Nid = "fY3QqeNkF8k", + ExportName = "sceNpWebApi2PushEventRegisterCallback", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libSceNpWebApi2")] + public static int NpWebApi2PushEventRegisterCallback(CpuContext ctx) + { + var userContextId = unchecked((int)ctx[CpuRegister.Rdi]); + var filterId = unchecked((int)ctx[CpuRegister.Rsi]); + var callback = ctx[CpuRegister.Rdx]; + if (callback == 0 || !IsRegisteredPushEventPair(userContextId, filterId)) + { + return ctx.SetReturn(NpWebApi2ErrorInvalidArgument); + } + + var callbackId = Interlocked.Increment(ref _nextPushEventCallbackHandle); + TraceNpWebApi2("push-event-register-callback", userContextId, (ulong)callbackId); + return ctx.SetReturn(callbackId); + } + [SysAbiExport( Nid = "bEvXpcEk200", ExportName = "sceNpWebApi2Terminate", @@ -171,10 +233,25 @@ public static class NpWebApi2Exports if (_libraryContexts.Count == 0) { Interlocked.Exchange(ref _initialized, 0); + _userContexts.Clear(); + _pushEventFilters.Clear(); } } } + private static bool IsRegisteredPushEventPair(int userContextId, int filterId) + { + if (Volatile.Read(ref _initialized) == 0 || userContextId <= 0 || filterId <= 0) + { + return false; + } + + lock (_contextGate) + { + return _userContexts.Contains(userContextId) && _pushEventFilters.Contains(filterId); + } + } + private static void TraceNpWebApi2(string operation, int id, ulong arg0) { if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP_WEB_API2"), "1", StringComparison.Ordinal)) diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index 9656d52..2688f09 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -398,15 +398,18 @@ public static partial class VideoOutExports public static int VideoOutClose(CpuContext ctx) { var handle = unchecked((int)ctx[CpuRegister.Rdi]); - lock (_stateGate) + GuestGpu.Current.RunAfterPendingCommandStreams(() => { - foreach (var request in _flipRequests.Values.Where(request => request.Handle == handle).ToArray()) + lock (_stateGate) { - CancelFlipLocked(request); + foreach (var request in _flipRequests.Values.Where(request => request.Handle == handle).ToArray()) + { + CancelFlipLocked(request); + } + _ports.Remove(handle); + Monitor.PulseAll(_stateGate); } - _ports.Remove(handle); - Monitor.PulseAll(_stateGate); - } + }); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -1246,36 +1249,42 @@ public static partial class VideoOutExports { var handle = unchecked((int)ctx[CpuRegister.Rdi]); var attributeIndex = unchecked((int)ctx[CpuRegister.Rsi]); - if (!TryGetPort(handle, out var port)) - { - return OrbisVideoOutErrorInvalidHandle; - } - if (attributeIndex < 0) { return OrbisVideoOutErrorInvalidValue; } - lock (_stateGate) + var result = (int)OrbisGen2Result.ORBIS_GEN2_OK; + GuestGpu.Current.RunAfterPendingCommandStreams(() => { - if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null) + lock (_stateGate) { - return OrbisVideoOutErrorInvalidValue; - } - - port.Groups[attributeIndex] = null; - foreach (var slot in port.BufferSlots) - { - if (slot.GroupIndex == attributeIndex) + if (!_ports.TryGetValue(handle, out var port)) { - slot.GroupIndex = -1; - slot.AddressLeft = 0; - slot.AddressRight = 0; + result = OrbisVideoOutErrorInvalidHandle; + return; + } + + if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null) + { + result = OrbisVideoOutErrorInvalidValue; + return; + } + + port.Groups[attributeIndex] = null; + foreach (var slot in port.BufferSlots) + { + if (slot.GroupIndex == attributeIndex) + { + slot.GroupIndex = -1; + slot.AddressLeft = 0; + slot.AddressRight = 0; + } } } + }); - return (int)OrbisGen2Result.ORBIS_GEN2_OK; - } + return result; } [SysAbiExport( diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs index 194b952..36e741d 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs @@ -72,6 +72,26 @@ internal static unsafe partial class VulkanVideoPresenter ? testStream.Done() : IdleOutcome.Completed; + public static void RunAfterPendingCommandStreams(Action work) + { + ArgumentNullException.ThrowIfNull(work); + if (TryGetActivePresenter(out var presenter)) + { + if (presenter.Relay.TryRunAfterAcceptedCommandStreams(work)) + { + return; + } + + if (!HostSessionControl.IsShutdownRequested && !Volatile.Read(ref _closed) && + !Volatile.Read(ref _presenterCloseRequested)) + { + throw SubmissionScheduler.Fatal("The GPU worker rejected an ordered video-out state change."); + } + } + + work(); + } + // The blocked heads of the stream this memory submits to; null when no stream exists for it. internal static BlockedSnapshot? SnapshotBlockedCommandStream(ICpuMemory? memory) { diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Scheduler.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Scheduler.cs index 1f09f5c..dbdff71 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Scheduler.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Scheduler.cs @@ -22,7 +22,7 @@ internal static unsafe partial class VulkanVideoPresenter // This partial owns the submission scheduler and the GPU worker relay. private readonly object _queueGate = new(); - private readonly GpuWorkerRelay _relay = new(WakeRenderThread); + private readonly GpuWorkerRelay _relay; internal GpuWorkerRelay Relay => _relay; private readonly SubmissionContext _submissionContext = new(); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 8fd8978..c9a8d52 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -9,6 +9,7 @@ using SharpEmu.Libs.AvPlayer; using SharpEmu.Libs.Media; using SharpEmu.Libs.Gpu; using SharpEmu.Libs.Gpu.GpuCommands; +using SharpEmu.Libs.Gpu.Scheduling; using SharpEmu.Libs.Gpu.Images; using SharpEmu.ShaderCompiler; using SharpEmu.ShaderCompiler.Vulkan; @@ -255,6 +256,7 @@ internal static unsafe partial class VulkanVideoPresenter public Presenter(uint width, uint height) { _commandStream = new CommandStreamQueue(this); + _relay = new GpuWorkerRelay(WakeRenderThread, _commandStream.TryEnqueueControlBarrier); _hostBufferPool = new VulkanHostBufferPool( MaximumCachedHostBufferBytes, DestroyHostBufferAllocation); diff --git a/src/SharpEmu.ShaderCompiler/Ir/Gen5ExecFullAnalysis.cs b/src/SharpEmu.ShaderCompiler/Ir/Gen5ExecFullAnalysis.cs index 4328d1b..c37863e 100644 --- a/src/SharpEmu.ShaderCompiler/Ir/Gen5ExecFullAnalysis.cs +++ b/src/SharpEmu.ShaderCompiler/Ir/Gen5ExecFullAnalysis.cs @@ -158,12 +158,14 @@ public static class Gen5ExecFullAnalysis written |= Bit(sdst) | Bit(sdst + 1); } - if (opcode.StartsWith("VCmp", StringComparison.Ordinal) && scalarDestination is null) + if (opcode.StartsWith("VCmpx", StringComparison.Ordinal)) { - // VOPC compares write VCC, V_CMPX writes EXEC. - written |= opcode.StartsWith("VCmpx", StringComparison.Ordinal) - ? Bit(ExecLow) | Bit(ExecHigh) - : Bit(106) | Bit(107); + written |= Bit(ExecLow) | Bit(ExecHigh); + } + else if (opcode.StartsWith("VCmp", StringComparison.Ordinal) && scalarDestination is null) + { + // VOPC compares write VCC. + written |= Bit(106) | Bit(107); } if (opcode.StartsWith('V') && opcode.Contains("Co", StringComparison.Ordinal) && scalarDestination is null) diff --git a/tests/SharpEmu.Libs.Tests/Cpu/GuestThreadStackBoundsTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/GuestThreadStackBoundsTests.cs new file mode 100644 index 0000000..8fe77a6 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/GuestThreadStackBoundsTests.cs @@ -0,0 +1,51 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections; +using System.Reflection; +using SharpEmu.Core.Cpu.Native; +using SharpEmu.HLE; +using Xunit; + +namespace SharpEmu.Libs.Tests.Cpu; + +public sealed class GuestThreadStackBoundsTests +{ + [NativeX64Fact] + public void SchedulerReturnsAuthoritativeBoundsAcrossTheFullSlotWindow() + { + const BindingFlags privateInstance = BindingFlags.NonPublic | BindingFlags.Instance; + const ulong stackSize = 0x20_0000; + const ulong stackStride = 0x100_0000; + var highestStack = OperatingSystem.IsWindows() + ? 0x0000_7FFF_E000_0000UL + : 0x0000_6FFF_E000_0000UL; + var backendType = typeof(DirectExecutionBackend); + var threadType = backendType.GetNestedType("GuestThreadState", BindingFlags.NonPublic)!; + var modules = new ModuleManager(); + modules.Freeze(); + using var backend = new DirectExecutionBackend(modules); + var gate = backendType.GetField("_guestThreadGate", privateInstance)!.GetValue(backend)!; + var threads = (IDictionary)backendType.GetField("_guestThreads", privateInstance)!.GetValue(backend)!; + + foreach (var slot in new[] { 0, 48, 56, 1023 }) + { + var handle = 0x1000UL + (ulong)slot; + var expectedBase = highestStack - ((ulong)slot * stackStride); + var thread = Activator.CreateInstance(threadType)!; + threadType.GetProperty("ThreadHandle")!.SetValue(thread, handle); + threadType.GetProperty("StackBase")!.SetValue(thread, expectedBase); + threadType.GetProperty("StackSize")!.SetValue(thread, stackSize); + lock (gate) + threads.Add(handle, thread); + + Assert.True(backend.TryGetGuestThreadStackBounds(handle, out var actualBase, out var actualSize)); + Assert.Equal(expectedBase, actualBase); + Assert.Equal(stackSize, actualSize); + } + + Assert.False(backend.TryGetGuestThreadStackBounds(0xDEAD, out var missingBase, out var missingSize)); + Assert.Equal(0UL, missingBase); + Assert.Equal(0UL, missingSize); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs index 5e461d1..b942cbf 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/GpuCommands/CommandStreamQueueTests.cs @@ -154,6 +154,83 @@ public sealed class CommandStreamQueueTests Assert.Equal(new[] { "begin 0 1", "begin 1 3", "begin 0 2" }, host.Calls.Where(call => call.StartsWith("begin", StringComparison.Ordinal))); } + [Fact] + public void ControlBarrier_RunsBetweenTheAcceptedSliceAndLaterAdmissions() + { + var (host, queue) = NewQueue(); + Enqueue(host, queue, Graphics, 1, CreateInstanceCountPacket(1)); + var cancelled = false; + host.PendingCommands.Enqueue(() => + { + Assert.True(queue.TryEnqueueControlBarrier( + () => host.Calls.Add("mapping_change"), + () => cancelled = true)); + Enqueue(host, queue, Graphics + 0x100, 2, CreateInstanceCountPacket(2)); + }); + + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.DoesNotContain("mapping_change", host.Calls); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + + var first = host.Calls.IndexOf("begin 0 1"); + var barrier = host.Calls.IndexOf("mapping_change"); + var second = host.Calls.IndexOf("begin 0 2"); + Assert.True(first >= 0 && barrier > first && second > barrier, string.Join(", ", host.Calls)); + Assert.False(cancelled); + } + + [Fact] + public void ControlBarrier_PreservesCrossQueueProgressWhileAnOlderHeadIsBlocked() + { + var (host, queue) = NewQueue(); + host.WriteDword(Label, 0); + Enqueue(host, queue, Graphics, 1, WaitEqual(Label, 1)); + Assert.Equal(SliceResult.BlockedWithoutProgress, queue.ProcessOne()); + Assert.True(queue.TryEnqueueControlBarrier(() => host.Calls.Add("mapping_change"), () => { })); + EnqueueCompute(host, queue, 0x20, Compute, 2, CreateInstanceCountPacket(2)); + + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.DoesNotContain("mapping_change", host.Calls); + host.WriteDword(Label, 1); + queue.RetryBlocked(); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + Assert.Equal(SliceResult.Completed, queue.ProcessOne()); + + var compute = host.Calls.IndexOf("begin 1 2"); + var retriedGraphics = host.Calls.FindLastIndex(call => call == "begin 0 1"); + var barrier = host.Calls.IndexOf("mapping_change"); + Assert.True(compute >= 0 && retriedGraphics > compute && barrier > retriedGraphics, string.Join(", ", host.Calls)); + } + + [Fact] + public void ControlBarrier_IsCancelledWhenTheQueueIsDiscarded() + { + var (_, queue) = NewQueue(); + var ran = false; + var cancelled = false; + Assert.True(queue.TryEnqueueControlBarrier(() => ran = true, () => cancelled = true)); + + queue.DiscardAll(); + + Assert.False(ran); + Assert.True(cancelled); + Assert.False(queue.HasPending); + } + + [Fact] + public void ShutdownDrain_RunsAnAcceptedControlBarrier() + { + var (_, queue) = NewQueue(); + var ran = false; + Assert.True(queue.TryEnqueueControlBarrier(() => ran = true, () => { })); + queue.StopAccepting(); + + Assert.Equal(IdleOutcome.Completed, queue.DrainForShutdown(cancelBlockedOnNoProgress: true)); + Assert.True(ran); + Assert.False(queue.HasPending); + } + [Fact] public void BlockedHead_IsRequeuedAndRetriedWithoutStoppingOtherQueues() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs index 6f8a042..b0438dc 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Metadata.cs @@ -196,6 +196,41 @@ public sealed partial class GuestImageCacheTests harness.Shutdown(); } + [Fact] + public void ATextureRequestWithDcc_GivesAnImageFoundWithoutMetadataItsDccMetadata() + { + if (!GatePrerequisites.Ready(_vulkan)) return; + using var harness = new CacheHarness(_vulkan); + const ulong SliceSize = 0x1000; + var address = harness.MapBacked(0x20000, ReadWrite); + var metadata = address + 0x10000; + var plain = LinearRequest(address + 0xc000, 4, Format.R8G8B8A8Unorm, GuestPixelFormat.Bits8_8_8_8UNorm, GuestImageType.Color2D, new Extent3D(1, 1, 1), 1, 4, 1); + var imageId = harness.Find(ref plain); + var image = harness.Image(imageId); + image.Description.TileMode = GuestTileMode.RenderTarget; + Assert.Equal(MetadataKind.None, image.Description.Metadata.Kind); + harness.Write(metadata, Enumerable.Repeat((byte)0x55, (int)SliceSize).ToArray()); + harness.Worker.Run(() => + { + Assert.NotNull(harness.Cache.ObtainBuffer(metadata, SliceSize, isWritten: true).Buffer); + harness.Cache.FillBuffer(metadata, SliceSize, 0, isGds: false); + }); + Assert.True(harness.Cache.HasGpuDirtyBytes(metadata, SliceSize)); + + var compressed = plain; + compressed.Description.TileMode = GuestTileMode.RenderTarget; + compressed.Description.Metadata.Kind = MetadataKind.Dcc; + compressed.Description.Metadata.Range = new GuestSpan(metadata, 0); + Assert.Equal(imageId, harness.Find(ref compressed)); + Assert.Equal(MetadataKind.Dcc, image.Description.Metadata.Kind); + Assert.Equal(metadata, image.Description.Metadata.Range.Address); + Assert.Equal(SliceSize, image.Description.DccSliceSize); + Assert.False(harness.Cache.HasGpuDirtyBytes(metadata, SliceSize)); + Assert.True(harness.Images.TryReadGuestDccClear(metadata, SliceSize, 0, out _, out var code)); + Assert.Equal(0x00, code); + harness.Shutdown(); + } + [Fact] public void GuestDccClear_ReadsOnlyUniformClearSlices() { diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs index c7511b3..cfd8351 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs @@ -361,6 +361,38 @@ public sealed class ImageRequestBuildersTests : IClassFixture? tryEnqueueAfterPendingWork = null) { - Relay = new GpuWorkerRelay(Wake); + Relay = new GpuWorkerRelay(Wake, tryEnqueueAfterPendingWork); _thread = new Thread(Run) { IsBackground = true }; } @@ -160,6 +160,116 @@ public sealed class GpuWorkerRelayTests : IDisposable device.Complete(1); } + [Fact] + public void AfterPendingWork_BypassesTheCommandStreamBarrier() + { + var barrierCalled = false; + using var worker = new TestWorker( + (_, _) => + { + barrierCalled = true; + return true; + }); + worker.Start(); + var workRan = false; + var ranOnWorker = false; + + Assert.True(worker.Relay.TryRunAfterPendingWork(() => + { + workRan = true; + ranOnWorker = worker.Relay.IsGpuQueueThread; + })); + Assert.True(workRan); + Assert.True(ranOnWorker); + Assert.False(barrierCalled); + } + + [Fact] + public void AfterPendingWork_ReturnsFalseWhenTheWorkerIsClosed() + { + var barrierCalled = false; + var relay = new GpuWorkerRelay( + () => { }, + (_, _) => + { + barrierCalled = true; + return true; + }); + relay.StopAcceptingWork(); + + Assert.False(relay.TryRunAfterPendingWork(() => { })); + Assert.False(barrierCalled); + } + + [Fact] + public async Task AfterAcceptedCommandStreams_WaitsForTheControlBarrier() + { + Action? run = null; + Action? cancel = null; + var relay = new GpuWorkerRelay( + () => { }, + (work, cancellation) => + { + run = work; + cancel = cancellation; + return true; + }); + var workRan = false; + + var completion = Task.Run(() => + relay.TryRunAfterAcceptedCommandStreams(() => workRan = true)); + Assert.True(WaitUntil(() => Volatile.Read(ref run) is not null)); + Assert.False(await CompletesWithin(completion, 50)); + + run!(); + + Assert.True(await completion.WaitAsync(TimeSpan.FromSeconds(2))); + Assert.True(workRan); + Assert.NotNull(cancel); + } + + [Fact] + public async Task AfterAcceptedCommandStreams_CancellationReleasesTheCaller() + { + Action? cancel = null; + var relay = new GpuWorkerRelay( + () => { }, + (_, cancellation) => + { + cancel = cancellation; + return true; + }); + + var completion = Task.Run(() => Record.Exception(() => + relay.TryRunAfterAcceptedCommandStreams(() => { }))); + Assert.True(WaitUntil(() => Volatile.Read(ref cancel) is not null)); + cancel!(); + + Assert.IsType( + await completion.WaitAsync(TimeSpan.FromSeconds(2))); + } + + [Fact] + public async Task AfterAcceptedCommandStreams_RethrowsTheWorkException() + { + Action? run = null; + var relay = new GpuWorkerRelay( + () => { }, + (work, _) => + { + run = work; + return true; + }); + var expected = new InvalidOperationException("ordered state change failed"); + + var completion = Task.Run(() => Record.Exception(() => + relay.TryRunAfterAcceptedCommandStreams(() => throw expected))); + Assert.True(WaitUntil(() => Volatile.Read(ref run) is not null)); + run!(); + + Assert.Same(expected, await completion.WaitAsync(TimeSpan.FromSeconds(2))); + } + [Fact] public void CommandsRunBeforeGuestWork() { diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelHostWaitExceptionTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelHostWaitExceptionTests.cs new file mode 100644 index 0000000..da5218a --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelHostWaitExceptionTests.cs @@ -0,0 +1,135 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using System.Reflection; +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.Kernel; + +[Collection("GuestThreadFlowProfile")] +public sealed class KernelHostWaitExceptionTests +{ + private const ulong MemoryBase = 0x1_3000_0000; + private const ulong SemaphoreAddress = MemoryBase + 0x100; + private const ulong NameAddress = MemoryBase + 0x200; + private const ulong ValueAddress = MemoryBase + 0x300; + private const ulong TimeoutAddress = MemoryBase + 0x400; + + [Fact] + public void SemaphoreHostWaitDeliversPendingExceptionBeforeReturning() + { + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + var createContext = new CpuContext(memory, Generation.Gen5); + Assert.True(memory.TryWrite(NameAddress, "exception-sema\0"u8)); + createContext[CpuRegister.Rdi] = SemaphoreAddress; + createContext[CpuRegister.Rsi] = NameAddress; + createContext[CpuRegister.Rdx] = 1; + createContext[CpuRegister.Rcx] = 0; + createContext[CpuRegister.R8] = 1; + createContext[CpuRegister.R9] = 0; + Assert.Equal(0, KernelSemaphoreCompatExports.KernelCreateSema(createContext)); + Assert.True(createContext.TryReadUInt64(SemaphoreAddress, out var handle)); + + WriteUInt32(memory, TimeoutAddress, 1_000_000); + var waitContext = new CpuContext(memory, Generation.Gen5); + waitContext[CpuRegister.Rdi] = handle; + waitContext[CpuRegister.Rsi] = 1; + waitContext[CpuRegister.Rdx] = TimeoutAddress; + var signalContext = new CpuContext(memory, Generation.Gen5); + var scheduler = DispatchProxy.Create(); + var proxy = (PendingExceptionScheduler)(object)scheduler; + proxy.DeliverException = context => + { + Assert.Same(waitContext, context); + Assert.Equal( + 0, + KernelSemaphoreCompatExports.KernelSignalSema( + signalContext, + unchecked((uint)handle), + 1)); + }; + + var previousScheduler = GuestThreadExecution.Scheduler; + try + { + GuestThreadExecution.Scheduler = scheduler; + Assert.Equal(0, KernelSemaphoreCompatExports.KernelWaitSema(waitContext)); + Assert.Equal(1, proxy.DeliverCalls); + } + finally + { + GuestThreadExecution.Scheduler = previousScheduler; + createContext[CpuRegister.Rdi] = handle; + Assert.Equal(0, KernelSemaphoreCompatExports.KernelDeleteSema(createContext)); + } + } + + [Fact] + public void SyncOnAddressHostWaitDeliversPendingExceptionBeforeReturning() + { + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + WriteUInt32(memory, ValueAddress, 7); + WriteUInt32(memory, TimeoutAddress, 1_000_000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = ValueAddress; + context[CpuRegister.Rsi] = 7; + context[CpuRegister.Rdx] = TimeoutAddress; + var scheduler = DispatchProxy.Create(); + var proxy = (PendingExceptionScheduler)(object)scheduler; + proxy.DeliverException = deliveredContext => + { + Assert.Same(context, deliveredContext); + WriteUInt32(memory, ValueAddress, 8); + }; + + var previousScheduler = GuestThreadExecution.Scheduler; + try + { + GuestThreadExecution.Scheduler = scheduler; + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelSyncOnAddressCompatExports.SyncOnAddressWait32(context)); + Assert.Equal(1, proxy.DeliverCalls); + } + finally + { + GuestThreadExecution.Scheduler = previousScheduler; + } + } + + private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value) + { + Span bytes = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); + Assert.True(memory.TryWrite(address, bytes)); + } + + public class PendingExceptionScheduler : DispatchProxy + { + private int _deliverCalls; + + public Action? DeliverException { get; set; } + + public int DeliverCalls => Volatile.Read(ref _deliverCalls); + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + if (targetMethod?.Name == nameof(IGuestThreadScheduler.DeliverPendingGuestExceptionIfReady)) + { + Interlocked.Increment(ref _deliverCalls); + DeliverException!((CpuContext)args![0]!); + return null; + } + + if (targetMethod?.Name == nameof(IGuestThreadScheduler.WakeBlockedThreads)) + { + return 0; + } + + throw new NotSupportedException(targetMethod?.Name); + } + } +} diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelSemaphoreCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelSemaphoreCompatExportsTests.cs new file mode 100644 index 0000000..6377e0d --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelSemaphoreCompatExportsTests.cs @@ -0,0 +1,38 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.Kernel; + +public sealed class KernelSemaphoreCompatExportsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const ulong SemaphoreAddress = MemoryBase + 0x100; + private const ulong NameAddress = MemoryBase + 0x200; + + [Fact] + public void KernelCreateSemaWritesPointerSizedHandleAndClearsUpperBits() + { + var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5); + Assert.True(context.TryWriteUInt64(SemaphoreAddress, 0xC0DE_C0DE_CAFE_BA00)); + Assert.True(context.Memory.TryWrite(NameAddress, "unity-worker\0"u8)); + + context[CpuRegister.Rdi] = SemaphoreAddress; + context[CpuRegister.Rsi] = NameAddress; + context[CpuRegister.Rdx] = 1; + context[CpuRegister.Rcx] = 0; + context[CpuRegister.R8] = 1; + context[CpuRegister.R9] = 0; + + Assert.Equal(0, KernelSemaphoreCompatExports.KernelCreateSema(context)); + Assert.True(context.TryReadUInt64(SemaphoreAddress, out var handle)); + Assert.NotEqual(0UL, handle); + Assert.Equal(0UL, handle >> 32); + + context[CpuRegister.Rdi] = handle; + Assert.Equal(0, KernelSemaphoreCompatExports.KernelDeleteSema(context)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelSyncOnAddressCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelSyncOnAddressCompatExportsTests.cs index ec3224d..a1ba3a3 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/KernelSyncOnAddressCompatExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelSyncOnAddressCompatExportsTests.cs @@ -145,6 +145,166 @@ public sealed class KernelSyncOnAddressCompatExportsTests waiter.Resume()); } + [Fact] + public void UmtxWaitReturnsImmediatelyWhenValueDoesNotMatch() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 7); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 9; + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = 0; + + RunAsGuest(0x809, () => Assert.Equal( + 0, + KernelSyncOnAddressCompatExports.UmtxOp(ctx))); + + Assert.False(GuestThreadExecution.TryConsumeCurrentThreadBlock(out _)); + Assert.Equal(0UL, ctx[CpuRegister.Rax]); + } + + [Fact] + public void UmtxWaitAndWakeShareTheAddressWaitRegistry() + { + var (memory, waitContext) = CreateContext(); + var wakeContext = new CpuContext(memory, Generation.Gen5); + WriteUInt64(memory, ValueAddress, 0); + + IGuestThreadBlockWaiter? stagedWaiter = null; + waitContext[CpuRegister.Rdi] = ValueAddress; + waitContext[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + waitContext[CpuRegister.Rdx] = 0; + waitContext[CpuRegister.Rcx] = 0; + waitContext[CpuRegister.R8] = 0; + RunAsGuest(0x80A, () => + { + Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(waitContext)); + Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock( + out var reason, + out _, + out var hasContinuation, + out _, + out var waiter, + out var deadline)); + Assert.Equal("_umtx_op.wait", reason); + Assert.True(hasContinuation); + Assert.Equal(0, deadline); + stagedWaiter = Assert.IsAssignableFrom(waiter); + }); + + wakeContext[CpuRegister.Rdi] = ValueAddress; + wakeContext[CpuRegister.Rsi] = 3; // UMTX_OP_WAKE + wakeContext[CpuRegister.Rdx] = 1; + wakeContext[CpuRegister.Rcx] = 0; + wakeContext[CpuRegister.R8] = 0; + Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(wakeContext)); + + var resumed = Assert.IsAssignableFrom(stagedWaiter); + Assert.True(resumed.TryWake()); + Assert.Equal(0, resumed.Resume()); + } + + [Fact] + public void UmtxZeroTimespecReturnsMinusOneAndTimedOutErrno() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 12); + WriteUInt64(memory, TimeoutAddress, 0); + WriteUInt64(memory, TimeoutAddress + sizeof(long), 0); + SetErrnoStorage(ctx); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 12; + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = TimeoutAddress; + + Assert.Equal(-1, KernelSyncOnAddressCompatExports.UmtxOp(ctx)); + Assert.Equal(ulong.MaxValue, ctx[CpuRegister.Rax]); + Assert.Equal(60, ReadInt32(memory, ctx.FsBase + 0x40)); // ETIMEDOUT + Assert.False(GuestThreadExecution.TryConsumeCurrentThreadBlock(out _)); + } + + [Fact] + public void UmtxRejectsInvalidTimespecWithEinval() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 12); + WriteUInt64(memory, TimeoutAddress, 0); + WriteUInt64(memory, TimeoutAddress + sizeof(long), 1_000_000_000); + SetErrnoStorage(ctx); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 12; + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = TimeoutAddress; + + Assert.Equal(-1, KernelSyncOnAddressCompatExports.UmtxOp(ctx)); + Assert.Equal(22, ReadInt32(memory, ctx.FsBase + 0x40)); // EINVAL + } + + [Fact] + public void UmtxLargeTimespecSaturatesWithoutOverflow() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 1); + WriteUInt64(memory, TimeoutAddress, unchecked((ulong)long.MaxValue)); + WriteUInt64(memory, TimeoutAddress + sizeof(long), 999_999_999); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 2; // Already changed, so no host wait. + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = TimeoutAddress; + + Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(ctx)); + } + + [Fact] + public void UmtxTimedContinuationReturnsPosixTimeout() + { + var (memory, ctx) = CreateContext(); + WriteUInt64(memory, ValueAddress, 21); + WriteUInt64(memory, TimeoutAddress, 0); + WriteUInt64(memory, TimeoutAddress + sizeof(long), 1_000_000); // 1 ms + SetErrnoStorage(ctx); + ctx[CpuRegister.Rdi] = ValueAddress; + ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT + ctx[CpuRegister.Rdx] = 21; + ctx[CpuRegister.Rcx] = 0; + ctx[CpuRegister.R8] = TimeoutAddress; + + IGuestThreadBlockWaiter? stagedWaiter = null; + RunAsGuest(0x80B, () => + { + Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(ctx)); + Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock( + out _, + out _, + out _, + out _, + out var waiter, + out var deadline)); + Assert.True(deadline > 0); + stagedWaiter = Assert.IsAssignableFrom(waiter); + }); + + Thread.Sleep(10); + var resumed = Assert.IsAssignableFrom(stagedWaiter); + Assert.Equal(-1, resumed.Resume()); + Assert.Equal(60, ReadInt32(memory, ctx.FsBase + 0x40)); // ETIMEDOUT + } + + [Fact] + public void UmtxExportIsRegisteredForGen5() + { + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + + Assert.True(manager.TryGetExport("04AjkP0jO9U", out var export)); + Assert.Equal("_umtx_op", export.Name); + Assert.Equal("libKernel", export.LibraryName); + } + private static (FakeCpuMemory Memory, CpuContext Context) CreateContext() { var memory = new FakeCpuMemory(BaseAddress, 0x1000); @@ -213,4 +373,16 @@ public sealed class KernelSyncOnAddressCompatExportsTests BinaryPrimitives.WriteUInt64LittleEndian(bytes, value); Assert.True(memory.TryWrite(address, bytes)); } + + private static void SetErrnoStorage(CpuContext ctx) + { + ctx.FsBase = BaseAddress + 0x700; + } + + private static int ReadInt32(FakeCpuMemory memory, ulong address) + { + Span bytes = stackalloc byte[sizeof(int)]; + Assert.True(memory.TryRead(address, bytes)); + return BinaryPrimitives.ReadInt32LittleEndian(bytes); + } } diff --git a/tests/SharpEmu.Libs.Tests/Kernel/SemaphoreSignalProfileTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/SemaphoreSignalProfileTests.cs index c92fada..b183830 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/SemaphoreSignalProfileTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/SemaphoreSignalProfileTests.cs @@ -41,7 +41,8 @@ public sealed class SemaphoreSignalProfileTests [Theory] [InlineData(0, 2, true)] - [InlineData(0, 0, false)] + [InlineData(0, 0, true)] + [InlineData(1, -1, false)] [InlineData(1, int.MaxValue, false)] public void SignalKeepsTokenSemanticsAndRecordsStagesWhenEnabled(uint initial, int count, bool succeeds) { diff --git a/tests/SharpEmu.Libs.Tests/Np/NpWebApi2ExportsTests.cs b/tests/SharpEmu.Libs.Tests/Np/NpWebApi2ExportsTests.cs new file mode 100644 index 0000000..e69ea9e --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Np/NpWebApi2ExportsTests.cs @@ -0,0 +1,78 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Np; +using Xunit; + +namespace SharpEmu.Libs.Tests.Np; + +public sealed class NpWebApi2ExportsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const int InvalidArgument = unchecked((int)0x80553402); + + [Fact] + public void PushEventRegisterCallback_ValidatesStateAndReturnsCallbackId() + { + var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5); + NpWebApi2Exports.ResetRuntimeState(); + try + { + context[CpuRegister.Rdi] = 1; + context[CpuRegister.Rsi] = 0x1000; + var libraryContextId = NpWebApi2Exports.NpWebApi2Initialize(context); + Assert.True(libraryContextId > 0); + + context[CpuRegister.Rdi] = unchecked((ulong)libraryContextId); + context[CpuRegister.Rsi] = 0x1000_0000; + var userContextId = NpWebApi2Exports.NpWebApi2CreateUserContext(context); + Assert.True(userContextId > 0); + + context[CpuRegister.Rdi] = unchecked((ulong)libraryContextId); + var filterId = NpWebApi2Exports.NpWebApi2PushEventCreateFilter(context); + Assert.True(filterId > 0); + + context[CpuRegister.Rdi] = unchecked((ulong)userContextId); + context[CpuRegister.Rsi] = unchecked((ulong)filterId); + context[CpuRegister.Rdx] = 0x8000_1000; + context[CpuRegister.Rcx] = 0x1234; + Assert.True(NpWebApi2Exports.NpWebApi2PushEventRegisterCallback(context) > 0); + + context[CpuRegister.Rdx] = 0; + Assert.Equal( + InvalidArgument, + NpWebApi2Exports.NpWebApi2PushEventRegisterCallback(context)); + } + finally + { + NpWebApi2Exports.ResetRuntimeState(); + } + } + + [Fact] + public void PushEventRegisterCallback_RegistersForGen5() + { + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + + Assert.True(manager.TryGetExport("fY3QqeNkF8k", out var export)); + Assert.Equal("sceNpWebApi2PushEventRegisterCallback", export.Name); + Assert.Equal("libSceNpWebApi2", export.LibraryName); + } + + [Fact] + public void PushEventDeleteHandle_IsIdempotentAndRegisteredForGen5() + { + var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5); + context[CpuRegister.Rdi] = 1; + context[CpuRegister.Rsi] = 1; + Assert.Equal(0, NpWebApi2Exports.NpWebApi2PushEventDeleteHandle(context)); + + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + Assert.True(manager.TryGetExport("fIATVMo4Y1w", out var export)); + Assert.Equal("sceNpWebApi2PushEventDeleteHandle", export.Name); + Assert.Equal("libSceNpWebApi2", export.LibraryName); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs b/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs index 5dfab21..c3bf2e0 100644 --- a/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Pthread/PthreadSemaphoreSemanticsTests.cs @@ -17,6 +17,7 @@ public sealed class PthreadSemaphoreSemanticsTests [InlineData("C36iRE0F5sE", "scePthreadSemWait")] [InlineData("aishVAiFaYM", "scePthreadSemPost")] [InlineData("H2a+IN9TP0E", "scePthreadSemTrywait")] + [InlineData("fjN6NQHhK8k", "scePthreadSemTimedwait")] [InlineData("GEnUkDZoUwY", "scePthreadSemInit")] [InlineData("Vwc+L05e6oE", "scePthreadSemDestroy")] public void RegistryResolvesPthreadSemaphoreExports(string nid, string exportName) @@ -68,6 +69,41 @@ public sealed class PthreadSemaphoreSemanticsTests DestroySemaphore(context); } + [Fact] + public void PthreadSemTimedWaitTreatsSecondArgumentAsRelativeMicroseconds() + { + var context = CreateContext(); + InitializeSemaphore(context, initialCount: 1); + + context[CpuRegister.Rdi] = SemaphoreAddress; + context[CpuRegister.Rsi] = 250_000; + + Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemTimedWait(context)); + Assert.Equal(0UL, context[CpuRegister.Rax]); + Assert.Equal(0, ReadSemaphoreValue(context)); + + DestroySemaphore(context); + } + + [Fact] + public void PthreadSemTimedWaitWithZeroTimeoutReturnsTimedOutWhenEmpty() + { + var context = CreateContext(); + InitializeSemaphore(context, initialCount: 0); + + context[CpuRegister.Rdi] = SemaphoreAddress; + context[CpuRegister.Rsi] = 0; + + Assert.Equal( + (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT, + KernelSemaphoreCompatExports.PthreadSemTimedWait(context)); + Assert.Equal( + unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT), + context[CpuRegister.Rax]); + + DestroySemaphore(context); + } + [Fact] public void PthreadSemInitRejectsNonPrivateFlag() { diff --git a/tests/SharpEmu.Libs.Tests/Pthread/PthreadStackBoundsTests.cs b/tests/SharpEmu.Libs.Tests/Pthread/PthreadStackBoundsTests.cs new file mode 100644 index 0000000..2399637 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Pthread/PthreadStackBoundsTests.cs @@ -0,0 +1,138 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Reflection; +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.Pthread; + +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class PthreadStackBoundsCollection +{ + public const string Name = "PthreadStackBounds"; +} + +[Collection(PthreadStackBoundsCollection.Name)] +public sealed class PthreadStackBoundsTests : IDisposable +{ + private const ulong MemoryBase = 0x6_2000_0000; + private const ulong ThreadHandle = 0x1234_5000; + private const ulong AttrAddress = MemoryBase + 0x100; + private const ulong OutStackAddress = MemoryBase + 0x200; + private const ulong OutStackSizeAddress = MemoryBase + 0x208; + private readonly IGuestThreadScheduler? _previousScheduler = GuestThreadExecution.Scheduler; + + [Fact] + public void PosixGetAttrUsesSchedulerBoundsForRemoteHighSlotThread() + { + const ulong stackBase = 0x0000_7FFF_A800_0000; + const ulong stackSize = 0x20_0000; + var scheduler = CreateScheduler(ThreadHandle, stackBase, stackSize); + GuestThreadExecution.Scheduler = scheduler; + + var context = CreateContext(); + context[CpuRegister.Rsp] = stackBase + stackSize - 0xEA0; + context[CpuRegister.Rdi] = ThreadHandle; + context[CpuRegister.Rsi] = AttrAddress; + + Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGetNpPOSIX(context)); + AssertStackBounds(context, stackBase, stackSize); + } + + [Fact] + public void CurrentPrimaryThreadRetainsBoundedStackPointerFallback() + { + const ulong primaryThreadHandle = ThreadHandle + 1; + var stackBase = OperatingSystem.IsWindows() + ? 0x0000_7FFF_F000_0000UL + : 0x0000_6FFF_F000_0000UL; + GuestThreadExecution.Scheduler = CreateScheduler(); + var previousThread = GuestThreadExecution.EnterGuestThread(primaryThreadHandle); + try + { + var context = CreateContext(); + context[CpuRegister.Rsp] = stackBase + 0x1F_F160; + context[CpuRegister.Rdi] = primaryThreadHandle; + context[CpuRegister.Rsi] = AttrAddress + 0x20; + + Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGet(context)); + AssertStackBounds(context, stackBase, 0x20_0000, AttrAddress + 0x20); + } + finally + { + GuestThreadExecution.RestoreGuestThread(previousThread); + } + } + + [Fact] + public void InvalidSchedulerBoundsDoNotReplaceDefaultAttributes() + { + const ulong invalidThreadHandle = ThreadHandle + 2; + GuestThreadExecution.Scheduler = CreateScheduler(invalidThreadHandle, 0, 0x20_0000); + var context = CreateContext(); + context[CpuRegister.Rdi] = invalidThreadHandle; + context[CpuRegister.Rsi] = AttrAddress + 0x40; + + Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGet(context)); + AssertStackBounds(context, 0, 0x10_0000, AttrAddress + 0x40); + } + + public void Dispose() + { + GuestThreadExecution.Scheduler = _previousScheduler; + } + + private static CpuContext CreateContext() => + new(new FakeCpuMemory(MemoryBase, 0x4000), Generation.Gen5); + + private static IGuestThreadScheduler CreateScheduler( + ulong threadHandle = 0, + ulong stackBase = 0, + ulong stackSize = 0) + { + var scheduler = DispatchProxy.Create(); + var proxy = (StackBoundsSchedulerProxy)(object)scheduler; + if (threadHandle != 0) + { + proxy.Bounds[threadHandle] = (stackBase, stackSize); + } + + return scheduler; + } + + private static void AssertStackBounds( + CpuContext context, + ulong expectedBase, + ulong expectedSize, + ulong attrAddress = AttrAddress) + { + context[CpuRegister.Rdi] = attrAddress; + context[CpuRegister.Rsi] = OutStackAddress; + context[CpuRegister.Rdx] = OutStackSizeAddress; + Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGetstackPOSIX(context)); + Assert.True(context.TryReadUInt64(OutStackAddress, out var actualBase)); + Assert.True(context.TryReadUInt64(OutStackSizeAddress, out var actualSize)); + Assert.Equal(expectedBase, actualBase); + Assert.Equal(expectedSize, actualSize); + } + + public class StackBoundsSchedulerProxy : DispatchProxy + { + public Dictionary Bounds { get; } = []; + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + if (targetMethod?.Name == nameof(IGuestThreadScheduler.TryGetGuestThreadStackBounds)) + { + var found = Bounds.TryGetValue((ulong)args![0]!, out var bounds); + args[1] = found ? bounds.Base : 0UL; + args[2] = found ? bounds.Size : 0UL; + return found; + } + + throw new NotSupportedException(targetMethod?.Name); + } + } +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ExecFullAnalysisTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ExecFullAnalysisTests.cs index a276960..caffd90 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ExecFullAnalysisTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ExecFullAnalysisTests.cs @@ -66,6 +66,31 @@ public sealed class Gen5ExecFullAnalysisTests Assert.DoesNotContain(after, full); } + [Fact] + public void AfterAnSdwaVectorCompareIntoExec_ExecIsUnknown() + { + var before = VectorAdd(); + var pc = _pc; + _program.Add(new Gen5ShaderInstruction( + pc, + Gen5ShaderEncoding.Vopc, + "VCmpxNltF32", + [0u], + [Gen5Operand.Vector(0), Gen5Operand.Vector(1)], + [], + new Gen5SdwaControl(0, 0, 6, 6, false, false, 0, 0, 0, false, 106))); + _pc += 4; + var save = Scalar("SMovB64", 22, Gen5Operand.Scalar(Exec)); + VectorAdd(); + Scalar("SMovB64", Exec, Gen5Operand.Scalar(22)); + var after = VectorAdd(); + + var full = Analyze(); + Assert.Contains(before, full); + Assert.DoesNotContain(save, full); + Assert.DoesNotContain(after, full); + } + [Fact] public void RestoringASavedFullExec_MakesItFullAgain() {