Compare commits

...

6 Commits

Author SHA1 Message Date
Pacuka 7b86a91dfa Add files via upload (#202)
Added Hungarian tranlation. -Pacuka
2026-07-15 14:39:42 +03:00
Gutemberg Ribeiro 72645cb373 [Host] Abstract audio output and pad/keyboard input behind the host platform seam (#192)
* [Host] Abstract audio output behind IHostAudioOutput

Add IHostAudioOutput (opens streams, names the backend for diagnostics)
and IHostAudioStream (submit interleaved stereo 16-bit PCM, Dispose) to
the host seam, with the winmm waveOut implementation moving whole into
Host/Windows/WindowsWaveOutAudio — same device open, queueing,
32 KB backpressure wait, and buffer lifetime as WinMmAudioPort had. The
DllImports become source-generated LibraryImports in the move, matching
the other Windows backends.

The guest-format conversion (mono/stereo/7.1, s16/float32 -> stereo
PCM16) is platform policy, not device code, so it stays in Libs as
AudioPcmConversion; AudioOutOutput converts into a pooled buffer and
submits the result through the stream. Open failures still degrade to
the silent paced port with the same warning, and the port log line now
takes its backend name from the platform instead of a hardcoded string.

* [Host] Abstract pad and keyboard input behind IHostInput

Add IHostInput to the host seam: gamepad state snapshots, rumble /
trigger-rumble / lightbar sinks, and the keyboard-fallback queries
(window focus, key state). Gamepad state crosses the seam as the new
unmanaged HostGamepadState with HostGamepadButtons flags — named after
the PlayStation layout the guest API exposes but with the seam's own
values, so SCE_PAD_BUTTON bits never leak into host backends and the
per-frame poll can stackalloc its snapshot buffer.

The DualSense raw-HID reader, the XInput reader, and the Win32 HID
interop move whole into Host/Windows (report parsing, hot-plug loops,
rumble/lightbar output reports, and log strings unchanged), translating
to the neutral flags instead of ORBIS bits and converting their
DllImports to source-generated LibraryImports. WindowsHostInput
composes them plus the user32 keyboard queries; rumble still fans out
to both readers, trigger rumble stays XInput-only, lightbar stays
DualSense-only.

PadExports keeps all policy: the keyboard mapping (now via named
OrbisPadButton constants instead of raw hex), the controller-beats-
keyboard-past-deadzone merge, and the new host->ORBIS button
translation. The GUI's source-linked reader copies re-point to the
moved files (it still cannot reference SharpEmu.HLE wholesale), which
requires AllowUnsafeBlocks for the generated marshalling stubs; its
navigation code switches to the neutral flags.

* [Host] Move the timer-resolution request behind IHostThreading

IHostThreading gains RequestTimerResolution (idempotent, best-effort
~1 ms timed-wait granularity; a no-op wherever the platform default is
already fine). The winmm timeBeginPeriod call, its once-only latch, and
both warning strings move from the Libs-level HostTimerResolution
helper into WindowsHostThreading as a source-generated LibraryImport;
the vblank pump requests it through the platform instead.

HostSystemInfo in SharpEmu.Logging keeps its direct user32/kernel32
imports deliberately: Logging sits below HLE in the dependency chain so
it cannot see the host seam, every path is already OS-gated with
fallbacks, and it only runs once for the diagnostics banner.
2026-07-15 13:24:53 +03:00
SamuelEzequias 2ad9836d13 Add Brazilian translation to Environment tab (#196) 2026-07-15 13:00:30 +03:00
Gutemberg Ribeiro 62e1775c5c [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.
2026-07-15 12:57:40 +03:00
SamuelEzequias 6dacd59a08 [GUI] Add Portuguese (Portugal) translation (#197)
* [GUI] Add Portuguese (Portugal) translation

* [GUI] Add Portuguese (Portugal) translation
2026-07-15 12:56:25 +03:00
Spooks 9d88542efd Fix virtual memory allocation and access (#193)
* Fix virtual memory allocation and access

* Update test dependency lock file
2026-07-14 21:50:54 -06:00
42 changed files with 2356 additions and 858 deletions
@@ -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<byte> list = new List<byte>(Math.Min(maxLength, 256));
Span<byte> 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<byte> + ToArray per symbol resolution.
const int StackBufferLength = 512;
byte[]? rented = maxLength > StackBufferLength ? System.Buffers.ArrayPool<byte>.Shared.Rent(maxLength) : null;
Span<byte> 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<byte>.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<byte> destination)
@@ -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);
@@ -50,4 +50,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
address = 0;
return false;
}
public bool TryFreeGuestMemory(ulong address)
{
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
}
}
+115 -10
View File
@@ -42,7 +42,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private readonly IHostMemory _hostMemory;
private ulong _guestAllocationArenaBase;
private ulong _guestAllocationOffset;
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
@@ -301,7 +302,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
GuestAllocationArenaSize,
executable: false,
allowAlternative: true);
_guestAllocationOffset = GuestAllocationArenaStartOffset;
_guestAllocationFreeRanges.Add(
GuestAllocationArenaStartOffset,
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
}
catch (Exception)
{
@@ -309,14 +312,89 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
ulong rangeOffset = 0;
ulong rangeSize = 0;
ulong alignedOffset = 0;
var found = false;
foreach (var range in _guestAllocationFreeRanges)
{
alignedOffset = AlignUp(range.Key, alignment);
if (alignedOffset >= range.Key &&
alignedOffset - range.Key <= range.Value &&
size <= range.Value - (alignedOffset - range.Key))
{
rangeOffset = range.Key;
rangeSize = range.Value;
found = true;
break;
}
}
if (!found)
{
return false;
}
_guestAllocationFreeRanges.Remove(rangeOffset);
if (alignedOffset > rangeOffset)
{
_guestAllocationFreeRanges.Add(rangeOffset, alignedOffset - rangeOffset);
}
var allocationEnd = alignedOffset + size;
var rangeEnd = rangeOffset + rangeSize;
if (allocationEnd < rangeEnd)
{
_guestAllocationFreeRanges.Add(allocationEnd, rangeEnd - allocationEnd);
}
address = _guestAllocationArenaBase + alignedOffset;
_guestAllocationOffset = alignedOffset + size;
_guestAllocations.Add(address, (alignedOffset, size));
return true;
}
}
public bool TryFreeGuestMemory(ulong address)
{
lock (_guestAllocationGate)
{
if (!_guestAllocations.Remove(address, out var allocation))
{
return false;
}
var freeOffset = allocation.Offset;
var freeSize = allocation.Size;
ulong? previousOffset = null;
ulong? nextOffset = null;
foreach (var range in _guestAllocationFreeRanges)
{
if (range.Key < freeOffset)
{
previousOffset = range.Key;
continue;
}
nextOffset = range.Key;
break;
}
if (previousOffset is { } previous &&
previous + _guestAllocationFreeRanges[previous] == freeOffset)
{
freeOffset = previous;
freeSize += _guestAllocationFreeRanges[previous];
_guestAllocationFreeRanges.Remove(previous);
}
if (nextOffset is { } next && freeOffset + freeSize == next)
{
freeSize += _guestAllocationFreeRanges[next];
_guestAllocationFreeRanges.Remove(next);
}
_guestAllocationFreeRanges.Add(freeOffset, freeSize);
return true;
}
}
@@ -380,7 +458,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
_guestAllocationArenaBase = 0;
_guestAllocationOffset = 0;
_guestAllocationFreeRanges.Clear();
_guestAllocations.Clear();
}
}
@@ -439,12 +518,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
{
var runStart = mapStart;
var runFlags = ProgramHeaderFlags.None;
var hasRun = false;
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
{
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
var mergedFlags = existingFlags | flags;
_pageProtections[pageAddress] = mergedFlags;
SetProtection(pageAddress, PageSize, mergedFlags);
if (!hasRun)
{
runStart = pageAddress;
runFlags = mergedFlags;
hasRun = true;
}
else if (mergedFlags != runFlags)
{
SetProtection(runStart, pageAddress - runStart, runFlags);
runStart = pageAddress;
runFlags = mergedFlags;
}
}
if (hasRun)
{
SetProtection(runStart, mapEnd - runStart, runFlags);
}
}
@@ -789,9 +889,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
return FindRegion(virtualAddress, 1) is not null
? (void*)virtualAddress
: null;
var region = FindRegion(virtualAddress, 1);
if (region is null ||
(region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
{
return null;
}
return (void*)virtualAddress;
}
finally
{
+116 -29
View File
@@ -41,15 +41,14 @@ public sealed class VirtualMemory : IVirtualMemory
lock (_gate)
{
foreach (var existing in _regions)
var insertionIndex = FindInsertionIndex(virtualAddress);
if ((insertionIndex > 0 && virtualAddress < _regions[insertionIndex - 1].EndAddress) ||
(insertionIndex < _regions.Count && endAddress > _regions[insertionIndex].Region.VirtualAddress))
{
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
{
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
}
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
}
_regions.Add(new MappedRegion(
_regions.Insert(insertionIndex, new MappedRegion(
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
endAddress,
backingMemory));
@@ -74,12 +73,12 @@ public sealed class VirtualMemory : IVirtualMemory
{
lock (_gate)
{
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
if (!TryValidateRange(virtualAddress, destination.Length, ProgramHeaderFlags.Read, out var regionIndex))
{
return false;
}
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
CopyFromRegions(virtualAddress, destination, regionIndex);
return true;
}
}
@@ -88,39 +87,127 @@ public sealed class VirtualMemory : IVirtualMemory
{
lock (_gate)
{
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
if (!TryValidateRange(virtualAddress, source.Length, ProgramHeaderFlags.Write, out var regionIndex))
{
return false;
}
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
CopyToRegions(virtualAddress, source, regionIndex);
return true;
}
}
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
private bool TryValidateRange(
ulong virtualAddress,
int length,
ProgramHeaderFlags requiredProtection,
out int regionIndex)
{
foreach (var candidate in _regions)
regionIndex = FindContainingRegionIndex(virtualAddress);
if (regionIndex < 0)
{
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
{
continue;
}
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
if (candidateOffset + length > candidate.BackingMemory.Length)
{
break;
}
region = candidate;
offset = candidateOffset;
return true;
return false;
}
region = default;
offset = 0;
return false;
var currentAddress = virtualAddress;
var remaining = length;
var currentIndex = regionIndex;
while (true)
{
if (currentIndex >= _regions.Count)
{
return false;
}
var region = _regions[currentIndex];
if (currentAddress < region.Region.VirtualAddress ||
currentAddress >= region.EndAddress ||
(region.Region.Protection & requiredProtection) == 0)
{
return false;
}
if (remaining == 0)
{
return true;
}
var available = region.EndAddress - currentAddress;
var chunkLength = (int)Math.Min((ulong)remaining, available);
remaining -= chunkLength;
if (remaining == 0)
{
return true;
}
currentAddress += (ulong)chunkLength;
currentIndex++;
}
}
private int FindContainingRegionIndex(ulong virtualAddress)
{
var insertionIndex = FindInsertionIndex(virtualAddress);
if (insertionIndex < _regions.Count &&
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
{
return insertionIndex;
}
var candidateIndex = insertionIndex - 1;
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
? candidateIndex
: -1;
}
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
{
var copied = 0;
var currentAddress = virtualAddress;
while (copied < destination.Length)
{
var region = _regions[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
copied += chunkLength;
currentAddress += (ulong)chunkLength;
}
}
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
{
var copied = 0;
var currentAddress = virtualAddress;
while (copied < source.Length)
{
var region = _regions[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
copied += chunkLength;
currentAddress += (ulong)chunkLength;
}
}
private int FindInsertionIndex(ulong virtualAddress)
{
var lower = 0;
var upper = _regions.Count;
while (lower < upper)
{
var middle = lower + ((upper - lower) / 2);
if (_regions[middle].Region.VirtualAddress < virtualAddress)
{
lower = middle + 1;
}
else
{
upper = middle;
}
}
return lower;
}
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
+10 -1
View File
@@ -26,6 +26,15 @@
"Library.Loading": "Carregando biblioteca…",
"Options.General": "Opções Gerais",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
"Options.Env.Desc": "Switches passados para o emulador como variáveis de ambiente na inicialização.",
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica esperando indefinidamente.\nDeixe desativado normalmente. Alguns títulos travam quando a inicialização falha.",
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada por tempo demais.\nExperimente isso quando um jogo fecha sozinho durante o carregamento.",
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração de GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
"Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "LOGS",
"Options.Section.Launcher": "INICIALIZADOR",
@@ -126,4 +135,4 @@
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
"Dialog.PlainTextFiles": "Arquivos de texto simples",
"Dialog.LogFiles": "Arquivos de log"
}
}
+146
View File
@@ -0,0 +1,146 @@
{
"_languageName": "Hungarian",
"Page.Library": "Könyvtár",
"Page.Options": "Beállítások",
"Page.GameCount.One": "1 játék",
"Page.GameCount.Other": "{0} játékok",
"Library.SearchWatermark": "Keresés a könyvtárban",
"Library.AddFolder": " Mappa hozzáadása",
"Library.Rescan": "⟳ Újrakeresés",
"Library.OpenFile": "Fájl megnyitása…",
"Library.Context.Launch": "Inditás",
"Library.Context.OpenFolder": "Játékmappa megnyitása",
"Library.Context.CopyPath": "Elérési út másolása",
"Library.Context.CopyTitleId": "Cím ID másolása",
"Library.Context.Remove": "Eltávolítás a Könyvtárból",
"Library.Empty.Title": "A könyvtárad üres",
"Library.Empty.Hint": "Add meg a játékaidat tartalmazó mappát a kezdáshez.",
"Library.Empty.SearchTitle": "Nincs találat a elemre",
"Library.Empty.SearchHint": "A könyvtárban nincs olyan elem, amely egyezne a „{0}” kifejezéssel.",
"Library.Empty.AddFolder": " Játékmappa hozzáadása",
"Library.Loading": "Könyvtár betöltése",
"Options.General": "Általános",
"Options.Env.Tab": "Környezet",
"Options.Section.Environment": "KÖRNYEZETI VÁLTOZÓK",
"Options.Env.Desc": "Indításkor környezeti változóként az emulátorhoz átadott kapcsolók.",
"Options.Env.Bthid.Desc": "Jelenti, amely címeknél a Bluetooth HID nem elérhető, amelyeknél a kormány/FFB-közbenső szoftver végtelenül lekérdezi az adatokat.\nNormál esetben hagyja ki. Egyes címek lefagyanak, ha az inicializálás sikertelen.",
"Options.Env.LoopGuard.Desc": "Ne erőltesse a kilépést azoknál a címeknél, amelyek túl sokáig ismételnek ugyanazt a hívást.\nPróbálja ki ezt, ha egy játék betöltés közben magától kilép.",
"Options.Env.VkValidation.Desc": "Engedélyezze a Vulkan-érvényesítési rétegeket a GPU hibakereséshez.\nLassú. A Vulkan SDK telepítését igényli.",
"Options.Env.DumpSpirv.Desc": "Az AGC-shaderek és azok SPIR-V-fordításainak mentése a shader-dumps mappába.\nHasználd shader- vagy renderelési hibák jelentésekor.",
"Options.Env.LogDirectMemory.Desc": "A közvetlen memóriaallokációk és hibák naplózása a konzolra.\nHasználd, ha egy játék a rendszerindítás során megszakad vagy kilép.",
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
"Options.Section.Emulation": "EMULÁCIÓ",
"Options.Section.Logging": "LOGOLÁS",
"Options.Section.Launcher": "INDITÓ",
"Options.CpuEngine.Label": "CPU motor",
"Options.CpuEngine.Desc": "A játék kódjának futtatásához használt végrehajtó motor.",
"Options.CpuEngine.Native": "Natív",
"Options.Strict.Label": "Szigorú dynlib felbontás",
"Options.Strict.Desc": "Indítás megszakítása, ha egy importált szimbólum nem oldható fel.",
"Options.LogLevel.Label": "Naplózási szint",
"Options.LogLevel.Desc": "Az emulátor konzol kimenetének részletessége.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Információ",
"Options.LogLevel.Warning": "Figyelmeztetés",
"Options.LogLevel.Error": "Hiba",
"Options.LogLevel.Critical": "Kritikus",
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Az első N darab import nyomon követése modulonként (0 = ki).",
"Options.LogToFile.Label": "Naplozás fájlba",
"Options.LogToFile.Desc": "Az emulátor kimenetének tükrözése egy log fájlba.",
"Options.LogFilePath.Label": "Naplófájl elérési útja",
"Options.LogFilePath.Default": "Nincs egyéni út — a logok az emulátor melletti user/logs mappába kerülnek.",
"Options.LogFilePath.Select": "Kiválasztás…",
"Options.OverrideLogFile.Label": "Naplófájl felülírása",
"Options.OverrideLogFile.Desc": "A pontos fájlútvonal használata a cím ID és időbélyeg hozzáfűzése helyett.",
"Options.TitleMusic.Label": "Címzene",
"Options.TitleMusic.Desc": "A kiválasztott játék előnézeti zenéjének ismétlése a könyvtárban.",
"Options.Discord.Label": "Discord jelenlét",
"Options.Discord.Desc": "A futó játék megjelenítése a Discord profilodon.",
"Options.Language.Label": "Emulátor nyelve",
"Options.Language.Desc": "Az indítóban használt nyelv. Azonnal érvénybe lép.",
"Common.On": "Be",
"Common.Off": "Ki",
"Console.Title": "KONZOL",
"Console.SearchWatermark": "Keresés...",
"Console.AutoScroll": "Automatikus görgetés",
"Console.Split": "Felosztás",
"Console.Copy": "Másolás",
"Console.Clear": "Törlés",
"Console.WindowTitle": "SharpEmu Konzol",
"Launch.NoGameSelected": "Nincs játék kiválasztva",
"Launch.NoGameHint": "Válassz egy játékot a könyvtárból, vagy nyiss meg közvetlenül egy eboot.bin fájlt.",
"Launch.Idle": "Tétlen",
"Launch.Console": "≡ Konzol",
"Launch.Launch": "▶ Inditás",
"Launch.Stop": "■ Leállítás",
"Launch.Running": "Fut — {0}",
"Launch.Stopping": "Leállítás…",
"Launch.Exited": "Kilépett a következő kóddal: {0} ({1})",
"Launch.ExeNotFound": "A SharpEmu futtatható fájl nem található. Előbb építsd fel a SharpEmu.CLI projektet (dotnet build).",
"Launch.LogFile": "Naplófájl: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Nem sikerült elindítani az emulátort: {0}",
"Launch.ProcessExited": "A folyamat kilépett a következő kóddal: {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "nemérvényes argumentumok",
"Exit.EbootNotFound": "eboot nem található",
"Exit.RuntimeException": "runtime exception",
"Exit.EmulationError": "emulációs hiba",
"Exit.Unknown": "ismeretlen",
"Status.EmulatorLocating": "Emulátor: keresés…",
"Status.EmulatorPath": "Emulátor: {0}",
"Status.EmulatorNotFound": "Emulátor: a SharpEmu futtatható fájl nem található — előbb építsd fel a SharpEmu.CLI-t.",
"Status.ScanningLibrary": "Könyvtár beolvasása…",
"Status.AddFolderPrompt": "Adj hozzá egy játékmappát a könyvtár feltöltéséhez.",
"Status.LibraryScanned": "Könyvtár beolvasva: {0} játék {1} mappában.",
"Status.CouldNotOpenFolder": "Nem sikerült megnyitni a mappát: {0}",
"Status.CopiedToClipboard": "{0} másolva a vágólapra.",
"Status.RemovedFromLibrary": "„{0}” eltávolítva a könyvtárból. A visszaállításához add hozzá újra a mappáját.",
"Status.Running": "Fut {0}",
"Status.Stopping": "Leállítás…",
"Status.Idle": "Nyugodt",
"Clipboard.Path": "Út",
"Clipboard.TitleId": "Cím ID",
"Discord.Playing": "Játékban {0}",
"Discord.Browsing": "Böngéssz a könyvtárban",
"Dialog.ChooseGameFolder": "Válassz egy mappát ami a játékaidat tartalmazza",
"Dialog.OpenExecutable": "Futtatható fájl megnyitása az indításhoz",
"Dialog.PsExecutables": "PS futtatható fájlok",
"Dialog.SaveLogFile": "Válaszd ki, hogy hova szeretnéd menteni a napló fájlokat",
"Dialog.PlainTextFiles": "Egyszerű szöveges fájlok",
"Dialog.LogFiles": "Naplózási fájlok",
"Options.About" : "Erről",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Forrás kód, hibajelentések és a projekt fejlesztése.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.",
"About.GithubButton": "Járulj hozzá GitHubon!",
"About.DiscordButton": "Csatlakozz a Discordunhoz!"
}
+146
View File
@@ -0,0 +1,146 @@
{
"_languageName": "Português (Portugal)",
"Page.Library": "Biblioteca",
"Page.Options": "Opções",
"Page.GameCount.One": "1 jogo",
"Page.GameCount.Other": "{0} jogos",
"Library.SearchWatermark": "Pesquisar biblioteca…",
"Library.AddFolder": " Adicionar pasta",
"Library.Rescan": "⟳ Reanalisar",
"Library.OpenFile": "Abrir ficheiro…",
"Library.Context.Launch": "Iniciar",
"Library.Context.OpenFolder": "Abrir pasta do jogo",
"Library.Context.CopyPath": "Copiar caminho",
"Library.Context.CopyTitleId": "Copiar ID do título",
"Library.Context.Remove": "Remover da biblioteca",
"Library.Empty.Title": "A sua biblioteca está vazia",
"Library.Empty.Hint": "Adicione uma pasta com os seus jogos para começar.",
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua pesquisa",
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
"Library.Empty.AddFolder": " Adicionar pasta de jogos",
"Library.Loading": "A carregar biblioteca…",
"Options.General": "Geral",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
"Options.Env.Desc": "Switches passados ao emulador como variáveis de ambiente no arranque.",
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica à espera indefinidamente.\nDeixe desativado normalmente. Alguns títulos bloqueiam quando a inicialização falha.",
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada durante demasiado tempo.\nExperimente isto quando um jogo fecha sozinho durante o carregamento.",
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração da GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
"Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "REGISTOS",
"Options.Section.Launcher": "LANÇADOR",
"Options.CpuEngine.Label": "Motor de CPU",
"Options.CpuEngine.Desc": "Motor de execução utilizado para correr o código do jogo.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Resolução estrita de dynlib",
"Options.Strict.Desc": "Falha o arranque quando um símbolo importado não pode ser resolvido.",
"Options.LogLevel.Label": "Nível de registo",
"Options.LogLevel.Desc": "Nível de detalhe da saída da consola do emulador.",
"Options.LogLevel.Trace": "Rastreio",
"Options.LogLevel.Debug": "Depuração",
"Options.LogLevel.Info": "Informação",
"Options.LogLevel.Warning": "Aviso",
"Options.LogLevel.Error": "Erro",
"Options.LogLevel.Critical": "Crítico",
"Options.TraceImports.Label": "Limite de rastreio de importações",
"Options.TraceImports.Desc": "Rastreia as primeiras N importações por módulo (0 = desativado).",
"Options.LogToFile.Label": "Registar para ficheiro",
"Options.LogToFile.Desc": "Duplicar a saída do emulador para um ficheiro de registo.",
"Options.LogFilePath.Label": "Caminho do ficheiro de registo",
"Options.LogFilePath.Default": "Sem caminho personalizado — os registos vão para user/logs junto ao emulador.",
"Options.LogFilePath.Select": "Selecionar…",
"Options.OverrideLogFile.Label": "Substituir ficheiro de registo",
"Options.OverrideLogFile.Desc": "Utilizar o caminho de ficheiro exato em vez de acrescentar o ID do título e a hora.",
"Options.TitleMusic.Label": "Música do título",
"Options.TitleMusic.Desc": "Repetir em loop a música de pré-visualização do jogo selecionado na biblioteca.",
"Options.Discord.Label": "Presença no Discord",
"Options.Discord.Desc": "Mostrar o jogo em execução no seu perfil do Discord.",
"Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.",
"Common.On": "Ativado",
"Common.Off": "Desativado",
"Console.Title": "CONSOLA",
"Console.SearchWatermark": "Pesquisar...",
"Console.AutoScroll": "Deslocamento automático",
"Console.Split": "Dividir",
"Console.Copy": "Copiar",
"Console.Clear": "Limpar",
"Console.WindowTitle": "Consola do SharpEmu",
"Launch.NoGameSelected": "Nenhum jogo selecionado",
"Launch.NoGameHint": "Escolha um jogo da biblioteca ou abra um eboot.bin diretamente.",
"Launch.Idle": "Inativo",
"Launch.Console": "≡ Consola",
"Launch.Launch": "▶ Iniciar",
"Launch.Stop": "■ Parar",
"Launch.Running": "Em execução — {0}",
"Launch.Stopping": "A parar…",
"Launch.Exited": "Terminou com o código {0} ({1})",
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Ficheiro de registo: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
"Launch.ProcessExited": "O processo terminou com o código {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argumentos inválidos",
"Exit.EbootNotFound": "eboot não encontrado",
"Exit.RuntimeException": "exceção em tempo de execução",
"Exit.EmulationError": "erro de emulação",
"Exit.Unknown": "desconhecido",
"Status.EmulatorLocating": "Emulador: a localizar…",
"Status.EmulatorPath": "Emulador: {0}",
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile primeiro o SharpEmu.CLI.",
"Status.ScanningLibrary": "A analisar biblioteca…",
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
"Status.LibraryScanned": "Biblioteca analisada: {0} jogo(s) em {1} pasta(s).",
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente a pasta para o restaurar.",
"Status.Running": "A executar {0}",
"Status.Stopping": "A parar…",
"Status.Idle": "Inativo",
"Clipboard.Path": "Caminho",
"Clipboard.TitleId": "ID do título",
"Discord.Playing": "A jogar {0}",
"Discord.Browsing": "A navegar na biblioteca",
"Dialog.ChooseGameFolder": "Escolha uma pasta com jogos",
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
"Dialog.PsExecutables": "Executáveis PS",
"Dialog.SaveLogFile": "Selecione onde guardar o ficheiro de registo",
"Dialog.PlainTextFiles": "Ficheiros de Texto Simples",
"Dialog.LogFiles": "Ficheiros de Registo",
"Options.About" : "Sobre",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Código-fonte, problemas e desenvolvimento do projeto.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!",
"About.DiscordButton": "Junte-se ao nosso Discord!"
}
+15 -14
View File
@@ -12,7 +12,8 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Avalonia.VisualTree;
using SharpEmu.Libs.Pad;
using SharpEmu.HLE.Host;
using SharpEmu.HLE.Host.Windows;
using SharpEmu.Logging;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
@@ -62,7 +63,7 @@ public partial class MainWindow : Window
// Controller navigation state.
private readonly DispatcherTimer _gamepadTimer;
private uint _previousPadButtons;
private HostGamepadButtons _previousPadButtons;
private long _navLeftNextAt;
private long _navRightNextAt;
private long _navUpNextAt;
@@ -151,8 +152,8 @@ public partial class MainWindow : Window
Opened += async (_, _) => await OnOpenedAsync();
Closing += (_, _) => OnWindowClosing();
DualSenseReader.EnsureStarted();
XInputReader.EnsureStarted();
WindowsDualSenseReader.EnsureStarted();
WindowsXInputReader.EnsureStarted();
_gamepadTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(50),
@@ -224,9 +225,9 @@ public partial class MainWindow : Window
private void PollGamepad()
{
// DualSense wins when both are connected; XInput covers Xbox pads.
if (!DualSenseReader.TryGetState(out var pad) && !XInputReader.TryGetState(out pad))
if (!WindowsDualSenseReader.TryGetState(out var pad) && !WindowsXInputReader.TryGetState(out pad))
{
_previousPadButtons = 0;
_previousPadButtons = HostGamepadButtons.None;
return;
}
@@ -239,12 +240,12 @@ public partial class MainWindow : Window
}
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
if ((shoulderPressed & OrbisPadButton.L1) != 0)
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
{
SetActivePage(0);
}
if ((shoulderPressed & OrbisPadButton.R1) != 0)
if ((shoulderPressed & HostGamepadButtons.R1) != 0)
{
SetActivePage(1);
}
@@ -256,10 +257,10 @@ public partial class MainWindow : Window
}
var now = Environment.TickCount64;
var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64;
var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192;
var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64;
var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192;
var left = (pad.Buttons & HostGamepadButtons.Left) != 0 || pad.LeftX < 64;
var right = (pad.Buttons & HostGamepadButtons.Right) != 0 || pad.LeftX > 192;
var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
if (ShouldNavigate(left, ref _navLeftNextAt, now))
{
@@ -282,12 +283,12 @@ public partial class MainWindow : Window
}
var pressed = pad.Buttons & ~_previousPadButtons;
if ((pressed & 0x4000) != 0) // Cross
if ((pressed & HostGamepadButtons.Cross) != 0)
{
LaunchSelected();
}
if ((pressed & 0x2000) != 0) // Circle
if ((pressed & HostGamepadButtons.Circle) != 0)
{
StopEmulator();
}
+10 -7
View File
@@ -10,6 +10,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.0.1</Version>
<!-- Required by the source-generated LibraryImport stubs in the linked
controller readers below. -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<!-- Dependency-free; provides the BuildInfo provenance shown in the
@@ -41,14 +44,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
with the emulator's pad HLE. They are dependency-free, so they are
compiled in directly rather than pulling a reference to all of
SharpEmu.Libs into the launcher. -->
with the emulator's host input backend. They are dependency-free, so
they are compiled in directly rather than pulling a reference to all
of SharpEmu.HLE into the launcher. -->
<ItemGroup>
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
<Compile Include="..\SharpEmu.HLE\Host\HostGamepadState.cs" Link="Input/HostGamepadState.cs" />
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsHidNative.cs" Link="Input/WindowsHidNative.cs" />
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsDualSenseReader.cs" Link="Input/WindowsDualSenseReader.cs" />
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsXInputReader.cs" Link="Input/WindowsXInputReader.cs" />
</ItemGroup>
</Project>
+51 -10
View File
@@ -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<byte>.Shared.Rent(capacity) : null;
Span<byte> 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<byte>.Shared.Return(rented);
}
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
public bool PushUInt64(ulong value)
+25 -25
View File
@@ -23,6 +23,20 @@ public readonly record struct GuestThreadSnapshot(
ulong LastReturnRip,
string? BlockReason);
/// <summary>
/// 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.
/// </summary>
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<int>? _pendingBlockResumeHandler;
[ThreadStatic]
private static Func<bool>? _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<int>? resumeHandler = null,
Func<bool>? 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<int>? resumeHandler,
out Func<bool>? 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<int>? resumeHandler,
out Func<bool>? 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;
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host-neutral gamepad button flags. Named after the PlayStation layout the guest API
/// exposes, but the numeric values are the seam's own — the HLE pad exports translate
/// them to SCE_PAD_BUTTON bits, so guest ABI values never leak into host backends.
/// </summary>
[Flags]
public enum HostGamepadButtons : uint
{
None = 0,
Up = 1 << 0,
Down = 1 << 1,
Left = 1 << 2,
Right = 1 << 3,
Cross = 1 << 4,
Circle = 1 << 5,
Square = 1 << 6,
Triangle = 1 << 7,
L1 = 1 << 8,
R1 = 1 << 9,
L2 = 1 << 10,
R2 = 1 << 11,
L3 = 1 << 12,
R3 = 1 << 13,
Options = 1 << 14,
TouchPad = 1 << 15,
}
/// <summary>
/// Snapshot of one host gamepad: sticks are 0..255 with 128 centered and Y growing
/// downward; triggers 0..255. Unmanaged on purpose so per-frame polls can stackalloc
/// snapshot buffers.
/// </summary>
public readonly record struct HostGamepadState(
bool Connected,
HostGamepadButtons Buttons,
byte LeftX,
byte LeftY,
byte RightX,
byte RightY,
byte LeftTrigger,
byte RightTrigger);
+23
View File
@@ -0,0 +1,23 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host audio-output device access. The HLE audio exports convert guest submissions to
/// interleaved stereo 16-bit PCM (the format every backend accepts) and feed them through
/// streams opened here; everything device-specific — queueing, backpressure, native
/// buffer lifetime — lives behind <see cref="IHostAudioStream"/>.
/// </summary>
public interface IHostAudioOutput
{
/// <summary>Backend identifier for diagnostics (e.g. "winmm").</summary>
string BackendName { get; }
/// <summary>
/// Opens an interleaved stereo 16-bit PCM output stream at the given sample rate.
/// Throws when the host has no usable output device; callers degrade to a silent
/// port and pace the guest instead.
/// </summary>
IHostAudioStream OpenStereoPcm16Stream(uint sampleRate);
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// One open host audio output stream. Submissions are interleaved stereo 16-bit PCM at
/// the sample rate the stream was opened with.
/// </summary>
public interface IHostAudioStream : IDisposable
{
/// <summary>
/// Submits one buffer. May block briefly while the device drains its queue (this is
/// what paces the guest's audio loop); returns false when the stream cannot accept
/// audio, in which case the caller paces the guest itself.
/// </summary>
bool Submit(ReadOnlySpan<byte> stereoPcm16);
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host input devices: gamepad state snapshots, force-feedback/lightbar sinks, and the
/// keyboard-fallback queries. Which physical readers exist (DualSense over raw HID,
/// XInput, evdev, ...) is a backend detail; merge policy between devices and the
/// keyboard lives in the HLE pad exports.
/// </summary>
public interface IHostInput
{
/// <summary>Starts the background device readers once; safe to call repeatedly.</summary>
void EnsureStarted();
/// <summary>
/// Fills <paramref name="destination"/> with snapshots of currently connected
/// gamepads and returns how many were written (0 when none are connected).
/// </summary>
int GetGamepadStates(Span<HostGamepadState> destination);
/// <summary>Human-readable name of the first connected gamepad, or null.</summary>
string? DescribeConnectedGamepad();
/// <summary>Sets rumble on all connected gamepads; large = strong/left motor.</summary>
void SetRumble(byte largeMotor, byte smallMotor);
/// <summary>
/// Approximates per-trigger vibration on gamepads without independent trigger
/// actuators; null leaves that trigger's current value unchanged.
/// </summary>
void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger);
void SetLightbar(byte red, byte green, byte blue);
void ResetLightbar();
/// <summary>True when a window of this process has keyboard focus.</summary>
bool IsHostWindowFocused();
/// <summary>Windows virtual-key code semantics; other backends translate.</summary>
bool IsKeyDown(int virtualKey);
}
+4
View File
@@ -16,4 +16,8 @@ public interface IHostPlatform
IHostThreading Threading { get; }
IHostSymbolResolver Symbols { get; }
IHostAudioOutput Audio { get; }
IHostInput Input { get; }
}
+6
View File
@@ -24,6 +24,12 @@ public interface IHostThreading
bool TrySetCurrentThreadAffinity(nuint affinityMask);
/// <summary>
/// Asks the OS for ~1 ms timed-wait granularity for the life of the process
/// (idempotent; best-effort). No-op on platforms whose default is already fine.
/// </summary>
void RequestTimerResolution();
/// <summary>
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
@@ -3,21 +3,21 @@
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Pad;
namespace SharpEmu.HLE.Host.Windows;
/// <summary>
/// Reads a DualSense controller over raw HID on a background thread.
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
/// activated by requesting feature report 0x05), with hot-plug retry.
/// </summary>
internal static class DualSenseReader
internal static class WindowsDualSenseReader
{
private const ushort SonyVendorId = 0x054C;
private const ushort DualSenseProductId = 0x0CE6;
private const ushort DualSenseEdgeProductId = 0x0DF2;
private static readonly object Gate = new();
private static PadState _state;
private static HostGamepadState _state;
private static bool _started;
// Output (rumble/lightbar) state, all guarded by Gate.
@@ -37,6 +37,8 @@ internal static class DualSenseReader
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
internal static void EnsureStarted()
{
// The GUI source-links this reader and calls it directly, without the
// host-platform resolution that otherwise guarantees Windows.
if (!OperatingSystem.IsWindows())
{
return;
@@ -59,7 +61,7 @@ internal static class DualSenseReader
}
}
internal static bool TryGetState(out PadState state)
internal static bool TryGetState(out HostGamepadState state)
{
lock (Gate)
{
@@ -69,7 +71,7 @@ internal static class DualSenseReader
return state.Connected;
}
private static void SetState(in PadState state)
private static void SetState(in HostGamepadState state)
{
lock (Gate)
{
@@ -148,11 +150,11 @@ internal static class DualSenseReader
{
if (_outputStream is null)
{
var handle = HidNative.CreateFile(
var handle = WindowsHidNative.CreateFile(
_devicePath,
HidNative.GenericRead | HidNative.GenericWrite,
HidNative.FileShareRead | HidNative.FileShareWrite,
0, HidNative.OpenExisting, 0, 0);
WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
0, WindowsHidNative.OpenExisting, 0, 0);
if (handle.IsInvalid)
{
handle.Dispose();
@@ -262,7 +264,7 @@ internal static class DualSenseReader
// to the full 0x31 input report. Harmless over USB.
var feature = new byte[41];
feature[0] = 0x05;
_ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
_ = WindowsHidNative.HidD_GetFeature(handle, feature, feature.Length);
if (!announcedConnect)
{
@@ -320,18 +322,18 @@ internal static class DualSenseReader
private static SafeFileHandle? OpenDualSense(out string? devicePath)
{
devicePath = null;
foreach (var path in HidNative.EnumerateHidDevicePaths())
foreach (var path in WindowsHidNative.EnumerateHidDevicePaths())
{
// Open without access rights just to query VID/PID.
using var probe = HidNative.CreateFile(
path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0);
using var probe = WindowsHidNative.CreateFile(
path, 0, WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite, 0, WindowsHidNative.OpenExisting, 0, 0);
if (probe.IsInvalid)
{
continue;
}
var attributes = new HidNative.HiddAttributes { Size = 12 };
if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
var attributes = new WindowsHidNative.HiddAttributes { Size = 12 };
if (!WindowsHidNative.HidD_GetAttributes(probe, ref attributes) ||
attributes.VendorId != SonyVendorId ||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
{
@@ -339,19 +341,19 @@ internal static class DualSenseReader
}
// Read+write so feature reports work; fall back to read-only.
var handle = HidNative.CreateFile(
var handle = WindowsHidNative.CreateFile(
path,
HidNative.GenericRead | HidNative.GenericWrite,
HidNative.FileShareRead | HidNative.FileShareWrite,
0, HidNative.OpenExisting, 0, 0);
WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
0, WindowsHidNative.OpenExisting, 0, 0);
if (handle.IsInvalid)
{
handle.Dispose();
handle = HidNative.CreateFile(
handle = WindowsHidNative.CreateFile(
path,
HidNative.GenericRead,
HidNative.FileShareRead | HidNative.FileShareWrite,
0, HidNative.OpenExisting, 0, 0);
WindowsHidNative.GenericRead,
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
0, WindowsHidNative.OpenExisting, 0, 0);
}
if (!handle.IsInvalid)
@@ -366,7 +368,7 @@ internal static class DualSenseReader
return null;
}
private static bool TryParseReport(ReadOnlySpan<byte> report, out PadState state)
private static bool TryParseReport(ReadOnlySpan<byte> report, out HostGamepadState state)
{
// USB: report id 0x01, payload starts at [1].
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
@@ -395,43 +397,43 @@ internal static class DualSenseReader
var buttons1 = report[offset + 8];
var buttons2 = report[offset + 9];
uint buttons = 0;
buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0;
buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0;
buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0;
buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0;
var buttons = HostGamepadButtons.None;
buttons |= (buttons0 & 0x10) != 0 ? HostGamepadButtons.Square : 0;
buttons |= (buttons0 & 0x20) != 0 ? HostGamepadButtons.Cross : 0;
buttons |= (buttons0 & 0x40) != 0 ? HostGamepadButtons.Circle : 0;
buttons |= (buttons0 & 0x80) != 0 ? HostGamepadButtons.Triangle : 0;
buttons |= HatToButtons(buttons0 & 0x0F);
buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0;
buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0;
buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0;
buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0;
buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0;
buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0;
buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0;
buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0;
buttons |= (buttons1 & 0x01) != 0 ? HostGamepadButtons.L1 : 0;
buttons |= (buttons1 & 0x02) != 0 ? HostGamepadButtons.R1 : 0;
buttons |= (buttons1 & 0x04) != 0 ? HostGamepadButtons.L2 : 0;
buttons |= (buttons1 & 0x08) != 0 ? HostGamepadButtons.R2 : 0;
buttons |= (buttons1 & 0x20) != 0 ? HostGamepadButtons.Options : 0;
buttons |= (buttons1 & 0x40) != 0 ? HostGamepadButtons.L3 : 0;
buttons |= (buttons1 & 0x80) != 0 ? HostGamepadButtons.R3 : 0;
buttons |= (buttons2 & 0x02) != 0 ? HostGamepadButtons.TouchPad : 0;
state = new PadState(
state = new HostGamepadState(
Connected: true,
Buttons: buttons,
LeftX: leftX,
LeftY: leftY,
RightX: rightX,
RightY: rightY,
L2: l2,
R2: r2);
LeftTrigger: l2,
RightTrigger: r2);
return true;
}
private static uint HatToButtons(int hat) => hat switch
private static HostGamepadButtons HatToButtons(int hat) => hat switch
{
0 => OrbisPadButton.Up,
1 => OrbisPadButton.Up | OrbisPadButton.Right,
2 => OrbisPadButton.Right,
3 => OrbisPadButton.Right | OrbisPadButton.Down,
4 => OrbisPadButton.Down,
5 => OrbisPadButton.Down | OrbisPadButton.Left,
6 => OrbisPadButton.Left,
7 => OrbisPadButton.Left | OrbisPadButton.Up,
0 => HostGamepadButtons.Up,
1 => HostGamepadButtons.Up | HostGamepadButtons.Right,
2 => HostGamepadButtons.Right,
3 => HostGamepadButtons.Right | HostGamepadButtons.Down,
4 => HostGamepadButtons.Down,
5 => HostGamepadButtons.Down | HostGamepadButtons.Left,
6 => HostGamepadButtons.Left,
7 => HostGamepadButtons.Left | HostGamepadButtons.Up,
_ => 0,
};
}
@@ -4,13 +4,13 @@
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Pad;
namespace SharpEmu.HLE.Host.Windows;
/// <summary>
/// Minimal Win32 HID interop used to talk to a DualSense controller
/// directly, without any external input library.
/// </summary>
internal static partial class HidNative
internal static partial class WindowsHidNative
{
internal const int DigcfPresent = 0x02;
internal const int DigcfDeviceInterface = 0x10;
@@ -38,28 +38,32 @@ internal static partial class HidNative
public ushort VersionNumber;
}
[DllImport("hid.dll")]
internal static extern void HidD_GetHidGuid(out Guid hidGuid);
[LibraryImport("hid.dll")]
internal static partial void HidD_GetHidGuid(out Guid hidGuid);
[DllImport("hid.dll")]
internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
[LibraryImport("hid.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
[DllImport("hid.dll")]
internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
[LibraryImport("hid.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool HidD_GetFeature(SafeFileHandle hidDeviceObject, [In, Out] byte[] reportBuffer, int reportBufferLength);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
[LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetClassDevsW")]
internal static partial nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
[DllImport("setupapi.dll")]
internal static extern bool SetupDiEnumDeviceInterfaces(
[LibraryImport("setupapi.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetupDiEnumDeviceInterfaces(
nint deviceInfoSet,
nint deviceInfoData,
ref Guid interfaceClassGuid,
int memberIndex,
ref SpDeviceInterfaceData deviceInterfaceData);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
internal static extern bool SetupDiGetDeviceInterfaceDetail(
[LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetDeviceInterfaceDetailW")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetupDiGetDeviceInterfaceDetail(
nint deviceInfoSet,
ref SpDeviceInterfaceData deviceInterfaceData,
nint deviceInterfaceDetailData,
@@ -67,11 +71,12 @@ internal static partial class HidNative
out int requiredSize,
nint deviceInfoData);
[DllImport("setupapi.dll")]
internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
[LibraryImport("setupapi.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern SafeFileHandle CreateFile(
[LibraryImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
internal static partial SafeFileHandle CreateFile(
string fileName,
uint desiredAccess,
uint shareMode,
@@ -0,0 +1,84 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
/// <summary>
/// Windows input backend: DualSense over raw HID plus XInput controllers for gamepads,
/// user32 for the keyboard-fallback queries. Rumble fans out to every reader; lightbar
/// only exists on the DualSense.
/// </summary>
internal sealed partial class WindowsHostInput : IHostInput
{
public void EnsureStarted()
{
WindowsDualSenseReader.EnsureStarted();
WindowsXInputReader.EnsureStarted();
}
public int GetGamepadStates(Span<HostGamepadState> destination)
{
var count = 0;
if (count < destination.Length && WindowsDualSenseReader.TryGetState(out var dualSense))
{
destination[count++] = dualSense;
}
if (count < destination.Length && WindowsXInputReader.TryGetState(out var xinput))
{
destination[count++] = xinput;
}
return count;
}
public string? DescribeConnectedGamepad()
{
if (WindowsDualSenseReader.TryGetState(out _))
{
return "DualSense";
}
return WindowsXInputReader.TryGetState(out _) ? "Xbox controller" : null;
}
public void SetRumble(byte largeMotor, byte smallMotor)
{
WindowsDualSenseReader.SetRumble(largeMotor, smallMotor);
WindowsXInputReader.SetRumble(largeMotor, smallMotor);
}
public void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger) =>
WindowsXInputReader.SetTriggerRumble(leftTrigger, rightTrigger);
public void SetLightbar(byte red, byte green, byte blue) =>
WindowsDualSenseReader.SetLightbar(red, green, blue);
public void ResetLightbar() => WindowsDualSenseReader.ResetLightbar();
public bool IsHostWindowFocused()
{
var foregroundWindow = GetForegroundWindow();
if (foregroundWindow == 0)
{
return false;
}
GetWindowThreadProcessId(foregroundWindow, out var processId);
return processId == (uint)Environment.ProcessId;
}
public bool IsKeyDown(int virtualKey) =>
(GetAsyncKeyState(virtualKey) & 0x8000) != 0;
[LibraryImport("user32.dll")]
private static partial short GetAsyncKeyState(int vKey);
[LibraryImport("user32.dll")]
private static partial nint GetForegroundWindow();
[LibraryImport("user32.dll")]
private static partial uint GetWindowThreadProcessId(nint hWnd, out uint processId);
}
@@ -10,4 +10,8 @@ internal sealed class WindowsHostPlatform : IHostPlatform
public IHostThreading Threading { get; } = new WindowsHostThreading();
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
public IHostAudioOutput Audio { get; } = new WindowsWaveOutAudio();
public IHostInput Input { get; } = new WindowsHostInput();
}
@@ -23,6 +23,31 @@ internal sealed unsafe partial class WindowsHostThreading : IHostThreading
private const int CtxRbp = 160;
private const int CtxRip = 248;
private static int _timerResolutionRequested;
public void RequestTimerResolution()
{
if (Interlocked.Exchange(ref _timerResolutionRequested, 1) != 0)
{
return;
}
try
{
if (TimeBeginPeriod(1) != 0)
{
Console.Error.WriteLine(
"[LOADER][WARN] Host timer resolution request rejected; " +
"timed waits keep the default ~15.6 ms granularity.");
}
}
catch (DllNotFoundException exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
}
}
public uint AllocateTlsSlot() => TlsAlloc();
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
@@ -162,4 +187,7 @@ internal sealed unsafe partial class WindowsHostThreading : IHostThreading
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CloseHandle(nint hObject);
[LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
private static partial uint TimeBeginPeriod(uint uPeriod);
}
@@ -0,0 +1,243 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
internal sealed partial class WindowsWaveOutAudio : IHostAudioOutput
{
public string BackendName => "winmm";
public IHostAudioStream OpenStereoPcm16Stream(uint sampleRate) => new WaveOutStream(sampleRate);
private sealed partial class WaveOutStream : IHostAudioStream
{
private const uint WaveMapper = uint.MaxValue;
private const uint CallbackEvent = 0x0005_0000;
private const ushort WaveFormatPcm = 1;
private const uint WaveHeaderDone = 0x0000_0001;
private const int MaximumQueuedPcmBytes = 32 * 1024;
private readonly object _gate = new();
private readonly AutoResetEvent _completion = new(false);
private readonly Queue<NativeBuffer> _buffers = new();
private IntPtr _device;
private int _queuedPcmBytes;
private bool _disposed;
public WaveOutStream(uint sampleRate)
{
var format = new WaveFormat
{
FormatTag = WaveFormatPcm,
Channels = 2,
SamplesPerSecond = sampleRate,
AverageBytesPerSecond = checked(sampleRate * 4),
BlockAlign = 4,
BitsPerSample = 16,
ExtraSize = 0,
};
var result = WaveOutOpen(
out _device,
WaveMapper,
ref format,
_completion.SafeWaitHandle.DangerousGetHandle(),
IntPtr.Zero,
CallbackEvent);
if (result != 0)
{
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
}
}
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
{
lock (_gate)
{
if (_disposed)
{
return false;
}
ReapCompletedBuffers();
while (_queuedPcmBytes != 0 &&
_queuedPcmBytes + stereoPcm16.Length > MaximumQueuedPcmBytes)
{
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
{
return false;
}
ReapCompletedBuffers();
}
return QueueBuffer(stereoPcm16);
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed)
{
return;
}
_disposed = true;
if (_device != IntPtr.Zero)
{
WaveOutReset(_device);
while (_buffers.TryDequeue(out var buffer))
{
ReleaseBuffer(buffer);
}
WaveOutClose(_device);
_device = IntPtr.Zero;
}
_completion.Dispose();
}
}
private bool QueueBuffer(ReadOnlySpan<byte> data)
{
var dataAddress = Marshal.AllocHGlobal(data.Length);
var headerAddress = IntPtr.Zero;
try
{
unsafe
{
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
}
var header = new WaveHeader
{
Data = dataAddress,
BufferLength = checked((uint)data.Length),
};
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
Marshal.StructureToPtr(header, headerAddress, false);
var result = WaveOutPrepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
return false;
}
result = WaveOutWrite(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
WaveOutUnprepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
return false;
}
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
_queuedPcmBytes += data.Length;
dataAddress = IntPtr.Zero;
headerAddress = IntPtr.Zero;
return true;
}
finally
{
if (headerAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(headerAddress);
}
if (dataAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(dataAddress);
}
}
}
private void ReapCompletedBuffers()
{
while (_buffers.TryPeek(out var buffer))
{
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
if ((header.Flags & WaveHeaderDone) == 0)
{
return;
}
_buffers.Dequeue();
ReleaseBuffer(buffer);
}
}
private void ReleaseBuffer(NativeBuffer buffer)
{
WaveOutUnprepareHeader(
_device,
buffer.Header,
checked((uint)Marshal.SizeOf<WaveHeader>()));
_queuedPcmBytes -= buffer.Length;
Marshal.FreeHGlobal(buffer.Header);
Marshal.FreeHGlobal(buffer.Data);
}
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct WaveFormat
{
public ushort FormatTag;
public ushort Channels;
public uint SamplesPerSecond;
public uint AverageBytesPerSecond;
public ushort BlockAlign;
public ushort BitsPerSample;
public ushort ExtraSize;
}
[StructLayout(LayoutKind.Sequential)]
private struct WaveHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public nuint User;
public uint Flags;
public uint Loops;
public IntPtr Next;
public nuint Reserved;
}
[LibraryImport("winmm.dll", EntryPoint = "waveOutOpen")]
private static partial uint WaveOutOpen(
out IntPtr device,
uint deviceId,
ref WaveFormat format,
IntPtr callback,
IntPtr instance,
uint flags);
[LibraryImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
private static partial uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
[LibraryImport("winmm.dll", EntryPoint = "waveOutWrite")]
private static partial uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
[LibraryImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
private static partial uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
[LibraryImport("winmm.dll", EntryPoint = "waveOutReset")]
private static partial uint WaveOutReset(IntPtr device);
[LibraryImport("winmm.dll", EntryPoint = "waveOutClose")]
private static partial uint WaveOutClose(IntPtr device);
}
}
@@ -3,15 +3,15 @@
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Pad;
namespace SharpEmu.HLE.Host.Windows;
/// <summary>
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
/// the Windows XInput API on a background thread, translated to the same
/// ORBIS pad conventions as <see cref="DualSenseReader"/>. Supports rumble
/// and hot-plug retry; the first connected slot (of four) is used.
/// the Windows XInput API on a background thread, translated to
/// <see cref="HostGamepadState"/> conventions. Supports rumble and hot-plug
/// retry; the first connected slot (of four) is used.
/// </summary>
internal static class XInputReader
internal static partial class WindowsXInputReader
{
private const uint ErrorSuccess = 0;
private const int SlotCount = 4;
@@ -34,7 +34,7 @@ internal static class XInputReader
private const ushort XinputY = 0x8000;
private static readonly object Gate = new();
private static PadState _state;
private static HostGamepadState _state;
private static bool _started;
private static int _slot = -1; // connected XInput user index, -1 when none
private static byte _motorLeft;
@@ -45,6 +45,8 @@ internal static class XInputReader
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
internal static void EnsureStarted()
{
// The GUI source-links this reader and calls it directly, without the
// host-platform resolution that otherwise guarantees Windows.
if (!OperatingSystem.IsWindows())
{
return;
@@ -67,7 +69,7 @@ internal static class XInputReader
}
}
internal static bool TryGetState(out PadState state)
internal static bool TryGetState(out HostGamepadState state)
{
lock (Gate)
{
@@ -77,7 +79,7 @@ internal static class XInputReader
return state.Connected;
}
private static void SetState(in PadState state)
private static void SetState(in HostGamepadState state)
{
lock (Gate)
{
@@ -204,40 +206,40 @@ internal static class XInputReader
return -1;
}
private static PadState Translate(in XInputGamepad pad)
private static HostGamepadState Translate(in XInputGamepad pad)
{
uint buttons = 0;
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0;
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0;
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0;
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0;
buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0;
buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0;
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0;
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0;
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0;
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0;
buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0;
buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0;
buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0;
buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0;
buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0;
buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0;
var buttons = HostGamepadButtons.None;
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? HostGamepadButtons.Up : 0;
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? HostGamepadButtons.Down : 0;
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? HostGamepadButtons.Left : 0;
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? HostGamepadButtons.Right : 0;
buttons |= (pad.Buttons & XinputStart) != 0 ? HostGamepadButtons.Options : 0;
buttons |= (pad.Buttons & XinputBack) != 0 ? HostGamepadButtons.TouchPad : 0;
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? HostGamepadButtons.L3 : 0;
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? HostGamepadButtons.R3 : 0;
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? HostGamepadButtons.L1 : 0;
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? HostGamepadButtons.R1 : 0;
buttons |= (pad.Buttons & XinputA) != 0 ? HostGamepadButtons.Cross : 0;
buttons |= (pad.Buttons & XinputB) != 0 ? HostGamepadButtons.Circle : 0;
buttons |= (pad.Buttons & XinputX) != 0 ? HostGamepadButtons.Square : 0;
buttons |= (pad.Buttons & XinputY) != 0 ? HostGamepadButtons.Triangle : 0;
buttons |= pad.LeftTrigger > TriggerThreshold ? HostGamepadButtons.L2 : 0;
buttons |= pad.RightTrigger > TriggerThreshold ? HostGamepadButtons.R2 : 0;
return new PadState(
return new HostGamepadState(
Connected: true,
Buttons: buttons,
LeftX: AxisToByte(pad.ThumbLX),
LeftY: AxisToByteInverted(pad.ThumbLY),
RightX: AxisToByte(pad.ThumbRX),
RightY: AxisToByteInverted(pad.ThumbRY),
L2: pad.LeftTrigger,
R2: pad.RightTrigger);
LeftTrigger: pad.LeftTrigger,
RightTrigger: pad.RightTrigger);
}
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
// XInput Y grows upward, ORBIS pads report Y growing downward.
// XInput Y grows upward, host pad conventions report Y growing downward.
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
[StructLayout(LayoutKind.Sequential)]
@@ -267,9 +269,9 @@ internal static class XInputReader
}
// xinput1_4.dll ships with Windows 8 and later.
[DllImport("xinput1_4.dll")]
private static extern uint XInputGetState(uint userIndex, out XInputState state);
[LibraryImport("xinput1_4.dll")]
private static partial uint XInputGetState(uint userIndex, out XInputState state);
[DllImport("xinput1_4.dll")]
private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration);
[LibraryImport("xinput1_4.dll")]
private static partial uint XInputSetState(uint userIndex, ref XInputVibration vibration);
}
@@ -6,4 +6,6 @@ namespace SharpEmu.HLE;
public interface IGuestMemoryAllocator
{
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
bool TryFreeGuestMemory(ulong address);
}
+30 -12
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using System.Buffers;
using System.Collections.Concurrent;
using System.Diagnostics;
@@ -27,7 +28,7 @@ public static class AudioOutExports
int channels,
int bytesPerSample,
bool isFloat,
WinMmAudioPort? backend)
IHostAudioStream? backend)
{
UserId = userId;
Type = type;
@@ -48,7 +49,7 @@ public static class AudioOutExports
public int Channels { get; }
public int BytesPerSample { get; }
public bool IsFloat { get; }
public WinMmAudioPort? Backend { get; }
public IHostAudioStream? Backend { get; }
public int BufferByteLength =>
checked((int)BufferLength * Channels * BytesPerSample);
@@ -103,12 +104,13 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
WinMmAudioPort? backend = null;
IHostAudioStream? backend = null;
string backendName;
try
{
backend = new WinMmAudioPort(frequency);
backendName = "winmm";
var audio = HostPlatform.Current.Audio;
backend = audio.OpenStereoPcm16Stream(frequency);
backendName = audio.BackendName;
}
catch (Exception exception)
{
@@ -180,15 +182,31 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (port.Backend is null ||
!port.Backend.Submit(
source,
port.BufferLength,
port.Channels,
port.BytesPerSample,
port.IsFloat))
if (port.Backend is null)
{
port.PaceSilence();
return ctx.SetReturn(0);
}
var outputLength = checked((int)port.BufferLength * AudioPcmConversion.OutputFrameSize);
var output = ArrayPool<byte>.Shared.Rent(outputLength);
try
{
AudioPcmConversion.ConvertToStereoPcm16(
source,
output.AsSpan(0, outputLength),
checked((int)port.BufferLength),
port.Channels,
port.BytesPerSample,
port.IsFloat);
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
{
port.PaceSilence();
}
}
finally
{
ArrayPool<byte>.Shared.Return(output);
}
return ctx.SetReturn(0);
@@ -0,0 +1,55 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
namespace SharpEmu.Libs.Audio;
/// <summary>
/// Converts guest AudioOut submissions (mono/stereo/7.1, s16 or float32) into the
/// interleaved stereo 16-bit PCM that host audio streams accept. Platform-neutral —
/// device specifics live behind IHostAudioStream.
/// </summary>
internal static class AudioPcmConversion
{
/// <summary>Bytes per output frame: two 16-bit channels.</summary>
public const int OutputFrameSize = 4;
public static void ConvertToStereoPcm16(
ReadOnlySpan<byte> source,
Span<byte> destination,
int frames,
int channels,
int bytesPerSample,
bool isFloat)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
{
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
}
}
private static short ReadSample(
ReadOnlySpan<byte> frame,
int channel,
int bytesPerSample,
bool isFloat)
{
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
if (!isFloat)
{
return BinaryPrimitives.ReadInt16LittleEndian(sample);
}
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
}
}
-302
View File
@@ -1,302 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Audio;
internal sealed class WinMmAudioPort : IDisposable
{
private const uint WaveMapper = uint.MaxValue;
private const uint CallbackEvent = 0x0005_0000;
private const ushort WaveFormatPcm = 1;
private const uint WaveHeaderDone = 0x0000_0001;
private const int MaximumQueuedPcmBytes = 32 * 1024;
private readonly object _gate = new();
private readonly AutoResetEvent _completion = new(false);
private readonly Queue<NativeBuffer> _buffers = new();
private IntPtr _device;
private int _queuedPcmBytes;
private bool _disposed;
public WinMmAudioPort(uint sampleRate)
{
if (!OperatingSystem.IsWindows())
{
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
}
var format = new WaveFormat
{
FormatTag = WaveFormatPcm,
Channels = 2,
SamplesPerSecond = sampleRate,
AverageBytesPerSecond = checked(sampleRate * 4),
BlockAlign = 4,
BitsPerSample = 16,
ExtraSize = 0,
};
var result = WaveOutOpen(
out _device,
WaveMapper,
ref format,
_completion.SafeWaitHandle.DangerousGetHandle(),
IntPtr.Zero,
CallbackEvent);
if (result != 0)
{
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
}
}
public bool Submit(
ReadOnlySpan<byte> source,
uint frames,
int channels,
int bytesPerSample,
bool isFloat)
{
lock (_gate)
{
if (_disposed)
{
return false;
}
var outputLength = checked((int)frames * 4);
ReapCompletedBuffers();
while (_queuedPcmBytes != 0 &&
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
{
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
{
return false;
}
ReapCompletedBuffers();
}
var output = ArrayPool<byte>.Shared.Rent(outputLength);
try
{
ConvertToStereoPcm16(
source,
output.AsSpan(0, outputLength),
checked((int)frames),
channels,
bytesPerSample,
isFloat);
return QueueBuffer(output.AsSpan(0, outputLength));
}
finally
{
ArrayPool<byte>.Shared.Return(output);
}
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed)
{
return;
}
_disposed = true;
if (_device != IntPtr.Zero)
{
WaveOutReset(_device);
while (_buffers.TryDequeue(out var buffer))
{
ReleaseBuffer(buffer);
}
WaveOutClose(_device);
_device = IntPtr.Zero;
}
_completion.Dispose();
}
}
private bool QueueBuffer(ReadOnlySpan<byte> data)
{
var dataAddress = Marshal.AllocHGlobal(data.Length);
var headerAddress = IntPtr.Zero;
try
{
unsafe
{
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
}
var header = new WaveHeader
{
Data = dataAddress,
BufferLength = checked((uint)data.Length),
};
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
Marshal.StructureToPtr(header, headerAddress, false);
var result = WaveOutPrepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
return false;
}
result = WaveOutWrite(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
WaveOutUnprepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
return false;
}
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
_queuedPcmBytes += data.Length;
dataAddress = IntPtr.Zero;
headerAddress = IntPtr.Zero;
return true;
}
finally
{
if (headerAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(headerAddress);
}
if (dataAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(dataAddress);
}
}
}
private void ReapCompletedBuffers()
{
while (_buffers.TryPeek(out var buffer))
{
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
if ((header.Flags & WaveHeaderDone) == 0)
{
return;
}
_buffers.Dequeue();
ReleaseBuffer(buffer);
}
}
private void ReleaseBuffer(NativeBuffer buffer)
{
WaveOutUnprepareHeader(
_device,
buffer.Header,
checked((uint)Marshal.SizeOf<WaveHeader>()));
_queuedPcmBytes -= buffer.Length;
Marshal.FreeHGlobal(buffer.Header);
Marshal.FreeHGlobal(buffer.Data);
}
private static void ConvertToStereoPcm16(
ReadOnlySpan<byte> source,
Span<byte> destination,
int frames,
int channels,
int bytesPerSample,
bool isFloat)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
{
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
}
}
private static short ReadSample(
ReadOnlySpan<byte> frame,
int channel,
int bytesPerSample,
bool isFloat)
{
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
if (!isFloat)
{
return BinaryPrimitives.ReadInt16LittleEndian(sample);
}
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
}
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct WaveFormat
{
public ushort FormatTag;
public ushort Channels;
public uint SamplesPerSecond;
public uint AverageBytesPerSecond;
public ushort BlockAlign;
public ushort BitsPerSample;
public ushort ExtraSize;
}
[StructLayout(LayoutKind.Sequential)]
private struct WaveHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public nuint User;
public uint Flags;
public uint Loops;
public IntPtr Next;
public nuint Reserved;
}
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
private static extern uint WaveOutOpen(
out IntPtr device,
uint deviceId,
ref WaveFormat format,
IntPtr callback,
IntPtr instance,
uint flags);
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
private static extern uint WaveOutReset(IntPtr device);
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
private static extern uint WaveOutClose(IntPtr device);
}
-46
View File
@@ -1,46 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace SharpEmu.Libs;
public static class HostTimerResolution
{
private const uint TargetPeriodMilliseconds = 1;
private static int _requested;
public static void Request()
{
if (Interlocked.Exchange(ref _requested, 1) != 0)
{
return;
}
if (!OperatingSystem.IsWindows())
{
return;
}
try
{
if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
{
Console.Error.WriteLine(
"[LOADER][WARN] Host timer resolution request rejected; " +
"timed waits keep the default ~15.6 ms granularity.");
}
}
catch (DllNotFoundException exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
}
}
[SupportedOSPlatform("windows")]
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
private static extern uint TimeBeginPeriod(uint uPeriod);
}
@@ -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)}");
@@ -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<ulong> _eventQueues = new();
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
private static readonly Dictionary<ulong, KernelEventDeque> _pendingEvents = new();
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _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<KernelQueuedEvent>, 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<KernelQueuedEvent>();
_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<KernelQueuedEvent>();
queue = new KernelEventDeque();
_pendingEvents[handle] = queue;
}
@@ -480,7 +560,7 @@ public static class KernelEventQueueCompatExports
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new LinkedList<KernelQueuedEvent>();
queue = new KernelEventDeque();
_pendingEvents[handle] = queue;
}
@@ -527,7 +607,7 @@ public static class KernelEventQueueCompatExports
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new LinkedList<KernelQueuedEvent>();
queue = new KernelEventDeque();
_pendingEvents[handle] = queue;
}
@@ -563,15 +643,15 @@ public static class KernelEventQueueCompatExports
if (!_pendingEvents.TryGetValue(handle, out var events))
{
events = new LinkedList<KernelQueuedEvent>();
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<KernelQueuedEvent> 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<KernelQueuedEvent>? FindPendingEvent(
LinkedList<KernelQueuedEvent> 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<ulong, string> _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<KernelQueuedEvent>.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<KernelQueuedEvent>.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;
}
@@ -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<byte>.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<byte>.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<byte>(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<byte>.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<byte>.Shared.Return(chunk);
}
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -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
@@ -629,6 +653,7 @@ public static class KernelPthreadCompatExports
}
if (!InitializeMutexObject(ctx, handle, state))
{
TryFreeOpaqueObject(ctx, handle);
state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -641,6 +666,7 @@ public static class KernelPthreadCompatExports
_mutexStates.TryRemove(mutexAddress, out _);
_mutexStates.TryRemove(handle, out _);
TryFreeOpaqueObject(ctx, handle);
state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -668,6 +694,7 @@ public static class KernelPthreadCompatExports
}
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
TryFreeOpaqueObject(ctx, resolvedAddress);
state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -724,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;
@@ -736,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;
@@ -895,6 +927,7 @@ public static class KernelPthreadCompatExports
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
if (!WriteMutexAttrObject(ctx, handle, initialState))
{
TryFreeOpaqueObject(ctx, handle);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -912,6 +945,7 @@ public static class KernelPthreadCompatExports
_mutexAttrStates.Remove(handle);
}
TryFreeOpaqueObject(ctx, handle);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -935,6 +969,7 @@ public static class KernelPthreadCompatExports
}
}
TryFreeOpaqueObject(ctx, resolvedAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1195,6 +1230,7 @@ public static class KernelPthreadCompatExports
{
if (_condStates.TryGetValue(condAddress, out var raced))
{
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = condAddress;
state = raced;
return true;
@@ -1212,6 +1248,7 @@ public static class KernelPthreadCompatExports
_condStates.Remove(handle);
}
TryFreeOpaqueObject(ctx, handle);
return false;
}
@@ -1231,7 +1268,22 @@ public static class KernelPthreadCompatExports
Span<byte> initialData = stackalloc byte[size];
initialData.Clear();
return ctx.Memory.TryWrite(address, initialData);
if (ctx.Memory.TryWrite(address, initialData))
{
return true;
}
allocator.TryFreeGuestMemory(address);
address = 0;
return false;
}
private static void TryFreeOpaqueObject(CpuContext ctx, ulong address)
{
if (ctx.Memory is IGuestMemoryAllocator allocator)
{
allocator.TryFreeGuestMemory(address);
}
}
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
@@ -1269,6 +1321,7 @@ public static class KernelPthreadCompatExports
_condStates.Remove(handle);
}
TryFreeOpaqueObject(ctx, handle);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1293,6 +1346,7 @@ public static class KernelPthreadCompatExports
}
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
TryFreeOpaqueObject(ctx, resolvedAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1363,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);
@@ -1775,6 +1838,7 @@ public static class KernelPthreadCompatExports
}
if (!InitializeMutexObject(ctx, handle, createdState))
{
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = 0;
state = null;
return false;
@@ -1784,12 +1848,14 @@ public static class KernelPthreadCompatExports
{
if (_mutexStates.TryGetValue(mutexAddress, out state))
{
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = mutexAddress;
return true;
}
if (_mutexStates.TryGetValue(handle, out state))
{
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = handle;
return true;
}
@@ -1803,6 +1869,7 @@ public static class KernelPthreadCompatExports
_mutexStates.TryRemove(mutexAddress, out _);
_mutexStates.TryRemove(handle, out _);
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = 0;
state = null;
return false;
@@ -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;
}
@@ -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}");
}
}
+61 -73
View File
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Pad;
@@ -38,8 +38,7 @@ public static class PadExports
public static int PadInit(CpuContext ctx)
{
_initialized = true;
DualSenseReader.EnsureStarted();
XInputReader.EnsureStarted();
HostPlatform.Current.Input.EnsureStarted();
return ctx.SetReturn(0);
}
@@ -81,15 +80,13 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
}
DualSenseReader.EnsureStarted();
XInputReader.EnsureStarted();
var input = HostPlatform.Current.Input;
input.EnsureStarted();
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
{
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
: XInputReader.TryGetState(out _)
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
Console.Error.WriteLine(input.DescribeConnectedGamepad() is { } gamepadName
? $"[LOADER][INFO] Controls: {gamepadName} connected (keyboard fallback also active)."
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
}
return ctx.SetReturn(PrimaryPadHandle);
@@ -282,7 +279,7 @@ public static class PadExports
}
var triggerMask = parameter[0];
XInputReader.SetTriggerRumble(
HostPlatform.Current.Input.SetTriggerRumble(
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
@@ -326,8 +323,7 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
DualSenseReader.SetRumble(parameter[0], parameter[1]);
XInputReader.SetRumble(parameter[0], parameter[1]);
HostPlatform.Current.Input.SetRumble(parameter[0], parameter[1]);
return ctx.SetReturn(0);
}
@@ -357,7 +353,7 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
DualSenseReader.SetLightbar(color[0], color[1], color[2]);
HostPlatform.Current.Input.SetLightbar(color[0], color[1], color[2]);
return ctx.SetReturn(0);
}
@@ -374,7 +370,7 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
DualSenseReader.ResetLightbar();
HostPlatform.Current.Input.ResetLightbar();
return ctx.SetReturn(0);
}
@@ -420,37 +416,30 @@ public static class PadExports
return _cachedInputState;
}
var acceptsKeyboardInput = IsEmulatorWindowFocused();
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
var input = HostPlatform.Current.Input;
var acceptsKeyboardInput = input.IsHostWindowFocused();
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons(input) : 0;
var leftX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x41), input.IsKeyDown(0x44)) : (byte)128;
var leftY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x57), input.IsKeyDown(0x53)) : (byte)128;
var rightX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x4A), input.IsKeyDown(0x4C)) : (byte)128;
var rightY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x49), input.IsKeyDown(0x4B)) : (byte)128;
var l2 = acceptsKeyboardInput && input.IsKeyDown(0x52) ? (byte)255 : (byte)0;
var r2 = acceptsKeyboardInput && input.IsKeyDown(0x46) ? (byte)255 : (byte)0;
if (DualSenseReader.TryGetState(out var pad))
Span<HostGamepadState> gamepads = stackalloc HostGamepadState[2];
var gamepadCount = input.GetGamepadStates(gamepads);
for (var index = 0; index < gamepadCount; index++)
{
buttons |= pad.Buttons;
var pad = gamepads[index];
buttons |= ToOrbisButtons(pad.Buttons);
// The controller stick wins whenever it is deflected past a
// small deadzone; otherwise any keyboard value stays.
leftX = MergeAxis(pad.LeftX, leftX);
leftY = MergeAxis(pad.LeftY, leftY);
rightX = MergeAxis(pad.RightX, rightX);
rightY = MergeAxis(pad.RightY, rightY);
l2 = Math.Max(l2, pad.L2);
r2 = Math.Max(r2, pad.R2);
}
if (XInputReader.TryGetState(out var xpad))
{
buttons |= xpad.Buttons;
leftX = MergeAxis(xpad.LeftX, leftX);
leftY = MergeAxis(xpad.LeftY, leftY);
rightX = MergeAxis(xpad.RightX, rightX);
rightY = MergeAxis(xpad.RightY, rightY);
l2 = Math.Max(l2, xpad.L2);
r2 = Math.Max(r2, xpad.R2);
l2 = Math.Max(l2, pad.LeftTrigger);
r2 = Math.Max(r2, pad.RightTrigger);
}
_cachedInputState = new PadState(
@@ -466,50 +455,49 @@ public static class PadExports
return _cachedInputState;
}
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll")]
private static extern nint GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
private static bool IsKeyDown(int vk) =>
(GetAsyncKeyState(vk) & 0x8000) != 0;
private static bool IsEmulatorWindowFocused()
/// <summary>Maps the host seam's neutral button flags onto SCE_PAD_BUTTON bits.</summary>
private static uint ToOrbisButtons(HostGamepadButtons buttons)
{
var foregroundWindow = GetForegroundWindow();
if (foregroundWindow == 0)
{
return false;
}
GetWindowThreadProcessId(foregroundWindow, out var processId);
return processId == (uint)Environment.ProcessId;
uint result = 0;
if ((buttons & HostGamepadButtons.Up) != 0) result |= OrbisPadButton.Up;
if ((buttons & HostGamepadButtons.Down) != 0) result |= OrbisPadButton.Down;
if ((buttons & HostGamepadButtons.Left) != 0) result |= OrbisPadButton.Left;
if ((buttons & HostGamepadButtons.Right) != 0) result |= OrbisPadButton.Right;
if ((buttons & HostGamepadButtons.Cross) != 0) result |= OrbisPadButton.Cross;
if ((buttons & HostGamepadButtons.Circle) != 0) result |= OrbisPadButton.Circle;
if ((buttons & HostGamepadButtons.Square) != 0) result |= OrbisPadButton.Square;
if ((buttons & HostGamepadButtons.Triangle) != 0) result |= OrbisPadButton.Triangle;
if ((buttons & HostGamepadButtons.L1) != 0) result |= OrbisPadButton.L1;
if ((buttons & HostGamepadButtons.R1) != 0) result |= OrbisPadButton.R1;
if ((buttons & HostGamepadButtons.L2) != 0) result |= OrbisPadButton.L2;
if ((buttons & HostGamepadButtons.R2) != 0) result |= OrbisPadButton.R2;
if ((buttons & HostGamepadButtons.L3) != 0) result |= OrbisPadButton.L3;
if ((buttons & HostGamepadButtons.R3) != 0) result |= OrbisPadButton.R3;
if ((buttons & HostGamepadButtons.Options) != 0) result |= OrbisPadButton.Options;
if ((buttons & HostGamepadButtons.TouchPad) != 0) result |= OrbisPadButton.TouchPad;
return result;
}
private static uint ReadKeyboardButtons()
private static uint ReadKeyboardButtons(IHostInput input)
{
uint buttons = 0;
// D-pad
if (IsKeyDown(0x25)) buttons |= 0x0080; // Left
if (IsKeyDown(0x27)) buttons |= 0x0020; // Right
if (IsKeyDown(0x26)) buttons |= 0x0010; // Up
if (IsKeyDown(0x28)) buttons |= 0x0040; // Down
if (input.IsKeyDown(0x25)) buttons |= OrbisPadButton.Left;
if (input.IsKeyDown(0x27)) buttons |= OrbisPadButton.Right;
if (input.IsKeyDown(0x26)) buttons |= OrbisPadButton.Up;
if (input.IsKeyDown(0x28)) buttons |= OrbisPadButton.Down;
// Face buttons
if (IsKeyDown(0x5A) || IsKeyDown(0x0D)) buttons |= 0x4000; // Z / Enter = Cross
if (IsKeyDown(0x58) || IsKeyDown(0x1B)) buttons |= 0x2000; // X / Escape = Circle
if (IsKeyDown(0x43)) buttons |= 0x8000; // C = Square
if (IsKeyDown(0x56)) buttons |= 0x1000; // V = Triangle
if (input.IsKeyDown(0x5A) || input.IsKeyDown(0x0D)) buttons |= OrbisPadButton.Cross; // Z / Enter
if (input.IsKeyDown(0x58) || input.IsKeyDown(0x1B)) buttons |= OrbisPadButton.Circle; // X / Escape
if (input.IsKeyDown(0x43)) buttons |= OrbisPadButton.Square; // C
if (input.IsKeyDown(0x56)) buttons |= OrbisPadButton.Triangle; // V
// Shoulder buttons
if (IsKeyDown(0x51)) buttons |= 0x0400; // Q = L1
if (IsKeyDown(0x45)) buttons |= 0x0800; // E = R1
if (IsKeyDown(0x52)) buttons |= 0x0100; // R = L2 (digital)
if (IsKeyDown(0x46)) buttons |= 0x0200; // F = R2 (digital)
if (input.IsKeyDown(0x51)) buttons |= OrbisPadButton.L1; // Q
if (input.IsKeyDown(0x45)) buttons |= OrbisPadButton.R1; // E
if (input.IsKeyDown(0x52)) buttons |= OrbisPadButton.L2; // R (digital)
if (input.IsKeyDown(0x46)) buttons |= OrbisPadButton.R2; // F (digital)
// Options (Start)
if (IsKeyDown(0x09) || IsKeyDown(0x08)) buttons |= 0x0008; // Tab / Backspace = Options
if (input.IsKeyDown(0x09) || input.IsKeyDown(0x08)) buttons |= OrbisPadButton.Options; // Tab / Backspace
return buttons;
}
+109 -37
View File
@@ -2,9 +2,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
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;
@@ -67,7 +69,7 @@ public static class VideoOutExports
return;
}
HostTimerResolution.Request();
HostPlatform.Current.Threading.RequestTimerResolution();
_vblankPumpThread = new Thread(VblankPumpLoop)
{
@@ -180,6 +182,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<VideoOutPortState> _vblankPumpPorts = new();
private static void PumpVblanks()
{
lock (_vblankEdgeGate)
@@ -188,7 +194,7 @@ public static class VideoOutExports
Monitor.PulseAll(_vblankEdgeGate);
}
VideoOutPortState[] ports;
_vblankPumpPorts.Clear();
lock (_stateGate)
{
if (_ports.Count == 0)
@@ -198,10 +204,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 +233,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 +604,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 +644,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 +708,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 +1104,53 @@ public static class VideoOutExports
private static void SignalVblank(VideoOutPortState port)
{
List<FlipEventRegistration> 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<FlipEventRegistration>(port.VblankEvents);
vblankEventCount = port.VblankEvents.Count;
if (vblankEventCount != 0)
{
vblankEvents = ArrayPool<FlipEventRegistration>.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<FlipEventRegistration>.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 +1172,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<FlipEventRegistration> flipEvents;
FlipEventRegistration[]? flipEvents = null;
int flipEventCount;
lock (_stateGate)
{
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
@@ -1138,7 +1188,12 @@ public static class VideoOutExports
port.FlipCount++;
eventHint = SceVideoOutInternalEventFlip |
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
flipEventCount = port.FlipEvents.Count;
if (flipEventCount != 0)
{
flipEvents = ArrayPool<FlipEventRegistration>.Shared.Rent(flipEventCount);
port.FlipEvents.CopyTo(flipEvents);
}
}
var guestImageSubmitted = false;
@@ -1160,14 +1215,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<FlipEventRegistration>.Shared.Return(flipEvents);
}
}
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
@@ -1176,10 +1241,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 +1329,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 +1499,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 +1541,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 +1771,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}");
}
}
@@ -0,0 +1,254 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Memory;
using SharpEmu.Core.Loader;
using SharpEmu.HLE.Host;
using Xunit;
namespace SharpEmu.Libs.Tests.Memory;
public sealed class GuestMemoryAllocatorTests
{
[Fact]
public void FreedRangesAreReusedAndCoalesced()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
const ulong usableArenaSize = 0x0100_0000 - 0x1000;
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize - 0xC000, 0x1000, out var third));
Assert.False(memory.TryAllocateGuestMemory(1, 1, out _));
Assert.True(memory.TryFreeGuestMemory(second));
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var reused));
Assert.Equal(second, reused);
Assert.True(memory.TryFreeGuestMemory(first));
Assert.True(memory.TryFreeGuestMemory(reused));
Assert.True(memory.TryFreeGuestMemory(third));
Assert.False(memory.TryFreeGuestMemory(third));
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize, 0x1000, out var coalesced));
Assert.Equal(first, coalesced);
}
[Fact]
public void SegmentProtectionIsAppliedInContiguousRuns()
{
const ulong pageSize = 0x1000;
using var host = new RecordingHostMemory(3 * pageSize);
using var memory = new PhysicalVirtualMemory(host);
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Read);
Assert.Equal(
[
(host.Address, 3 * pageSize, HostPageProtection.ReadWrite),
(host.Address, 3 * pageSize, HostPageProtection.ReadOnly),
],
host.ProtectionCalls);
host.ProtectionCalls.Clear();
memory.Map(host.Address + pageSize, pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Write);
host.ProtectionCalls.Clear();
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Execute);
Assert.Equal(
[
(host.Address, 3 * pageSize, HostPageProtection.ReadWriteExecute),
(host.Address, pageSize, HostPageProtection.ReadExecute),
(host.Address + pageSize, pageSize, HostPageProtection.ReadWriteExecute),
(host.Address + (2 * pageSize), pageSize, HostPageProtection.ReadExecute),
],
host.ProtectionCalls);
}
[Fact]
public unsafe void GetPointerCommitsLazyPageBeforeReturningIt()
{
const ulong address = 0x00005000_0000_0000;
const ulong pageSize = 0x1000;
using var host = new LazyHostMemory(address);
using var memory = new PhysicalVirtualMemory(host);
memory.AllocateAt(address, (4UL << 30) + pageSize, executable: false, allowAlternative: false);
host.CommitCalls.Clear();
var pointer = memory.GetPointer(address + 0x123);
Assert.Equal(address + 0x123, (ulong)pointer);
Assert.Equal([(address, pageSize, HostPageProtection.ReadWrite)], host.CommitCalls);
}
[Fact]
public unsafe void GetPointerReturnsNullWhenLazyCommitFails()
{
const ulong address = 0x00005000_0000_0000;
using var host = new LazyHostMemory(address);
using var memory = new PhysicalVirtualMemory(host);
memory.AllocateAt(address, (4UL << 30) + 0x1000, executable: false, allowAlternative: false);
host.CommitCalls.Clear();
host.CommitSucceeds = false;
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
}
private sealed class FakeHostMemory : IHostMemory
{
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
desiredAddress != 0 ? desiredAddress : 0x00007000_0000_0000;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
Allocate(desiredAddress, size, protection);
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
public bool Free(ulong address) => true;
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
info = default;
return false;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
}
private sealed class RecordingHostMemory : IHostMemory, IDisposable
{
private readonly nint _allocation;
private bool _freed;
public RecordingHostMemory(ulong size)
{
_allocation = System.Runtime.InteropServices.Marshal.AllocHGlobal(checked((nint)(size + 0xFFF)));
Address = (unchecked((ulong)_allocation) + 0xFFF) & ~0xFFFUL;
}
public ulong Address { get; }
public List<(ulong Address, ulong Size, HostPageProtection Protection)> ProtectionCalls { get; } = [];
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
desiredAddress == Address ? Address : 0;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
public bool Free(ulong address)
{
if (address != Address || _freed)
{
return false;
}
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
_freed = true;
return true;
}
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
ProtectionCalls.Add((address, size, protection));
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
info = default;
return false;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
public void Dispose()
{
if (!_freed)
{
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
_freed = true;
}
}
}
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
{
public bool CommitSucceeds { get; set; } = true;
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
desiredAddress == address ? address : 0;
public bool Commit(ulong commitAddress, ulong size, HostPageProtection protection)
{
CommitCalls.Add((commitAddress, size, protection));
return CommitSucceeds;
}
public bool Free(ulong freeAddress) => freeAddress == address;
public bool Protect(ulong protectAddress, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong protectAddress, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong queryAddress, out HostRegionInfo info)
{
var pageAddress = queryAddress & ~0xFFFUL;
info = new HostRegionInfo(
pageAddress,
address,
0x1000,
HostRegionState.Reserved,
0,
HostPageProtection.NoAccess,
0,
0);
return true;
}
public void FlushInstructionCache(ulong flushAddress, ulong size)
{
}
public void Dispose()
{
}
}
}
@@ -0,0 +1,87 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using Xunit;
namespace SharpEmu.Libs.Tests.Memory;
public sealed class VirtualMemoryTests
{
[Fact]
public void OutOfOrderMappingsRemainSortedAndResolveAtBoundaries()
{
var memory = new VirtualMemory();
memory.Map(0x3000, 0x100, 0, [3], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
memory.Map(0x1000, 0x100, 0, [1], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
memory.Map(0x2000, 0x100, 0, [2], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
Assert.Equal([0x1000UL, 0x2000UL, 0x3000UL], memory.SnapshotRegions().Select(region => region.VirtualAddress));
Span<byte> value = stackalloc byte[1];
Assert.True(memory.TryRead(0x1000, value));
Assert.Equal(1, value[0]);
Assert.True(memory.TryRead(0x2000, value));
Assert.Equal(2, value[0]);
Assert.True(memory.TryRead(0x3000, value));
Assert.Equal(3, value[0]);
Assert.False(memory.TryRead(0x1100, value));
Assert.False(memory.TryRead(0x0FFF, value));
}
[Fact]
public void MappingRejectsOverlapWithEitherNeighbor()
{
var memory = new VirtualMemory();
memory.Map(0x2000, 0x100, 0, [], ProgramHeaderFlags.Read);
memory.Map(0x4000, 0x100, 0, [], ProgramHeaderFlags.Read);
Assert.Throws<InvalidOperationException>(() =>
memory.Map(0x1FFF, 2, 0, [], ProgramHeaderFlags.Read));
Assert.Throws<InvalidOperationException>(() =>
memory.Map(0x3FFF, 2, 0, [], ProgramHeaderFlags.Read));
memory.Map(0x2100, 0x1F00, 0, [], ProgramHeaderFlags.Read);
Assert.Equal(3, memory.SnapshotRegions().Count);
}
[Fact]
public void ReadAndWriteSpanAdjacentRegions()
{
var memory = new VirtualMemory();
const ProgramHeaderFlags protection = ProgramHeaderFlags.Read | ProgramHeaderFlags.Write;
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], protection);
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], protection);
Span<byte> read = stackalloc byte[4];
Assert.True(memory.TryRead(0x1002, read));
Assert.Equal([3, 4, 5, 6], read.ToArray());
Assert.True(memory.TryWrite(0x1002, [9, 10, 11, 12]));
Assert.True(memory.TryRead(0x1000, read));
Assert.Equal([1, 2, 9, 10], read.ToArray());
Assert.True(memory.TryRead(0x1004, read));
Assert.Equal([11, 12, 7, 8], read.ToArray());
}
[Fact]
public void AccessRequiresPermissionAcrossEntireRange()
{
var memory = new VirtualMemory();
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], ProgramHeaderFlags.Read);
memory.Map(0x2000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Execute);
memory.Map(0x3000, 4, 0, [], ProgramHeaderFlags.Write);
Assert.False(memory.TryWrite(0x1002, [9, 9, 9, 9]));
Span<byte> unchanged = stackalloc byte[4];
Assert.True(memory.TryRead(0x1000, unchanged));
Assert.Equal([1, 2, 3, 4], unchanged.ToArray());
Assert.False(memory.TryRead(0x2000, unchanged));
Assert.False(memory.TryWrite(0x2000, [9]));
Assert.False(memory.TryRead(0x3000, unchanged));
Assert.True(memory.TryWrite(0x3000, [9]));
}
}
@@ -7,9 +7,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PropertyGroup>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SharpEmu.Core\SharpEmu.Core.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
</ItemGroup>
@@ -149,6 +149,15 @@
"xunit.extensibility.core": "[2.9.3]"
}
},
"sharpemu.core": {
"type": "Project",
"dependencies": {
"Iced": "[1.21.0, )",
"SharpEmu.HLE": "[1.0.0, )",
"SharpEmu.Libs": "[1.0.0, )",
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
@@ -168,6 +177,12 @@
"sharpemu.logging": {
"type": "Project"
},
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",