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:
Berk 2026-10-06 18:43:21 +03:00 committed by GitHub
parent a17aaca533
commit 2df6f5b0a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1871 additions and 222 deletions

View File

@ -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)

View File

@ -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(

View File

@ -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)

View File

@ -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; }

View File

@ -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);

View File

@ -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)
{

View File

@ -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(

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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",

View File

@ -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);
}
}

View File

@ -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))

View File

@ -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(

View File

@ -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)
{

View File

@ -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();

View File

@ -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);

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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()
{

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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)
{

View File

@ -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);
}
}

View File

@ -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()
{

View File

@ -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);
}
}
}

View File

@ -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()
{