diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index f393bb9f..74b5d564 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -535,8 +535,7 @@ public sealed partial class DirectExecutionBackend out var blockContinuation, out var hasBlockContinuation, out var blockWakeKey, - out var blockResumeHandler, - out var blockWakeHandler, + out var blockWaiter, out var blockDeadlineTimestamp) && TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason)) { @@ -546,8 +545,7 @@ public sealed partial class DirectExecutionBackend GuestThreadExecution.CurrentGuestThreadHandle, blockContinuation, blockWakeKey, - blockResumeHandler, - blockWakeHandler, + blockWaiter, blockDeadlineTimestamp); } @@ -678,8 +676,7 @@ public sealed partial class DirectExecutionBackend out var blockContinuation, out var hasBlockContinuation, out var blockWakeKey, - out var blockResumeHandler, - out var blockWakeHandler, + out var blockWaiter, out var blockDeadlineTimestamp) && TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason)) { @@ -689,8 +686,7 @@ public sealed partial class DirectExecutionBackend GuestThreadExecution.CurrentGuestThreadHandle, blockContinuation, blockWakeKey, - blockResumeHandler, - blockWakeHandler, + blockWaiter, blockDeadlineTimestamp); } @@ -1970,23 +1966,36 @@ public sealed partial class DirectExecutionBackend return false; } - List list = new List(Math.Min(maxLength, 256)); - Span destination = stackalloc byte[1]; - for (int i = 0; i < maxLength; i++) + // Reads stay byte-by-byte through TryReadByteCompat (its Marshal.ReadByte + // fallback must probe exactly up to the terminator), but the bytes land in a + // stack buffer instead of a List + ToArray per symbol resolution. + const int StackBufferLength = 512; + byte[]? rented = maxLength > StackBufferLength ? System.Buffers.ArrayPool.Shared.Rent(maxLength) : null; + Span buffer = rented is null ? stackalloc byte[StackBufferLength] : rented; + try { - if (!TryReadByteCompat(address + (ulong)i, destination)) + for (int i = 0; i < maxLength; i++) { - return false; + if (!TryReadByteCompat(address + (ulong)i, buffer.Slice(i, 1))) + { + return false; + } + if (buffer[i] == 0) + { + value = System.Text.Encoding.ASCII.GetString(buffer[..i]); + return true; + } + } + value = System.Text.Encoding.ASCII.GetString(buffer[..maxLength]); + return true; + } + finally + { + if (rented is not null) + { + System.Buffers.ArrayPool.Shared.Return(rented); } - if (destination[0] == 0) - { - value = System.Text.Encoding.ASCII.GetString(list.ToArray()); - return true; - } - list.Add(destination[0]); } - value = System.Text.Encoding.ASCII.GetString(list.ToArray()); - return true; } private bool TryReadByteCompat(ulong address, Span destination) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index d4a43fe3..eb51bd6e 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -434,9 +434,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I public string? BlockWakeKey { get; set; } - public Func? BlockResumeHandler { get; set; } - - public Func? BlockWakeHandler { get; set; } + // Stays set through the wake transition; Resume() consumes it when the thread pumps. + public IGuestThreadBlockWaiter? BlockWaiter { get; set; } public long BlockDeadlineTimestamp { get; set; } @@ -2797,14 +2796,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I continue; } - if (thread.BlockWakeHandler is not null && !thread.BlockWakeHandler()) + if (thread.BlockWaiter is not null && !thread.BlockWaiter.TryWake()) { continue; } thread.State = GuestThreadRunState.Ready; thread.BlockReason = null; - thread.BlockWakeHandler = null; thread.BlockDeadlineTimestamp = 0; _readyGuestThreads.Enqueue(thread); Interlocked.Increment(ref _readyGuestThreadCount); @@ -2855,8 +2853,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I ulong guestThreadHandle, GuestCpuContinuation continuation, string wakeKey, - Func? resumeHandler, - Func? wakeHandler, + IGuestThreadBlockWaiter? waiter, long blockDeadlineTimestamp) { if (guestThreadHandle == 0 || continuation.Rip < 65536 || continuation.Rsp == 0) @@ -2874,8 +2871,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I thread.BlockedContinuation = continuation; thread.HasBlockedContinuation = true; thread.BlockWakeKey = wakeKey; - thread.BlockResumeHandler = resumeHandler; - thread.BlockWakeHandler = wakeHandler; + thread.BlockWaiter = waiter; thread.BlockDeadlineTimestamp = blockDeadlineTimestamp; } } @@ -2898,7 +2894,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I thread.State = GuestThreadRunState.Ready; thread.BlockReason = null; - thread.BlockWakeHandler = null; thread.BlockDeadlineTimestamp = 0; _readyGuestThreads.Enqueue(thread); Interlocked.Increment(ref _readyGuestThreadCount); @@ -3575,7 +3570,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { LastError = null; GuestCpuContinuation continuation = default; - Func? resumeHandler = null; + IGuestThreadBlockWaiter? blockWaiter = null; var resumeContinuation = false; lock (_guestThreadGate) { @@ -3585,17 +3580,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I thread.BlockedContinuation = default; thread.HasBlockedContinuation = false; thread.BlockWakeKey = null; - resumeHandler = thread.BlockResumeHandler; - thread.BlockResumeHandler = null; - thread.BlockWakeHandler = null; + blockWaiter = thread.BlockWaiter; + thread.BlockWaiter = null; thread.BlockDeadlineTimestamp = 0; resumeContinuation = true; } } - if (resumeHandler is not null) + if (blockWaiter is not null) { - continuation = continuation with { Rax = unchecked((ulong)(long)resumeHandler()) }; + continuation = continuation with { Rax = unchecked((ulong)(long)blockWaiter.Resume()) }; } if (_logGuestThreads) @@ -3620,12 +3614,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I thread.State = GuestThreadRunState.Blocked; thread.BlockReason = blockReason; if (thread.HasBlockedContinuation && - thread.BlockWakeHandler is not null && - thread.BlockWakeHandler()) + thread.BlockWaiter is not null && + thread.BlockWaiter.TryWake()) { thread.State = GuestThreadRunState.Ready; thread.BlockReason = null; - thread.BlockWakeHandler = null; thread.BlockDeadlineTimestamp = 0; _readyGuestThreads.Enqueue(thread); Interlocked.Increment(ref _readyGuestThreadCount); diff --git a/src/SharpEmu.HLE/CpuContext.cs b/src/SharpEmu.HLE/CpuContext.cs index bf24372d..adb37687 100644 --- a/src/SharpEmu.HLE/CpuContext.cs +++ b/src/SharpEmu.HLE/CpuContext.cs @@ -1,6 +1,7 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.Buffers; using System.Buffers.Binary; using System.Text; @@ -238,23 +239,63 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation) return false; } - var bytes = new byte[capacity]; - for (var index = 0; index < bytes.Length; index++) + const int StackBufferLength = 512; + const int ReadChunkLength = 128; + var rented = capacity > StackBufferLength ? ArrayPool.Shared.Rent(capacity) : null; + Span bytes = rented is null ? stackalloc byte[StackBufferLength] : rented; + try { - if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1))) + var length = 0; + while (length < capacity) { - return false; + // Bulk-read in bounded chunks rather than the full capacity: the string + // may end just before unmapped memory, and overreading past the + // terminator by more than a chunk could fault where the old + // byte-by-byte loop succeeded. + var chunk = Math.Min(ReadChunkLength, capacity - length); + var span = bytes.Slice(length, chunk); + if (Memory.TryRead(address + (ulong)length, span)) + { + var terminator = span.IndexOf((byte)0); + if (terminator >= 0) + { + value = Encoding.UTF8.GetString(bytes[..(length + terminator)]); + return true; + } + + length += chunk; + continue; + } + + // The chunk touches an unreadable range; fall back to per-byte reads so a + // terminator sitting before the bad byte still yields the string. + for (var i = 0; i < chunk; i++) + { + if (!Memory.TryRead(address + (ulong)(length + i), bytes.Slice(length + i, 1))) + { + return false; + } + + if (bytes[length + i] == 0) + { + value = Encoding.UTF8.GetString(bytes[..(length + i)]); + return true; + } + } + + length += chunk; } - if (bytes[index] == 0) + value = Encoding.UTF8.GetString(bytes[..capacity]); + return true; + } + finally + { + if (rented is not null) { - value = Encoding.UTF8.GetString(bytes, 0, index); - return true; + ArrayPool.Shared.Return(rented); } } - - value = Encoding.UTF8.GetString(bytes); - return true; } public bool PushUInt64(ulong value) diff --git a/src/SharpEmu.HLE/GuestThreadExecution.cs b/src/SharpEmu.HLE/GuestThreadExecution.cs index 6d572208..7a0c5ab9 100644 --- a/src/SharpEmu.HLE/GuestThreadExecution.cs +++ b/src/SharpEmu.HLE/GuestThreadExecution.cs @@ -23,6 +23,20 @@ public readonly record struct GuestThreadSnapshot( ulong LastReturnRip, string? BlockReason); +/// +/// Continuation state for a blocked guest thread, replacing the closure pair a blocking +/// wait used to allocate. TryWake runs under the scheduler's guest-thread gate and +/// returns true when the waiter has a final result and the thread should be re-readied; +/// false leaves it parked. Resume runs later on the woken thread outside that gate, and +/// its return value becomes the guest's RAX for the resumed call. +/// +public interface IGuestThreadBlockWaiter +{ + int Resume(); + + bool TryWake(); +} + public interface IGuestThreadScheduler { bool SupportsGuestContextTransfer { get; } @@ -106,10 +120,7 @@ public static class GuestThreadExecution private static string? _pendingBlockWakeKey; [ThreadStatic] - private static Func? _pendingBlockResumeHandler; - - [ThreadStatic] - private static Func? _pendingBlockWakeHandler; + private static IGuestThreadBlockWaiter? _pendingBlockWaiter; [ThreadStatic] private static long _pendingBlockDeadlineTimestamp; @@ -157,8 +168,7 @@ public static class GuestThreadExecution _pendingBlockContinuationValid = false; _pendingBlockContinuation = default; _pendingBlockWakeKey = null; - _pendingBlockResumeHandler = null; - _pendingBlockWakeHandler = null; + _pendingBlockWaiter = null; _pendingBlockDeadlineTimestamp = 0; _pendingEntryExit = false; _pendingEntryExitValue = 0; @@ -179,8 +189,7 @@ public static class GuestThreadExecution _pendingBlockContinuationValid = false; _pendingBlockContinuation = default; _pendingBlockWakeKey = null; - _pendingBlockResumeHandler = null; - _pendingBlockWakeHandler = null; + _pendingBlockWaiter = null; _pendingBlockDeadlineTimestamp = 0; _pendingEntryExit = false; _pendingEntryExitValue = 0; @@ -211,8 +220,7 @@ public static class GuestThreadExecution CpuContext? context, string reason, string? wakeKey = null, - Func? resumeHandler = null, - Func? wakeHandler = null, + IGuestThreadBlockWaiter? waiter = null, long blockDeadlineTimestamp = 0) { if (!IsGuestThread) @@ -222,8 +230,7 @@ public static class GuestThreadExecution _pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason; _pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey; - _pendingBlockResumeHandler = resumeHandler; - _pendingBlockWakeHandler = wakeHandler; + _pendingBlockWaiter = waiter; _pendingBlockDeadlineTimestamp = blockDeadlineTimestamp; if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation)) { @@ -255,7 +262,6 @@ public static class GuestThreadExecution out hasContinuation, out _, out _, - out _, out _); } @@ -264,16 +270,14 @@ public static class GuestThreadExecution out GuestCpuContinuation continuation, out bool hasContinuation, out string wakeKey, - out Func? resumeHandler, - out Func? wakeHandler) + out IGuestThreadBlockWaiter? waiter) { return TryConsumeCurrentThreadBlock( out reason, out continuation, out hasContinuation, out wakeKey, - out resumeHandler, - out wakeHandler, + out waiter, out _); } @@ -282,8 +286,7 @@ public static class GuestThreadExecution out GuestCpuContinuation continuation, out bool hasContinuation, out string wakeKey, - out Func? resumeHandler, - out Func? wakeHandler, + out IGuestThreadBlockWaiter? waiter, out long blockDeadlineTimestamp) { reason = _pendingBlockReason ?? string.Empty; @@ -292,8 +295,7 @@ public static class GuestThreadExecution continuation = default; hasContinuation = false; wakeKey = string.Empty; - resumeHandler = null; - wakeHandler = null; + waiter = null; blockDeadlineTimestamp = 0; return false; } @@ -301,15 +303,13 @@ public static class GuestThreadExecution continuation = _pendingBlockContinuation; hasContinuation = _pendingBlockContinuationValid; wakeKey = _pendingBlockWakeKey ?? reason; - resumeHandler = _pendingBlockResumeHandler; - wakeHandler = _pendingBlockWakeHandler; + waiter = _pendingBlockWaiter; blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp; _pendingBlockReason = null; _pendingBlockContinuation = default; _pendingBlockContinuationValid = false; _pendingBlockWakeKey = null; - _pendingBlockResumeHandler = null; - _pendingBlockWakeHandler = null; + _pendingBlockWaiter = null; _pendingBlockDeadlineTimestamp = 0; return true; } diff --git a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs index c692f725..c4febeb8 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs @@ -34,9 +34,43 @@ public static class KernelEventFlagCompatExports public object Gate { get; } = new(); } - private sealed class EventFlagWaiter + private sealed class EventFlagWaiter : IGuestThreadBlockWaiter { + public required CpuContext Ctx { get; init; } + public required EventFlagState State { get; init; } + public required ulong Pattern { get; init; } + public required uint WaitMode { get; init; } + public required ulong ResultAddress { get; init; } + public bool Timed { get; init; } + + // Timed-wait completion state; unused when Timed is false. + public ulong TimeoutAddress { get; init; } + public long DeadlineTimestamp { get; init; } + public OrbisGen2Result? Result { get; set; } + + // Untimed waits stash the prepared result here at wake and return it at resume. + private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK; + + public int Resume() => Timed + ? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp) + : (int)_blockedResult; + + public bool TryWake() + { + if (Timed) + { + return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress); + } + + if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult)) + { + return false; + } + + _blockedResult = preparedResult; + return true; + } } [SysAbiExport( @@ -249,27 +283,22 @@ public static class KernelEventFlagCompatExports var deadline = GuestThreadExecution.ComputeDeadlineTimestamp( TimeSpan.FromTicks((long)timeoutUsec * 10L)); - var timedWaiter = new EventFlagWaiter(); + var timedWaiter = new EventFlagWaiter + { + Ctx = ctx, + State = state, + Pattern = pattern, + WaitMode = waitMode, + ResultAddress = resultAddress, + Timed = true, + TimeoutAddress = timeoutAddress, + DeadlineTimestamp = deadline, + }; if (GuestThreadExecution.RequestCurrentThreadBlock( ctx, "sceKernelWaitEventFlag", GetEventFlagWakeKey(handle), - resumeHandler: () => CompleteBlockedTimedWait( - ctx, - state, - timedWaiter, - pattern, - waitMode, - resultAddress, - timeoutAddress, - deadline), - wakeHandler: () => TryCompleteBlockedTimedWait( - ctx, - state, - timedWaiter, - pattern, - waitMode, - resultAddress), + timedWaiter, blockDeadlineTimestamp: deadline)) { state.WaitingThreads++; @@ -286,27 +315,17 @@ public static class KernelEventFlagCompatExports var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle; var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx); var managedThread = Environment.CurrentManagedThreadId; - var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK; var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock( ctx, "sceKernelWaitEventFlag", GetEventFlagWakeKey(handle), - () => (int)blockedWaitResult, - () => + new EventFlagWaiter { - if (!TryPrepareBlockedWait( - ctx, - state, - pattern, - waitMode, - resultAddress, - out var preparedResult)) - { - return false; - } - - blockedWaitResult = preparedResult; - return true; + Ctx = ctx, + State = state, + Pattern = pattern, + WaitMode = waitMode, + ResultAddress = resultAddress, }); TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}"); TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}"); diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index 3073ee82..aac60798 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using System.Buffers; using System.Buffers.Binary; +using System.Collections.Concurrent; using System.Threading; namespace SharpEmu.Libs.Kernel; @@ -17,7 +19,7 @@ public static class KernelEventQueueCompatExports private static readonly object _eventQueueGate = new(); private static readonly HashSet _eventQueues = new(); - private static readonly Dictionary> _pendingEvents = new(); + private static readonly Dictionary _pendingEvents = new(); private static readonly Dictionary> _registeredEvents = new(); private static long _nextEventQueueHandle = 1; @@ -34,6 +36,76 @@ public static class KernelEventQueueCompatExports short Filter, ulong UserData); + // Grow-only ring buffer standing in for LinkedList, which + // allocated a node per enqueue — steady churn at one enqueue per vblank/flip edge + // per registered queue. Mutated only under _eventQueueGate. + private sealed class KernelEventDeque + { + private KernelQueuedEvent[] _items = new KernelQueuedEvent[4]; + private int _head; + + public int Count { get; private set; } + + public KernelQueuedEvent this[int index] + { + get => _items[(_head + index) % _items.Length]; + set => _items[(_head + index) % _items.Length] = value; + } + + public void AddLast(in KernelQueuedEvent item) + { + if (Count == _items.Length) + { + var grown = new KernelQueuedEvent[_items.Length * 2]; + for (var i = 0; i < Count; i++) + { + grown[i] = this[i]; + } + + _items = grown; + _head = 0; + } + + _items[(_head + Count) % _items.Length] = item; + Count++; + } + + public KernelQueuedEvent RemoveFirst() + { + var value = _items[_head]; + _head = (_head + 1) % _items.Length; + Count--; + return value; + } + + public int FindIndex(ulong ident, short filter) + { + for (var i = 0; i < Count; i++) + { + var candidate = this[i]; + if (candidate.Ident == ident && candidate.Filter == filter) + { + return i; + } + } + + return -1; + } + } + + private sealed class EqueueWaiter : IGuestThreadBlockWaiter + { + public required CpuContext Ctx { get; init; } + public required ulong Handle { get; init; } + public required ulong EventsAddress { get; init; } + public required int EventCapacity { get; init; } + public required ulong OutCountAddress { get; init; } + + public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress); + + public bool TryWake() => HasPendingEvents(Handle); + } + [SysAbiExport( Nid = "D0OdFMjp46I", ExportName = "sceKernelCreateEqueue", @@ -51,7 +123,7 @@ public static class KernelEventQueueCompatExports lock (_eventQueueGate) { _eventQueues.Add(handle); - _pendingEvents[handle] = new LinkedList(); + _pendingEvents[handle] = new KernelEventDeque(); _registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>(); } @@ -79,6 +151,8 @@ public static class KernelEventQueueCompatExports _registeredEvents.Remove(handle); } + _wakeKeys.TryRemove(handle, out _); + TraceEventQueue(ctx, "delete", handle); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -343,8 +417,14 @@ public static class KernelEventQueueCompatExports ctx, "sceKernelWaitEqueue", GetEventQueueWakeKey(handle), - () => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress), - () => HasPendingEvents(handle))) + new EqueueWaiter + { + Ctx = ctx, + Handle = handle, + EventsAddress = eventsAddress, + EventCapacity = eventCapacity, + OutCountAddress = outCountAddress, + })) { TraceEventQueue(ctx, "wait-block", handle); return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -409,7 +489,7 @@ public static class KernelEventQueueCompatExports if (!_pendingEvents.TryGetValue(handle, out var queue)) { - queue = new LinkedList(); + queue = new KernelEventDeque(); _pendingEvents[handle] = queue; } @@ -480,7 +560,7 @@ public static class KernelEventQueueCompatExports if (!_pendingEvents.TryGetValue(handle, out var queue)) { - queue = new LinkedList(); + queue = new KernelEventDeque(); _pendingEvents[handle] = queue; } @@ -527,7 +607,7 @@ public static class KernelEventQueueCompatExports if (!_pendingEvents.TryGetValue(handle, out var queue)) { - queue = new LinkedList(); + queue = new KernelEventDeque(); _pendingEvents[handle] = queue; } @@ -563,15 +643,15 @@ public static class KernelEventQueueCompatExports if (!_pendingEvents.TryGetValue(handle, out var events)) { - events = new LinkedList(); + events = new KernelEventDeque(); _pendingEvents[handle] = events; } var count = 1UL; - var pendingNode = FindPendingEvent(events, ident, filter); - if (pendingNode is not null) + var pendingIndex = events.FindIndex(ident, filter); + if (pendingIndex >= 0) { - count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL); + count = Math.Min(((events[pendingIndex].Data >> 12) & 0xFUL) + 1, 0xFUL); } var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL; @@ -584,9 +664,9 @@ public static class KernelEventQueueCompatExports eventData, userData); - if (pendingNode is not null) + if (pendingIndex >= 0) { - pendingNode.Value = triggeredEvent; + events[pendingIndex] = triggeredEvent; } else { @@ -631,40 +711,28 @@ public static class KernelEventQueueCompatExports } private static void QueueOrUpdateEvent( - LinkedList queue, + KernelEventDeque queue, KernelQueuedEvent queuedEvent) { - var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter); - if (pendingNode is null) + var pendingIndex = queue.FindIndex(queuedEvent.Ident, queuedEvent.Filter); + if (pendingIndex < 0) { queue.AddLast(queuedEvent); return; } - pendingNode.Value = queuedEvent with + queue[pendingIndex] = queuedEvent with { - Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags), + Fflags = Math.Max(queue[pendingIndex].Fflags + 1, queuedEvent.Fflags), }; } - private static LinkedListNode? FindPendingEvent( - LinkedList queue, - ulong ident, - short filter) - { - for (var node = queue.First; node is not null; node = node.Next) - { - if (node.Value.Ident == ident && node.Value.Filter == filter) - { - return node; - } - } - - return null; - } + // Wake keys are formatted once per handle: WakeEventQueue runs on every event + // enqueue (vblank/flip edges included), so formatting there is steady string churn. + private static readonly ConcurrentDictionary _wakeKeys = new(); private static string GetEventQueueWakeKey(ulong handle) => - $"sceKernelWaitEqueue:{handle:X16}"; + _wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}"); private static void WakeEventQueue(ulong handle) { @@ -678,7 +746,10 @@ public static class KernelEventQueueCompatExports return 0; } + // Engines wait on the vblank/flip equeue every frame, so the delivery buffer + // (usually a single event) comes from the pool instead of a per-call array. KernelQueuedEvent[] events; + int count; lock (_eventQueueGate) { if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0) @@ -686,24 +757,30 @@ public static class KernelEventQueueCompatExports return 0; } - var count = Math.Min(eventCapacity, queue.Count); - events = new KernelQueuedEvent[count]; + count = Math.Min(eventCapacity, queue.Count); + events = ArrayPool.Shared.Rent(count); for (var i = 0; i < count; i++) { - events[i] = queue.First!.Value; - queue.RemoveFirst(); + events[i] = queue.RemoveFirst(); } } - for (var i = 0; i < events.Length; i++) + try { - if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i])) + for (var i = 0; i < count; i++) { - return i; + if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i])) + { + return i; + } } } + finally + { + ArrayPool.Shared.Return(events); + } - return events.Length; + return count; } private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent) @@ -718,9 +795,12 @@ public static class KernelEventQueueCompatExports return ctx.Memory.TryWrite(address, eventBytes); } + private static readonly bool _logEqueue = + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal); + private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle) { - if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal)) + if (!_logEqueue) { return; } diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index eb6ceeb5..da9065d0 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -18,6 +18,10 @@ public static class KernelMemoryCompatExports private const int MaxGuestStringLength = 4096; private const int WideCharSize = sizeof(ushort); private const int MemsetChunkSize = 16 * 1024; + private const int MemcpyChunkSize = 256 * 1024; + + // Shared all-zero scratch for chunked zero-fill loops; never written to. + private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize]; private const int TlsModuleBlockSize = 0x10000; private const int O_WRONLY = 0x1; private const int O_RDWR = 0x2; @@ -255,11 +259,10 @@ public static class KernelMemoryCompatExports DirectStart: 0); } - var zeroes = new byte[(int)Math.Min(mappedLength, (ulong)MemsetChunkSize)]; for (ulong offset = 0; offset < mappedLength;) { - var chunkLength = (int)Math.Min((ulong)zeroes.Length, mappedLength - offset); - if (!ctx.Memory.TryWrite(address + offset, zeroes.AsSpan(0, chunkLength))) + var chunkLength = (int)Math.Min((ulong)_zeroChunk.Length, mappedLength - offset); + if (!ctx.Memory.TryWrite(address + offset, _zeroChunk.AsSpan(0, chunkLength))) { return false; } @@ -422,33 +425,50 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - var chunk = new byte[MemsetChunkSize]; - Array.Fill(chunk, value); - var remaining = length; - var cursor = destination; - while (remaining > 0) + // Rent may hand back a larger array than requested; only the first chunkLength + // bytes are filled, so the loop must cap at chunkLength rather than chunk.Length. + var chunkLength = (int)Math.Min(length, (ulong)MemsetChunkSize); + var chunk = value == 0 ? _zeroChunk : ArrayPool.Shared.Rent(chunkLength); + if (value != 0) { - var take = (int)Math.Min((ulong)chunk.Length, remaining); - if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take))) + chunk.AsSpan(0, chunkLength).Fill(value); + } + + try + { + var remaining = length; + var cursor = destination; + while (remaining > 0) { - if (length <= 0x40) + var take = (int)Math.Min((ulong)chunkLength, remaining); + if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take))) { - var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount); - if (recoveryIndex <= 8) + if (length <= 0x40) { - Console.Error.WriteLine( - $"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}"); + var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount); + if (recoveryIndex <= 8) + { + Console.Error.WriteLine( + $"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}"); + } + + ctx[CpuRegister.Rax] = destination; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - ctx[CpuRegister.Rax] = destination; - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + cursor += (ulong)take; + remaining -= (ulong)take; + } + } + finally + { + if (value != 0) + { + ArrayPool.Shared.Return(chunk); } - - cursor += (ulong)take; - remaining -= (ulong)take; } ctx[CpuRegister.Rax] = destination; @@ -1185,11 +1205,10 @@ public static class KernelMemoryCompatExports var rawCount = ctx[CpuRegister.Rdx]; // A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified - // upstream bug that also feeds bad lengths to memset) must not reach - // GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread - // call context corrupted the CLR outright ("Invalid Program: attempted to call a - // UnmanagedCallersOnly method from managed code") instead of throwing a normal - // exception. Reject anything above a sane bound before allocating. + // upstream bug that also feeds bad lengths to memset) must not turn into a multi-GB + // copy attempt from a guest-thread call context, which corrupted the CLR outright + // ("Invalid Program: attempted to call a UnmanagedCallersOnly method from managed + // code") instead of throwing a normal exception. Reject anything above a sane bound. const ulong maxSaneCount = 512UL * 1024 * 1024; if (rawCount > maxSaneCount) { @@ -1198,15 +1217,52 @@ public static class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; } - var count = (int)rawCount; - var payload = GC.AllocateUninitializedArray(count); - if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload))) + ctx[CpuRegister.Rax] = destination; + if (rawCount == 0) { - ctx[CpuRegister.Rax] = destination; - return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + // Cap iterations at the requested chunk size, not chunk.Length: Rent may hand + // back a larger array, and the copy granularity should not depend on pool + // bucketing internals. + var chunkLength = (int)Math.Min(rawCount, (ulong)MemcpyChunkSize); + var chunk = ArrayPool.Shared.Rent(chunkLength); + try + { + // memmove aliases this export, so overlapping ranges must survive the chunked + // copy: when the destination starts inside the source range, copy high-to-low + // so no source byte is overwritten before it has been read. + var copyBackward = destination > source && destination - source < rawCount; + var remaining = rawCount; + ulong offset = copyBackward ? rawCount : 0; + while (remaining > 0) + { + var take = (int)Math.Min((ulong)chunkLength, remaining); + if (copyBackward) + { + offset -= (ulong)take; + } + + var span = chunk.AsSpan(0, take); + if (!TryReadCompat(ctx, source + offset, span) || !TryWriteCompat(ctx, destination + offset, span)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (!copyBackward) + { + offset += (ulong)take; + } + + remaining -= (ulong)take; + } + } + finally + { + ArrayPool.Shared.Return(chunk); } - ctx[CpuRegister.Rax] = destination; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 6a61d8ea..b96956fe 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -61,10 +61,34 @@ public static class KernelPthreadCompatExports public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X"); } - private sealed class PthreadMutexWaiter + private sealed class PthreadMutexWaiter : IGuestThreadBlockWaiter { public required ulong ThreadId { get; init; } + public required CpuContext Ctx { get; init; } + public required ulong MutexAddress { get; init; } + public required ulong ResolvedAddress { get; init; } + public required PthreadMutexState State { get; init; } public int Reserved; + + public int Resume() => CompleteBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this); + + public bool TryWake() => TryReserveBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this); + } + + private sealed class PthreadCondWaiter : IGuestThreadBlockWaiter + { + public required CpuContext Ctx { get; init; } + public required ulong CondAddress { get; init; } + public required ulong MutexAddress { get; init; } + public required PthreadCondState State { get; init; } + public required ulong ObservedEpoch { get; init; } + public required bool Timed { get; init; } + public required int ReleasedRecursion { get; init; } + public required bool PosixResult { get; init; } + + public int Resume() => ResumePthreadCondWait(Ctx, CondAddress, MutexAddress, State, ObservedEpoch, Timed, ReleasedRecursion, PosixResult); + + public bool TryWake() => State.SignalEpoch != ObservedEpoch; } private sealed class PthreadCondState @@ -727,7 +751,6 @@ public static class KernelPthreadCompatExports if (!acquired) { TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId); - var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId }; // Fibers retain the synchronous fallback to preserve switch state. var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx); var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0; @@ -739,8 +762,14 @@ public static class KernelPthreadCompatExports ctx, "pthread_mutex_lock", state.WakeKey, - () => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter), - () => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter))) + new PthreadMutexWaiter + { + ThreadId = currentThreadId, + Ctx = ctx, + MutexAddress = mutexAddress, + ResolvedAddress = resolvedAddress, + State = state, + })) { TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -1388,8 +1417,17 @@ public static class KernelPthreadCompatExports ctx, timed ? "pthread_cond_timedwait" : "pthread_cond_wait", state.WakeKey, - () => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult), - () => state.SignalEpoch != observedEpoch, + new PthreadCondWaiter + { + Ctx = ctx, + CondAddress = condAddress, + MutexAddress = mutexAddress, + State = state, + ObservedEpoch = observedEpoch, + Timed = timed, + ReleasedRecursion = releasedRecursion, + PosixResult = posixResult, + }, timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0)) { TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult); diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs index c8ab5089..b7442013 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs @@ -164,6 +164,17 @@ public static class KernelPthreadExtendedCompatExports } } + private sealed class RwlockWaiter : IGuestThreadBlockWaiter + { + public required PthreadRwlockState Rwlock { get; init; } + public required ulong ThreadId { get; init; } + public required bool Write { get; init; } + + public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK; + + public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write); + } + private readonly record struct TlsKeyState(ulong Destructor); private readonly record struct PthreadAttrState( @@ -1359,8 +1370,7 @@ public static class KernelPthreadExtendedCompatExports ctx, "pthread_rwlock_wrlock", rwlock.WakeKey, - static () => (int)OrbisGen2Result.ORBIS_GEN2_OK, - () => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true))) + new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true })) { transferredToScheduler = true; return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -1396,8 +1406,7 @@ public static class KernelPthreadExtendedCompatExports ctx, "pthread_rwlock_rdlock", rwlock.WakeKey, - static () => (int)OrbisGen2Result.ORBIS_GEN2_OK, - () => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false))) + new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false })) { return (int)OrbisGen2Result.ORBIS_GEN2_OK; } diff --git a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs index 69657a0a..5ec001af 100644 --- a/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelSemaphoreCompatExports.cs @@ -19,6 +19,8 @@ public static class KernelSemaphoreCompatExports private sealed class KernelSemaphoreState { public required string Name { get; init; } + // Formatted once at creation; signal/wait/cancel/delete all wake through this key. + public required string WakeKey { get; init; } public required int InitialCount { get; init; } public required int MaxCount { get; init; } public int Count { get; set; } @@ -28,14 +30,26 @@ public static class KernelSemaphoreCompatExports public object Gate { get; } = new(); } - private sealed class SemaphoreWaiter + private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter { + public required KernelSemaphoreState Semaphore { get; init; } public required int NeedCount { get; init; } public required int CancelEpochAtBlock { get; init; } public bool Timed { get; init; } + // Timed-wait completion state; unused when Timed is false. + public CpuContext? Ctx { get; init; } + public ulong TimeoutAddress { get; init; } + public long DeadlineTimestamp { get; init; } + // Written and read only under the owning semaphore's Gate. public int? Result { get; set; } + + public int Resume() => Timed + ? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp) + : CompleteBlockedSemaWait(Semaphore, this); + + public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this); } private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}"; @@ -79,6 +93,7 @@ public static class KernelSemaphoreCompatExports var state = new KernelSemaphoreState { Name = name, + WakeKey = GetSemaphoreWakeKey(handle), InitialCount = initialCount, MaxCount = maxCount, Count = initialCount, @@ -96,11 +111,14 @@ public static class KernelSemaphoreCompatExports state.Deleted = true; } - _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle)); + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } - TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}"); + if (_traceSema) + { + TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}"); + } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } @@ -131,7 +149,10 @@ public static class KernelSemaphoreCompatExports if (semaphore.Count >= needCount) { semaphore.Count -= needCount; - TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + if (_traceSema) + { + TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } @@ -145,7 +166,10 @@ public static class KernelSemaphoreCompatExports if (timeoutMicros == 0) { _ = ctx.TryWriteUInt32(timeoutAddress, 0); - TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + if (_traceSema) + { + TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + } pollTimedOut = true; } else @@ -153,27 +177,36 @@ public static class KernelSemaphoreCompatExports var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L)); var timedWaiter = new SemaphoreWaiter { + Semaphore = semaphore, NeedCount = needCount, CancelEpochAtBlock = semaphore.CancelEpoch, Timed = true, + Ctx = ctx, + TimeoutAddress = timeoutAddress, + DeadlineTimestamp = deadline, }; if (GuestThreadExecution.RequestCurrentThreadBlock( ctx, "sceKernelWaitSema", - GetSemaphoreWakeKey(handle), - resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline), - wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter), + semaphore.WakeKey, + timedWaiter, blockDeadlineTimestamp: deadline)) { semaphore.WaitingThreads++; - TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}"); + if (_traceSema) + { + TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}"); + } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } // Host-owned threads cannot park in the guest scheduler; degrade to the // immediate-timeout poll the callers already tolerate. _ = ctx.TryWriteUInt32(timeoutAddress, 0); - TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + if (_traceSema) + { + TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + } pollTimedOut = true; } } @@ -182,22 +215,28 @@ public static class KernelSemaphoreCompatExports { var waiter = new SemaphoreWaiter { + Semaphore = semaphore, NeedCount = needCount, CancelEpochAtBlock = semaphore.CancelEpoch, }; if (!GuestThreadExecution.RequestCurrentThreadBlock( ctx, "sceKernelWaitSema", - GetSemaphoreWakeKey(handle), - resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter), - wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter))) + semaphore.WakeKey, + waiter)) { - TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + if (_traceSema) + { + TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN); } semaphore.WaitingThreads++; - TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + if (_traceSema) + { + TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } } @@ -239,12 +278,18 @@ public static class KernelSemaphoreCompatExports { if (semaphore.Count < needCount) { - TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + if (_traceSema) + { + TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); } semaphore.Count -= needCount; - TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + if (_traceSema) + { + TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); + } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } } @@ -277,14 +322,17 @@ public static class KernelSemaphoreCompatExports } semaphore.Count += signalCount; - TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + if (_traceSema) + { + TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + } } // Wake after releasing the gate (lock order: scheduler gate -> semaphore gate). // Wake everyone; the wake handler consumes the count per waiter, so a waiter // whose needCount exceeds the remaining count stays parked while a smaller // waiter can proceed. - _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle)); + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } @@ -322,10 +370,13 @@ public static class KernelSemaphoreCompatExports // exactly once in its wake handler. Zeroing here as well would double-count // and silently absorb the increment of a waiter that parks between this // gate release and the wake-all below. - TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}"); + if (_traceSema) + { + TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}"); + } } - _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle)); + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } @@ -349,8 +400,11 @@ public static class KernelSemaphoreCompatExports semaphore.Deleted = true; } - _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle)); - TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'"); + _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey); + if (_traceSema) + { + TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'"); + } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } @@ -376,7 +430,10 @@ public static class KernelSemaphoreCompatExports { waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED; semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); - TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}"); + if (_traceSema) + { + TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}"); + } return true; } @@ -384,7 +441,10 @@ public static class KernelSemaphoreCompatExports { waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED; semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); - TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}"); + if (_traceSema) + { + TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}"); + } return true; } @@ -393,7 +453,10 @@ public static class KernelSemaphoreCompatExports semaphore.Count -= waiter.NeedCount; waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK; semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); - TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + if (_traceSema) + { + TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + } return true; } @@ -434,7 +497,10 @@ public static class KernelSemaphoreCompatExports { waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); - TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + if (_traceSema) + { + TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); + } } result = waiter.Result!.Value; @@ -456,11 +522,13 @@ public static class KernelSemaphoreCompatExports return result; } + // Call sites must check this before building the interpolated message; the trace + // strings would otherwise be allocated on every semaphore op even with tracing off. + private static readonly bool _traceSema = + string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal); + private static void TraceSemaphore(string message) { - if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal)) - { - Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}"); - } + Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}"); } } diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index 3aff3866..91f7e150 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -5,6 +5,7 @@ using SharpEmu.HLE; using SharpEmu.Libs.Audio; using SharpEmu.Libs.Kernel; using SharpEmu.Logging; +using System.Buffers; using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -180,6 +181,10 @@ public static class VideoOutExports } } + // Only ever touched by the vblank pump thread; reused across edges so the 60 Hz + // pump does not allocate a fresh snapshot per edge. + private static readonly List _vblankPumpPorts = new(); + private static void PumpVblanks() { lock (_vblankEdgeGate) @@ -188,7 +193,7 @@ public static class VideoOutExports Monitor.PulseAll(_vblankEdgeGate); } - VideoOutPortState[] ports; + _vblankPumpPorts.Clear(); lock (_stateGate) { if (_ports.Count == 0) @@ -198,10 +203,16 @@ public static class VideoOutExports // Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global // flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads. - ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray(); + foreach (var port in _ports.Values) + { + if (port.VblankEvents.Count != 0) + { + _vblankPumpPorts.Add(port); + } + } } - foreach (var port in ports) + foreach (var port in _vblankPumpPorts) { SignalVblank(port); } @@ -221,6 +232,12 @@ public static class VideoOutExports Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"), "1", StringComparison.Ordinal); + // Call sites must check this before building the interpolated message; the trace + // strings would otherwise be allocated on the per-frame flip path even with tracing off. + private static readonly bool _logVideoOut = string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), + "1", + StringComparison.Ordinal); private static readonly bool _dumpVideoOut = string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"), "1", @@ -586,7 +603,10 @@ public static class VideoOutExports // Some engines wait on this queue before issuing their first flip. Provide a first // edge now; later calls to WaitVblank advance the same notification sequence. SignalVblank(port); - TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}"); + if (_logVideoOut) + { + TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}"); + } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -623,7 +643,10 @@ public static class VideoOutExports } } - TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}"); + if (_logVideoOut) + { + TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}"); + } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -684,7 +707,10 @@ public static class VideoOutExports KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg)); KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer); - TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}"); + if (_logVideoOut) + { + TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}"); + } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -1077,33 +1103,53 @@ public static class VideoOutExports private static void SignalVblank(VideoOutPortState port) { - List vblankEvents; + // Snapshot the registrations into a pooled rental so the triggers can run outside + // _stateGate without copying the list into a fresh allocation on every edge. + // A per-port reusable buffer would race: the pump thread and a guest thread's + // first-edge signal (AddVblankEvent) can signal the same port concurrently. + FlipEventRegistration[]? vblankEvents = null; + int vblankEventCount; ulong eventHint; lock (_stateGate) { port.VblankCount++; eventHint = SceVideoOutInternalEventVblank | ((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16); - vblankEvents = new List(port.VblankEvents); + vblankEventCount = port.VblankEvents.Count; + if (vblankEventCount != 0) + { + vblankEvents = ArrayPool.Shared.Rent(vblankEventCount); + port.VblankEvents.CopyTo(vblankEvents); + } } var signalCount = Interlocked.Increment(ref _vblankSignalCount); - foreach (var vblankEvent in vblankEvents) + if (vblankEvents is not null) { - _ = KernelEventQueueCompatExports.TriggerDisplayEvent( - vblankEvent.Equeue, - SceVideoOutInternalEventVblank, - OrbisKernelEventFilterVideoOut, - eventHint, - vblankEvent.UserData); + try + { + for (var i = 0; i < vblankEventCount; i++) + { + _ = KernelEventQueueCompatExports.TriggerDisplayEvent( + vblankEvents[i].Equeue, + SceVideoOutInternalEventVblank, + OrbisKernelEventFilterVideoOut, + eventHint, + vblankEvents[i].UserData); + } + } + finally + { + ArrayPool.Shared.Return(vblankEvents); + } } if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0)) { Console.Error.WriteLine( $"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " + - $"queues={vblankEvents.Count} hint=0x{eventHint:X16}"); + $"queues={vblankEventCount} hint=0x{eventHint:X16}"); } } @@ -1125,8 +1171,11 @@ public static class VideoOutExports return OrbisVideoOutErrorInvalidIndex; } + // Pooled snapshot for the same reason as SignalVblank: triggers run outside + // _stateGate, and SubmitFlip is per-frame so a fresh List copy is steady churn. ulong eventHint; - List flipEvents; + FlipEventRegistration[]? flipEvents = null; + int flipEventCount; lock (_stateGate) { if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0) @@ -1138,7 +1187,12 @@ public static class VideoOutExports port.FlipCount++; eventHint = SceVideoOutInternalEventFlip | ((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16); - flipEvents = new List(port.FlipEvents); + flipEventCount = port.FlipEvents.Count; + if (flipEventCount != 0) + { + flipEvents = ArrayPool.Shared.Rent(flipEventCount); + port.FlipEvents.CopyTo(flipEvents); + } } var guestImageSubmitted = false; @@ -1160,14 +1214,24 @@ public static class VideoOutExports _ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg); } - foreach (var flipEvent in flipEvents) + if (flipEvents is not null) { - _ = KernelEventQueueCompatExports.TriggerDisplayEvent( - flipEvent.Equeue, - SceVideoOutInternalEventFlip, - OrbisKernelEventFilterVideoOut, - eventHint, - flipEvent.UserData); + try + { + for (var i = 0; i < flipEventCount; i++) + { + _ = KernelEventQueueCompatExports.TriggerDisplayEvent( + flipEvents[i].Equeue, + SceVideoOutInternalEventFlip, + OrbisKernelEventFilterVideoOut, + eventHint, + flipEvents[i].UserData); + } + } + finally + { + ArrayPool.Shared.Return(flipEvents); + } } var flipCount = Interlocked.Increment(ref _flipSubmitCount); @@ -1176,10 +1240,13 @@ public static class VideoOutExports Console.Error.WriteLine( $"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " + $"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " + - $"flipQueues={flipEvents.Count}"); + $"flipQueues={flipEventCount}"); } - TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}"); + if (_logVideoOut) + { + TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEventCount}"); + } ReportFrameRate(presented: false); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -1261,8 +1328,11 @@ public static class VideoOutExports slot.AddressRight = 0; } - TraceVideoOut( - $"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}"); + if (_logVideoOut) + { + TraceVideoOut( + $"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}"); + } VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height); var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat); @@ -1428,7 +1498,10 @@ public static class VideoOutExports var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex); WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb); WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint); - TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}"); + if (_logVideoOut) + { + TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}"); + } return true; } @@ -1467,7 +1540,10 @@ public static class VideoOutExports var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex); File.WriteAllBytes(basePath + ".raw", bytes); WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint); - TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}"); + if (_logVideoOut) + { + TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}"); + } return true; } @@ -1694,11 +1770,6 @@ public static class VideoOutExports private static void TraceVideoOut(string message) { - if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal)) - { - return; - } - Console.Error.WriteLine($"[LOADER][TRACE] {message}"); } }