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>
This commit is contained in:
parent
a17aaca533
commit
2df6f5b0a0
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,21 @@ public interface IGuestThreadScheduler
|
|||
/// </summary>
|
||||
void RegisterGuestThreadContext(ulong threadHandle, CpuContext context);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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<CommandSubmission>[] _queues = new LinkedList<CommandSubmission>[QueueCount];
|
||||
private readonly GpuCommandInterpreter?[] _interpreters = new GpuCommandInterpreter?[QueueCount];
|
||||
private readonly Queue<ControlBarrier> _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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
|
||||
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,16 +14,60 @@ public sealed class GpuWorkerRelay : IGpuQueueRelay
|
|||
private readonly object _gate = new();
|
||||
private readonly Queue<Action> _commands = new();
|
||||
private readonly Action _wake;
|
||||
private readonly Func<Action, Action, bool>? _tryEnqueueAfterPendingWork;
|
||||
private int _pendingCount;
|
||||
private bool _accepting = true;
|
||||
|
||||
public GpuWorkerRelay(Action wake)
|
||||
public GpuWorkerRelay(Action wake, Func<Action, Action, bool>? 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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<SyncWaiter>? 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<int> _libraryContexts = [];
|
||||
private static readonly HashSet<int> _userContexts = [];
|
||||
private static readonly HashSet<int> _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))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -361,6 +361,38 @@ public sealed class ImageRequestBuildersTests : IClassFixture<HeadlessVulkanFixt
|
|||
Assert.False(value.StencilClearEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DepthTarget_StencilIsCompressedOnlyWithHtileBacking()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
const ulong stencilBase = Base + 0x80000;
|
||||
const ulong htileBase = Base + 0x100000;
|
||||
var words = RegisterWords.Depth(Base, 64, 64, stencilBase: stencilBase) with
|
||||
{
|
||||
ZInfo = (uint)GuestDepthFormat.Z32Float | (1u << 29),
|
||||
StencilInfo = 0x00100981,
|
||||
HtileBase = htileBase,
|
||||
};
|
||||
|
||||
var resolution = ImageRequestBuilders.DepthTarget(words, _vulkan.DeviceInfo);
|
||||
|
||||
Assert.NotNull(resolution);
|
||||
var value = resolution.Value;
|
||||
Assert.True(value.HasStencil);
|
||||
Assert.True(value.HasHtile);
|
||||
Assert.Equal(MetadataKind.Htile, value.Request.Description.Metadata.Kind);
|
||||
Assert.Equal(htileBase, value.Request.Description.Metadata.Range.Address);
|
||||
Assert.True(value.Request.Description.Metadata.StencilCompressed);
|
||||
|
||||
var missingHtile = words with { ZInfo = (uint)GuestDepthFormat.Z32Float, HtileBase = 0 };
|
||||
var uncompressed = ImageRequestBuilders.DepthTarget(missingHtile, _vulkan.DeviceInfo);
|
||||
|
||||
Assert.NotNull(uncompressed);
|
||||
Assert.True(uncompressed.Value.HasStencil);
|
||||
Assert.False(uncompressed.Value.HasHtile);
|
||||
Assert.False(uncompressed.Value.Request.Description.Metadata.StencilCompressed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DepthTarget_NoAttachmentWhenNothingIsActive()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ public sealed class GpuWorkerRelayTests : IDisposable
|
|||
private readonly Thread _thread;
|
||||
private bool _stop;
|
||||
|
||||
public TestWorker()
|
||||
public TestWorker(Func<Action, Action, bool>? 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<OperationCanceledException>(
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Reflection;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Kernel;
|
||||
|
||||
[Collection("GuestThreadFlowProfile")]
|
||||
public sealed class KernelHostWaitExceptionTests
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_3000_0000;
|
||||
private const ulong SemaphoreAddress = MemoryBase + 0x100;
|
||||
private const ulong NameAddress = MemoryBase + 0x200;
|
||||
private const ulong ValueAddress = MemoryBase + 0x300;
|
||||
private const ulong TimeoutAddress = MemoryBase + 0x400;
|
||||
|
||||
[Fact]
|
||||
public void SemaphoreHostWaitDeliversPendingExceptionBeforeReturning()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||
var createContext = new CpuContext(memory, Generation.Gen5);
|
||||
Assert.True(memory.TryWrite(NameAddress, "exception-sema\0"u8));
|
||||
createContext[CpuRegister.Rdi] = SemaphoreAddress;
|
||||
createContext[CpuRegister.Rsi] = NameAddress;
|
||||
createContext[CpuRegister.Rdx] = 1;
|
||||
createContext[CpuRegister.Rcx] = 0;
|
||||
createContext[CpuRegister.R8] = 1;
|
||||
createContext[CpuRegister.R9] = 0;
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelCreateSema(createContext));
|
||||
Assert.True(createContext.TryReadUInt64(SemaphoreAddress, out var handle));
|
||||
|
||||
WriteUInt32(memory, TimeoutAddress, 1_000_000);
|
||||
var waitContext = new CpuContext(memory, Generation.Gen5);
|
||||
waitContext[CpuRegister.Rdi] = handle;
|
||||
waitContext[CpuRegister.Rsi] = 1;
|
||||
waitContext[CpuRegister.Rdx] = TimeoutAddress;
|
||||
var signalContext = new CpuContext(memory, Generation.Gen5);
|
||||
var scheduler = DispatchProxy.Create<IGuestThreadScheduler, PendingExceptionScheduler>();
|
||||
var proxy = (PendingExceptionScheduler)(object)scheduler;
|
||||
proxy.DeliverException = context =>
|
||||
{
|
||||
Assert.Same(waitContext, context);
|
||||
Assert.Equal(
|
||||
0,
|
||||
KernelSemaphoreCompatExports.KernelSignalSema(
|
||||
signalContext,
|
||||
unchecked((uint)handle),
|
||||
1));
|
||||
};
|
||||
|
||||
var previousScheduler = GuestThreadExecution.Scheduler;
|
||||
try
|
||||
{
|
||||
GuestThreadExecution.Scheduler = scheduler;
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelWaitSema(waitContext));
|
||||
Assert.Equal(1, proxy.DeliverCalls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadExecution.Scheduler = previousScheduler;
|
||||
createContext[CpuRegister.Rdi] = handle;
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelDeleteSema(createContext));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SyncOnAddressHostWaitDeliversPendingExceptionBeforeReturning()
|
||||
{
|
||||
var memory = new FakeCpuMemory(MemoryBase, 0x1000);
|
||||
WriteUInt32(memory, ValueAddress, 7);
|
||||
WriteUInt32(memory, TimeoutAddress, 1_000_000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = ValueAddress;
|
||||
context[CpuRegister.Rsi] = 7;
|
||||
context[CpuRegister.Rdx] = TimeoutAddress;
|
||||
var scheduler = DispatchProxy.Create<IGuestThreadScheduler, PendingExceptionScheduler>();
|
||||
var proxy = (PendingExceptionScheduler)(object)scheduler;
|
||||
proxy.DeliverException = deliveredContext =>
|
||||
{
|
||||
Assert.Same(context, deliveredContext);
|
||||
WriteUInt32(memory, ValueAddress, 8);
|
||||
};
|
||||
|
||||
var previousScheduler = GuestThreadExecution.Scheduler;
|
||||
try
|
||||
{
|
||||
GuestThreadExecution.Scheduler = scheduler;
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
KernelSyncOnAddressCompatExports.SyncOnAddressWait32(context));
|
||||
Assert.Equal(1, proxy.DeliverCalls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadExecution.Scheduler = previousScheduler;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
Assert.True(memory.TryWrite(address, bytes));
|
||||
}
|
||||
|
||||
public class PendingExceptionScheduler : DispatchProxy
|
||||
{
|
||||
private int _deliverCalls;
|
||||
|
||||
public Action<CpuContext>? DeliverException { get; set; }
|
||||
|
||||
public int DeliverCalls => Volatile.Read(ref _deliverCalls);
|
||||
|
||||
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
|
||||
{
|
||||
if (targetMethod?.Name == nameof(IGuestThreadScheduler.DeliverPendingGuestExceptionIfReady))
|
||||
{
|
||||
Interlocked.Increment(ref _deliverCalls);
|
||||
DeliverException!((CpuContext)args![0]!);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (targetMethod?.Name == nameof(IGuestThreadScheduler.WakeBlockedThreads))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw new NotSupportedException(targetMethod?.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Kernel;
|
||||
|
||||
public sealed class KernelSemaphoreCompatExportsTests
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private const ulong SemaphoreAddress = MemoryBase + 0x100;
|
||||
private const ulong NameAddress = MemoryBase + 0x200;
|
||||
|
||||
[Fact]
|
||||
public void KernelCreateSemaWritesPointerSizedHandleAndClearsUpperBits()
|
||||
{
|
||||
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
||||
Assert.True(context.TryWriteUInt64(SemaphoreAddress, 0xC0DE_C0DE_CAFE_BA00));
|
||||
Assert.True(context.Memory.TryWrite(NameAddress, "unity-worker\0"u8));
|
||||
|
||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
||||
context[CpuRegister.Rsi] = NameAddress;
|
||||
context[CpuRegister.Rdx] = 1;
|
||||
context[CpuRegister.Rcx] = 0;
|
||||
context[CpuRegister.R8] = 1;
|
||||
context[CpuRegister.R9] = 0;
|
||||
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelCreateSema(context));
|
||||
Assert.True(context.TryReadUInt64(SemaphoreAddress, out var handle));
|
||||
Assert.NotEqual(0UL, handle);
|
||||
Assert.Equal(0UL, handle >> 32);
|
||||
|
||||
context[CpuRegister.Rdi] = handle;
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.KernelDeleteSema(context));
|
||||
}
|
||||
}
|
||||
|
|
@ -145,6 +145,166 @@ public sealed class KernelSyncOnAddressCompatExportsTests
|
|||
waiter.Resume());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxWaitReturnsImmediatelyWhenValueDoesNotMatch()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 7);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 9;
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = 0;
|
||||
|
||||
RunAsGuest(0x809, () => Assert.Equal(
|
||||
0,
|
||||
KernelSyncOnAddressCompatExports.UmtxOp(ctx)));
|
||||
|
||||
Assert.False(GuestThreadExecution.TryConsumeCurrentThreadBlock(out _));
|
||||
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxWaitAndWakeShareTheAddressWaitRegistry()
|
||||
{
|
||||
var (memory, waitContext) = CreateContext();
|
||||
var wakeContext = new CpuContext(memory, Generation.Gen5);
|
||||
WriteUInt64(memory, ValueAddress, 0);
|
||||
|
||||
IGuestThreadBlockWaiter? stagedWaiter = null;
|
||||
waitContext[CpuRegister.Rdi] = ValueAddress;
|
||||
waitContext[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
waitContext[CpuRegister.Rdx] = 0;
|
||||
waitContext[CpuRegister.Rcx] = 0;
|
||||
waitContext[CpuRegister.R8] = 0;
|
||||
RunAsGuest(0x80A, () =>
|
||||
{
|
||||
Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(waitContext));
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock(
|
||||
out var reason,
|
||||
out _,
|
||||
out var hasContinuation,
|
||||
out _,
|
||||
out var waiter,
|
||||
out var deadline));
|
||||
Assert.Equal("_umtx_op.wait", reason);
|
||||
Assert.True(hasContinuation);
|
||||
Assert.Equal(0, deadline);
|
||||
stagedWaiter = Assert.IsAssignableFrom<IGuestThreadBlockWaiter>(waiter);
|
||||
});
|
||||
|
||||
wakeContext[CpuRegister.Rdi] = ValueAddress;
|
||||
wakeContext[CpuRegister.Rsi] = 3; // UMTX_OP_WAKE
|
||||
wakeContext[CpuRegister.Rdx] = 1;
|
||||
wakeContext[CpuRegister.Rcx] = 0;
|
||||
wakeContext[CpuRegister.R8] = 0;
|
||||
Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(wakeContext));
|
||||
|
||||
var resumed = Assert.IsAssignableFrom<IGuestThreadBlockWaiter>(stagedWaiter);
|
||||
Assert.True(resumed.TryWake());
|
||||
Assert.Equal(0, resumed.Resume());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxZeroTimespecReturnsMinusOneAndTimedOutErrno()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 12);
|
||||
WriteUInt64(memory, TimeoutAddress, 0);
|
||||
WriteUInt64(memory, TimeoutAddress + sizeof(long), 0);
|
||||
SetErrnoStorage(ctx);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 12;
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = TimeoutAddress;
|
||||
|
||||
Assert.Equal(-1, KernelSyncOnAddressCompatExports.UmtxOp(ctx));
|
||||
Assert.Equal(ulong.MaxValue, ctx[CpuRegister.Rax]);
|
||||
Assert.Equal(60, ReadInt32(memory, ctx.FsBase + 0x40)); // ETIMEDOUT
|
||||
Assert.False(GuestThreadExecution.TryConsumeCurrentThreadBlock(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxRejectsInvalidTimespecWithEinval()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 12);
|
||||
WriteUInt64(memory, TimeoutAddress, 0);
|
||||
WriteUInt64(memory, TimeoutAddress + sizeof(long), 1_000_000_000);
|
||||
SetErrnoStorage(ctx);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 12;
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = TimeoutAddress;
|
||||
|
||||
Assert.Equal(-1, KernelSyncOnAddressCompatExports.UmtxOp(ctx));
|
||||
Assert.Equal(22, ReadInt32(memory, ctx.FsBase + 0x40)); // EINVAL
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxLargeTimespecSaturatesWithoutOverflow()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 1);
|
||||
WriteUInt64(memory, TimeoutAddress, unchecked((ulong)long.MaxValue));
|
||||
WriteUInt64(memory, TimeoutAddress + sizeof(long), 999_999_999);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 2; // Already changed, so no host wait.
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = TimeoutAddress;
|
||||
|
||||
Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(ctx));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxTimedContinuationReturnsPosixTimeout()
|
||||
{
|
||||
var (memory, ctx) = CreateContext();
|
||||
WriteUInt64(memory, ValueAddress, 21);
|
||||
WriteUInt64(memory, TimeoutAddress, 0);
|
||||
WriteUInt64(memory, TimeoutAddress + sizeof(long), 1_000_000); // 1 ms
|
||||
SetErrnoStorage(ctx);
|
||||
ctx[CpuRegister.Rdi] = ValueAddress;
|
||||
ctx[CpuRegister.Rsi] = 2; // UMTX_OP_WAIT
|
||||
ctx[CpuRegister.Rdx] = 21;
|
||||
ctx[CpuRegister.Rcx] = 0;
|
||||
ctx[CpuRegister.R8] = TimeoutAddress;
|
||||
|
||||
IGuestThreadBlockWaiter? stagedWaiter = null;
|
||||
RunAsGuest(0x80B, () =>
|
||||
{
|
||||
Assert.Equal(0, KernelSyncOnAddressCompatExports.UmtxOp(ctx));
|
||||
Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock(
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out var waiter,
|
||||
out var deadline));
|
||||
Assert.True(deadline > 0);
|
||||
stagedWaiter = Assert.IsAssignableFrom<IGuestThreadBlockWaiter>(waiter);
|
||||
});
|
||||
|
||||
Thread.Sleep(10);
|
||||
var resumed = Assert.IsAssignableFrom<IGuestThreadBlockWaiter>(stagedWaiter);
|
||||
Assert.Equal(-1, resumed.Resume());
|
||||
Assert.Equal(60, ReadInt32(memory, ctx.FsBase + 0x40)); // ETIMEDOUT
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UmtxExportIsRegisteredForGen5()
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
|
||||
Assert.True(manager.TryGetExport("04AjkP0jO9U", out var export));
|
||||
Assert.Equal("_umtx_op", export.Name);
|
||||
Assert.Equal("libKernel", export.LibraryName);
|
||||
}
|
||||
|
||||
private static (FakeCpuMemory Memory, CpuContext Context) CreateContext()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
|
|
@ -213,4 +373,16 @@ public sealed class KernelSyncOnAddressCompatExportsTests
|
|||
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
|
||||
Assert.True(memory.TryWrite(address, bytes));
|
||||
}
|
||||
|
||||
private static void SetErrnoStorage(CpuContext ctx)
|
||||
{
|
||||
ctx.FsBase = BaseAddress + 0x700;
|
||||
}
|
||||
|
||||
private static int ReadInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
return BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ public sealed class SemaphoreSignalProfileTests
|
|||
|
||||
[Theory]
|
||||
[InlineData(0, 2, true)]
|
||||
[InlineData(0, 0, false)]
|
||||
[InlineData(0, 0, true)]
|
||||
[InlineData(1, -1, false)]
|
||||
[InlineData(1, int.MaxValue, false)]
|
||||
public void SignalKeepsTokenSemanticsAndRecordsStagesWhenEnabled(uint initial, int count, bool succeeds)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Np;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Np;
|
||||
|
||||
public sealed class NpWebApi2ExportsTests
|
||||
{
|
||||
private const ulong MemoryBase = 0x1_0000_0000;
|
||||
private const int InvalidArgument = unchecked((int)0x80553402);
|
||||
|
||||
[Fact]
|
||||
public void PushEventRegisterCallback_ValidatesStateAndReturnsCallbackId()
|
||||
{
|
||||
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
||||
NpWebApi2Exports.ResetRuntimeState();
|
||||
try
|
||||
{
|
||||
context[CpuRegister.Rdi] = 1;
|
||||
context[CpuRegister.Rsi] = 0x1000;
|
||||
var libraryContextId = NpWebApi2Exports.NpWebApi2Initialize(context);
|
||||
Assert.True(libraryContextId > 0);
|
||||
|
||||
context[CpuRegister.Rdi] = unchecked((ulong)libraryContextId);
|
||||
context[CpuRegister.Rsi] = 0x1000_0000;
|
||||
var userContextId = NpWebApi2Exports.NpWebApi2CreateUserContext(context);
|
||||
Assert.True(userContextId > 0);
|
||||
|
||||
context[CpuRegister.Rdi] = unchecked((ulong)libraryContextId);
|
||||
var filterId = NpWebApi2Exports.NpWebApi2PushEventCreateFilter(context);
|
||||
Assert.True(filterId > 0);
|
||||
|
||||
context[CpuRegister.Rdi] = unchecked((ulong)userContextId);
|
||||
context[CpuRegister.Rsi] = unchecked((ulong)filterId);
|
||||
context[CpuRegister.Rdx] = 0x8000_1000;
|
||||
context[CpuRegister.Rcx] = 0x1234;
|
||||
Assert.True(NpWebApi2Exports.NpWebApi2PushEventRegisterCallback(context) > 0);
|
||||
|
||||
context[CpuRegister.Rdx] = 0;
|
||||
Assert.Equal(
|
||||
InvalidArgument,
|
||||
NpWebApi2Exports.NpWebApi2PushEventRegisterCallback(context));
|
||||
}
|
||||
finally
|
||||
{
|
||||
NpWebApi2Exports.ResetRuntimeState();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PushEventRegisterCallback_RegistersForGen5()
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
|
||||
Assert.True(manager.TryGetExport("fY3QqeNkF8k", out var export));
|
||||
Assert.Equal("sceNpWebApi2PushEventRegisterCallback", export.Name);
|
||||
Assert.Equal("libSceNpWebApi2", export.LibraryName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PushEventDeleteHandle_IsIdempotentAndRegisteredForGen5()
|
||||
{
|
||||
var context = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = 1;
|
||||
context[CpuRegister.Rsi] = 1;
|
||||
Assert.Equal(0, NpWebApi2Exports.NpWebApi2PushEventDeleteHandle(context));
|
||||
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
Assert.True(manager.TryGetExport("fIATVMo4Y1w", out var export));
|
||||
Assert.Equal("sceNpWebApi2PushEventDeleteHandle", export.Name);
|
||||
Assert.Equal("libSceNpWebApi2", export.LibraryName);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ public sealed class PthreadSemaphoreSemanticsTests
|
|||
[InlineData("C36iRE0F5sE", "scePthreadSemWait")]
|
||||
[InlineData("aishVAiFaYM", "scePthreadSemPost")]
|
||||
[InlineData("H2a+IN9TP0E", "scePthreadSemTrywait")]
|
||||
[InlineData("fjN6NQHhK8k", "scePthreadSemTimedwait")]
|
||||
[InlineData("GEnUkDZoUwY", "scePthreadSemInit")]
|
||||
[InlineData("Vwc+L05e6oE", "scePthreadSemDestroy")]
|
||||
public void RegistryResolvesPthreadSemaphoreExports(string nid, string exportName)
|
||||
|
|
@ -68,6 +69,41 @@ public sealed class PthreadSemaphoreSemanticsTests
|
|||
DestroySemaphore(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PthreadSemTimedWaitTreatsSecondArgumentAsRelativeMicroseconds()
|
||||
{
|
||||
var context = CreateContext();
|
||||
InitializeSemaphore(context, initialCount: 1);
|
||||
|
||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
||||
context[CpuRegister.Rsi] = 250_000;
|
||||
|
||||
Assert.Equal(0, KernelSemaphoreCompatExports.PthreadSemTimedWait(context));
|
||||
Assert.Equal(0UL, context[CpuRegister.Rax]);
|
||||
Assert.Equal(0, ReadSemaphoreValue(context));
|
||||
|
||||
DestroySemaphore(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PthreadSemTimedWaitWithZeroTimeoutReturnsTimedOutWhenEmpty()
|
||||
{
|
||||
var context = CreateContext();
|
||||
InitializeSemaphore(context, initialCount: 0);
|
||||
|
||||
context[CpuRegister.Rdi] = SemaphoreAddress;
|
||||
context[CpuRegister.Rsi] = 0;
|
||||
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT,
|
||||
KernelSemaphoreCompatExports.PthreadSemTimedWait(context));
|
||||
Assert.Equal(
|
||||
unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT),
|
||||
context[CpuRegister.Rax]);
|
||||
|
||||
DestroySemaphore(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PthreadSemInitRejectsNonPrivateFlag()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Reflection;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Pthread;
|
||||
|
||||
[CollectionDefinition(Name, DisableParallelization = true)]
|
||||
public sealed class PthreadStackBoundsCollection
|
||||
{
|
||||
public const string Name = "PthreadStackBounds";
|
||||
}
|
||||
|
||||
[Collection(PthreadStackBoundsCollection.Name)]
|
||||
public sealed class PthreadStackBoundsTests : IDisposable
|
||||
{
|
||||
private const ulong MemoryBase = 0x6_2000_0000;
|
||||
private const ulong ThreadHandle = 0x1234_5000;
|
||||
private const ulong AttrAddress = MemoryBase + 0x100;
|
||||
private const ulong OutStackAddress = MemoryBase + 0x200;
|
||||
private const ulong OutStackSizeAddress = MemoryBase + 0x208;
|
||||
private readonly IGuestThreadScheduler? _previousScheduler = GuestThreadExecution.Scheduler;
|
||||
|
||||
[Fact]
|
||||
public void PosixGetAttrUsesSchedulerBoundsForRemoteHighSlotThread()
|
||||
{
|
||||
const ulong stackBase = 0x0000_7FFF_A800_0000;
|
||||
const ulong stackSize = 0x20_0000;
|
||||
var scheduler = CreateScheduler(ThreadHandle, stackBase, stackSize);
|
||||
GuestThreadExecution.Scheduler = scheduler;
|
||||
|
||||
var context = CreateContext();
|
||||
context[CpuRegister.Rsp] = stackBase + stackSize - 0xEA0;
|
||||
context[CpuRegister.Rdi] = ThreadHandle;
|
||||
context[CpuRegister.Rsi] = AttrAddress;
|
||||
|
||||
Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGetNpPOSIX(context));
|
||||
AssertStackBounds(context, stackBase, stackSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentPrimaryThreadRetainsBoundedStackPointerFallback()
|
||||
{
|
||||
const ulong primaryThreadHandle = ThreadHandle + 1;
|
||||
var stackBase = OperatingSystem.IsWindows()
|
||||
? 0x0000_7FFF_F000_0000UL
|
||||
: 0x0000_6FFF_F000_0000UL;
|
||||
GuestThreadExecution.Scheduler = CreateScheduler();
|
||||
var previousThread = GuestThreadExecution.EnterGuestThread(primaryThreadHandle);
|
||||
try
|
||||
{
|
||||
var context = CreateContext();
|
||||
context[CpuRegister.Rsp] = stackBase + 0x1F_F160;
|
||||
context[CpuRegister.Rdi] = primaryThreadHandle;
|
||||
context[CpuRegister.Rsi] = AttrAddress + 0x20;
|
||||
|
||||
Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGet(context));
|
||||
AssertStackBounds(context, stackBase, 0x20_0000, AttrAddress + 0x20);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadExecution.RestoreGuestThread(previousThread);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidSchedulerBoundsDoNotReplaceDefaultAttributes()
|
||||
{
|
||||
const ulong invalidThreadHandle = ThreadHandle + 2;
|
||||
GuestThreadExecution.Scheduler = CreateScheduler(invalidThreadHandle, 0, 0x20_0000);
|
||||
var context = CreateContext();
|
||||
context[CpuRegister.Rdi] = invalidThreadHandle;
|
||||
context[CpuRegister.Rsi] = AttrAddress + 0x40;
|
||||
|
||||
Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGet(context));
|
||||
AssertStackBounds(context, 0, 0x10_0000, AttrAddress + 0x40);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GuestThreadExecution.Scheduler = _previousScheduler;
|
||||
}
|
||||
|
||||
private static CpuContext CreateContext() =>
|
||||
new(new FakeCpuMemory(MemoryBase, 0x4000), Generation.Gen5);
|
||||
|
||||
private static IGuestThreadScheduler CreateScheduler(
|
||||
ulong threadHandle = 0,
|
||||
ulong stackBase = 0,
|
||||
ulong stackSize = 0)
|
||||
{
|
||||
var scheduler = DispatchProxy.Create<IGuestThreadScheduler, StackBoundsSchedulerProxy>();
|
||||
var proxy = (StackBoundsSchedulerProxy)(object)scheduler;
|
||||
if (threadHandle != 0)
|
||||
{
|
||||
proxy.Bounds[threadHandle] = (stackBase, stackSize);
|
||||
}
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
private static void AssertStackBounds(
|
||||
CpuContext context,
|
||||
ulong expectedBase,
|
||||
ulong expectedSize,
|
||||
ulong attrAddress = AttrAddress)
|
||||
{
|
||||
context[CpuRegister.Rdi] = attrAddress;
|
||||
context[CpuRegister.Rsi] = OutStackAddress;
|
||||
context[CpuRegister.Rdx] = OutStackSizeAddress;
|
||||
Assert.Equal(0, KernelPthreadExtendedCompatExports.PthreadAttrGetstackPOSIX(context));
|
||||
Assert.True(context.TryReadUInt64(OutStackAddress, out var actualBase));
|
||||
Assert.True(context.TryReadUInt64(OutStackSizeAddress, out var actualSize));
|
||||
Assert.Equal(expectedBase, actualBase);
|
||||
Assert.Equal(expectedSize, actualSize);
|
||||
}
|
||||
|
||||
public class StackBoundsSchedulerProxy : DispatchProxy
|
||||
{
|
||||
public Dictionary<ulong, (ulong Base, ulong Size)> Bounds { get; } = [];
|
||||
|
||||
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
|
||||
{
|
||||
if (targetMethod?.Name == nameof(IGuestThreadScheduler.TryGetGuestThreadStackBounds))
|
||||
{
|
||||
var found = Bounds.TryGetValue((ulong)args![0]!, out var bounds);
|
||||
args[1] = found ? bounds.Base : 0UL;
|
||||
args[2] = found ? bounds.Size : 0UL;
|
||||
return found;
|
||||
}
|
||||
|
||||
throw new NotSupportedException(targetMethod?.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue