From 62e1775c5cf3acf2f9d34824c7940348981e3649 Mon Sep 17 00:00:00 2001 From: Gutemberg Ribeiro Date: Wed, 15 Jul 2026 10:57:40 +0100 Subject: [PATCH] [HLE] Remove steady-state allocations from the hot HLE paths (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [HLE] Stop allocating on the memcpy/memset and trace hot paths memcpy/memmove no longer allocate a bounce buffer sized to the whole copy (large copies previously landed on the LOH); they loop through a single pooled 256 KB rental, copying high-to-low when the destination overlaps above the source so memmove semantics survive the chunking. memset reuses a shared zero chunk for the dominant zero-fill case and rents/fills only min(length, 16K) bytes for non-zero values instead of allocating and filling a fresh 16 KB array per call; the map-time zero-fill loop shares the same zero chunk. SHARPEMU_LOG_SEMA / SHARPEMU_LOG_VIDEOOUT are now read once into cached bools and every TraceSemaphore/TraceVideoOut call site is guarded, so trace messages are no longer interpolated (and the env var no longer queried) on every semaphore op and every flip with tracing off. Trace output when the flags are set is unchanged. * [HLE] Remove per-frame allocations from the vblank/flip/equeue plumbing The 60 Hz vblank pump no longer allocates per edge: PumpVblanks reuses a pump-thread-only port list instead of a LINQ Where/ToArray, and SignalVblank/SubmitFlip snapshot their event registrations into pooled rentals instead of copying the List on every edge and every flip (the snapshot must still be taken, since triggers run outside _stateGate and a per-port reusable buffer would race the pump thread against a guest thread's first-edge signal). sceKernelWaitEqueue delivery rents the dequeue buffer from the pool instead of allocating an array per wait, and event-queue wake keys are formatted once per handle (cached in a ConcurrentDictionary, dropped on queue delete) instead of building the string on every enqueue. The semaphore wake key moves onto KernelSemaphoreState at creation, the same pattern the pthread mutex state already uses, removing the per-signal/per-wait formatting. SHARPEMU_LOG_EQUEUE is read once into a cached bool like the sema/videoout flags. * [HLE] Read guest C-strings without per-call buffer allocations CpuContext.TryReadNullTerminatedUtf8 allocated a byte[capacity] and issued one TryRead per byte for every string-argument import. It now reads through a stack buffer (pooled above 512 bytes) in 128-byte bulk chunks, falling back to per-byte reads only when a chunk touches an unreadable range so a terminator sitting just before unmapped memory still resolves exactly as before. The chunk bound also keeps the overread past the terminator smaller than the old loop's worst case is wide, so no fault can appear where the byte loop succeeded. TryReadAsciiZ (dlsym/symbol resolution) drops its List + ToArray round-trip for the same stack/pooled buffer, keeping the byte-by-byte TryReadByteCompat reads because their Marshal.ReadByte fallback must probe exactly up to the terminator. Only the final string is allocated on either path now. * [HLE] Replace blocking-wait closures with waiter continuation objects Every wait that actually parked a guest thread allocated two capturing lambdas (plus their display classes) for the scheduler's resume/wake callbacks. RequestCurrentThreadBlock and the backend's blocked-thread state now carry a single IGuestThreadBlockWaiter instead of the Func/Func pair: TryWake keeps the run-under-the-scheduler- gate contract and Resume still produces the guest's RAX on the woken thread. The waiter stays attached through the wake transition (the old code nulled only the wake handler there) and is consumed at resume. The existing waiter objects absorb the captured state as fields, so a blocking wait now allocates exactly one object: SemaphoreWaiter, PthreadMutexWaiter, and EventFlagWaiter implement the interface directly, and the equeue, cond, and rwlock waits get small waiter classes replacing their closures. Handler bodies delegate to the same static methods with the same arguments as before; the untimed event flag wait's mutable captured result becomes a field on its waiter. * [HLE] Back pending event queues with a ring deque instead of LinkedList LinkedList allocated a node object on every non-coalesced enqueue — one per vblank/flip edge per registered queue, 60+ times a second in steady state. KernelEventDeque is a grow-only ring buffer over a KernelQueuedEvent[] with the three operations the queue actually uses (AddLast, RemoveFirst, find-and-update-in-place by ident/filter), so steady-state enqueue/dequeue allocates nothing and the coalescing update writes the struct back through an indexer instead of a node reference. All accesses stay under _eventQueueGate, matching the LinkedList usage it replaces. * [HLE] Cap memcpy chunk iterations at the requested size, not the rented length Address Copilot review: ArrayPool.Rent may return a larger array than requested, so sizing each iteration by chunk.Length let the copy granularity depend on pool bucketing internals instead of the intended 256 KB chunking. Behavior was already correct for any chunk size (each iteration re-reads the source, and the overlap ordering is size- independent), but the loop now mins against the requested chunkLength, matching what memset already does. * [HLE] Skip the flip/vblank snapshot rental when no events are registered Address Copilot review: SignalVblank and SubmitFlip rented (and returned) a pooled snapshot even with zero registrations — steady per-frame pool traffic for games that never register flip events and only poll flip status. Zero-count signals now skip the rental, the copy, and the trigger loop entirely, which also retires the Math.Max(count, 1) minimum-rent guard. --- .../Native/DirectExecutionBackend.Imports.cs | 51 +++--- .../Cpu/Native/DirectExecutionBackend.cs | 31 ++-- src/SharpEmu.HLE/CpuContext.cs | 61 +++++-- src/SharpEmu.HLE/GuestThreadExecution.cs | 50 +++--- .../Kernel/KernelEventFlagCompatExports.cs | 87 +++++---- .../Kernel/KernelEventQueueCompatExports.cs | 166 +++++++++++++----- .../Kernel/KernelMemoryCompatExports.cs | 120 +++++++++---- .../Kernel/KernelPthreadCompatExports.cs | 50 +++++- .../KernelPthreadExtendedCompatExports.cs | 17 +- .../Kernel/KernelSemaphoreCompatExports.cs | 130 ++++++++++---- src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 143 +++++++++++---- 11 files changed, 645 insertions(+), 261 deletions(-) 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}"); } }