mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-23 03:16:22 +08:00
[HLE] Remove steady-state allocations from the hot HLE paths (#190)
* [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<byte> + 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<int>/Func<bool> 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<KernelQueuedEvent> 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.
This commit is contained in:
committed by
GitHub
parent
6dacd59a08
commit
62e1775c5c
@@ -434,9 +434,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public string? BlockWakeKey { get; set; }
|
||||
|
||||
public Func<int>? BlockResumeHandler { get; set; }
|
||||
|
||||
public Func<bool>? 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<int>? resumeHandler,
|
||||
Func<bool>? 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<int>? 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);
|
||||
|
||||
Reference in New Issue
Block a user