• win64-main-62e1775 62e1775c5c

    Ghost released this 2026-07-15 05:57:40 -04:00 | 452 commits to 1016_part3 since this release

    • [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.

    Downloads