mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-27 13:11:04 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b86a91dfa | |||
| 72645cb373 | |||
| 2ad9836d13 | |||
| 62e1775c5c | |||
| 6dacd59a08 | |||
| 9d88542efd |
@@ -535,8 +535,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
out var blockContinuation,
|
out var blockContinuation,
|
||||||
out var hasBlockContinuation,
|
out var hasBlockContinuation,
|
||||||
out var blockWakeKey,
|
out var blockWakeKey,
|
||||||
out var blockResumeHandler,
|
out var blockWaiter,
|
||||||
out var blockWakeHandler,
|
|
||||||
out var blockDeadlineTimestamp) &&
|
out var blockDeadlineTimestamp) &&
|
||||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
||||||
{
|
{
|
||||||
@@ -546,8 +545,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
blockContinuation,
|
blockContinuation,
|
||||||
blockWakeKey,
|
blockWakeKey,
|
||||||
blockResumeHandler,
|
blockWaiter,
|
||||||
blockWakeHandler,
|
|
||||||
blockDeadlineTimestamp);
|
blockDeadlineTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,8 +676,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
out var blockContinuation,
|
out var blockContinuation,
|
||||||
out var hasBlockContinuation,
|
out var hasBlockContinuation,
|
||||||
out var blockWakeKey,
|
out var blockWakeKey,
|
||||||
out var blockResumeHandler,
|
out var blockWaiter,
|
||||||
out var blockWakeHandler,
|
|
||||||
out var blockDeadlineTimestamp) &&
|
out var blockDeadlineTimestamp) &&
|
||||||
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
||||||
{
|
{
|
||||||
@@ -689,8 +686,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
blockContinuation,
|
blockContinuation,
|
||||||
blockWakeKey,
|
blockWakeKey,
|
||||||
blockResumeHandler,
|
blockWaiter,
|
||||||
blockWakeHandler,
|
|
||||||
blockDeadlineTimestamp);
|
blockDeadlineTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1970,23 +1966,36 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
// Reads stay byte-by-byte through TryReadByteCompat (its Marshal.ReadByte
|
||||||
Span<byte> destination = stackalloc byte[1];
|
// fallback must probe exactly up to the terminator), but the bytes land in a
|
||||||
for (int i = 0; i < maxLength; i++)
|
// 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)
|
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 string? BlockWakeKey { get; set; }
|
||||||
|
|
||||||
public Func<int>? BlockResumeHandler { get; set; }
|
// Stays set through the wake transition; Resume() consumes it when the thread pumps.
|
||||||
|
public IGuestThreadBlockWaiter? BlockWaiter { get; set; }
|
||||||
public Func<bool>? BlockWakeHandler { get; set; }
|
|
||||||
|
|
||||||
public long BlockDeadlineTimestamp { get; set; }
|
public long BlockDeadlineTimestamp { get; set; }
|
||||||
|
|
||||||
@@ -2797,14 +2796,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thread.BlockWakeHandler is not null && !thread.BlockWakeHandler())
|
if (thread.BlockWaiter is not null && !thread.BlockWaiter.TryWake())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
thread.State = GuestThreadRunState.Ready;
|
thread.State = GuestThreadRunState.Ready;
|
||||||
thread.BlockReason = null;
|
thread.BlockReason = null;
|
||||||
thread.BlockWakeHandler = null;
|
|
||||||
thread.BlockDeadlineTimestamp = 0;
|
thread.BlockDeadlineTimestamp = 0;
|
||||||
_readyGuestThreads.Enqueue(thread);
|
_readyGuestThreads.Enqueue(thread);
|
||||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||||
@@ -2855,8 +2853,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ulong guestThreadHandle,
|
ulong guestThreadHandle,
|
||||||
GuestCpuContinuation continuation,
|
GuestCpuContinuation continuation,
|
||||||
string wakeKey,
|
string wakeKey,
|
||||||
Func<int>? resumeHandler,
|
IGuestThreadBlockWaiter? waiter,
|
||||||
Func<bool>? wakeHandler,
|
|
||||||
long blockDeadlineTimestamp)
|
long blockDeadlineTimestamp)
|
||||||
{
|
{
|
||||||
if (guestThreadHandle == 0 || continuation.Rip < 65536 || continuation.Rsp == 0)
|
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.BlockedContinuation = continuation;
|
||||||
thread.HasBlockedContinuation = true;
|
thread.HasBlockedContinuation = true;
|
||||||
thread.BlockWakeKey = wakeKey;
|
thread.BlockWakeKey = wakeKey;
|
||||||
thread.BlockResumeHandler = resumeHandler;
|
thread.BlockWaiter = waiter;
|
||||||
thread.BlockWakeHandler = wakeHandler;
|
|
||||||
thread.BlockDeadlineTimestamp = blockDeadlineTimestamp;
|
thread.BlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2898,7 +2894,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
thread.State = GuestThreadRunState.Ready;
|
thread.State = GuestThreadRunState.Ready;
|
||||||
thread.BlockReason = null;
|
thread.BlockReason = null;
|
||||||
thread.BlockWakeHandler = null;
|
|
||||||
thread.BlockDeadlineTimestamp = 0;
|
thread.BlockDeadlineTimestamp = 0;
|
||||||
_readyGuestThreads.Enqueue(thread);
|
_readyGuestThreads.Enqueue(thread);
|
||||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||||
@@ -3575,7 +3570,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
LastError = null;
|
LastError = null;
|
||||||
GuestCpuContinuation continuation = default;
|
GuestCpuContinuation continuation = default;
|
||||||
Func<int>? resumeHandler = null;
|
IGuestThreadBlockWaiter? blockWaiter = null;
|
||||||
var resumeContinuation = false;
|
var resumeContinuation = false;
|
||||||
lock (_guestThreadGate)
|
lock (_guestThreadGate)
|
||||||
{
|
{
|
||||||
@@ -3585,17 +3580,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
thread.BlockedContinuation = default;
|
thread.BlockedContinuation = default;
|
||||||
thread.HasBlockedContinuation = false;
|
thread.HasBlockedContinuation = false;
|
||||||
thread.BlockWakeKey = null;
|
thread.BlockWakeKey = null;
|
||||||
resumeHandler = thread.BlockResumeHandler;
|
blockWaiter = thread.BlockWaiter;
|
||||||
thread.BlockResumeHandler = null;
|
thread.BlockWaiter = null;
|
||||||
thread.BlockWakeHandler = null;
|
|
||||||
thread.BlockDeadlineTimestamp = 0;
|
thread.BlockDeadlineTimestamp = 0;
|
||||||
resumeContinuation = true;
|
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)
|
if (_logGuestThreads)
|
||||||
@@ -3620,12 +3614,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
thread.State = GuestThreadRunState.Blocked;
|
thread.State = GuestThreadRunState.Blocked;
|
||||||
thread.BlockReason = blockReason;
|
thread.BlockReason = blockReason;
|
||||||
if (thread.HasBlockedContinuation &&
|
if (thread.HasBlockedContinuation &&
|
||||||
thread.BlockWakeHandler is not null &&
|
thread.BlockWaiter is not null &&
|
||||||
thread.BlockWakeHandler())
|
thread.BlockWaiter.TryWake())
|
||||||
{
|
{
|
||||||
thread.State = GuestThreadRunState.Ready;
|
thread.State = GuestThreadRunState.Ready;
|
||||||
thread.BlockReason = null;
|
thread.BlockReason = null;
|
||||||
thread.BlockWakeHandler = null;
|
|
||||||
thread.BlockDeadlineTimestamp = 0;
|
thread.BlockDeadlineTimestamp = 0;
|
||||||
_readyGuestThreads.Enqueue(thread);
|
_readyGuestThreads.Enqueue(thread);
|
||||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||||
|
|||||||
@@ -50,4 +50,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
|||||||
address = 0;
|
address = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryFreeGuestMemory(ulong address)
|
||||||
|
{
|
||||||
|
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private readonly IHostMemory _hostMemory;
|
private readonly IHostMemory _hostMemory;
|
||||||
private ulong _guestAllocationArenaBase;
|
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();
|
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||||
|
|
||||||
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||||
@@ -301,7 +302,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
GuestAllocationArenaSize,
|
GuestAllocationArenaSize,
|
||||||
executable: false,
|
executable: false,
|
||||||
allowAlternative: true);
|
allowAlternative: true);
|
||||||
_guestAllocationOffset = GuestAllocationArenaStartOffset;
|
_guestAllocationFreeRanges.Add(
|
||||||
|
GuestAllocationArenaStartOffset,
|
||||||
|
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -309,14 +312,89 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
|
ulong rangeOffset = 0;
|
||||||
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
|
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;
|
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;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +458,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
_guestAllocationArenaBase = 0;
|
_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)
|
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)
|
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
|
||||||
{
|
{
|
||||||
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
||||||
var mergedFlags = existingFlags | flags;
|
var mergedFlags = existingFlags | flags;
|
||||||
_pageProtections[pageAddress] = mergedFlags;
|
_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();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return FindRegion(virtualAddress, 1) is not null
|
var region = FindRegion(virtualAddress, 1);
|
||||||
? (void*)virtualAddress
|
if (region is null ||
|
||||||
: null;
|
(region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (void*)virtualAddress;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,15 +41,14 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
|
|
||||||
lock (_gate)
|
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),
|
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
||||||
endAddress,
|
endAddress,
|
||||||
backingMemory));
|
backingMemory));
|
||||||
@@ -74,12 +73,12 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
|
CopyFromRegions(virtualAddress, destination, regionIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,39 +87,127 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
{
|
{
|
||||||
lock (_gate)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
|
CopyToRegions(virtualAddress, source, regionIndex);
|
||||||
return true;
|
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)
|
return false;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
|
|
||||||
if (candidateOffset + length > candidate.BackingMemory.Length)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
region = candidate;
|
|
||||||
offset = candidateOffset;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
region = default;
|
var currentAddress = virtualAddress;
|
||||||
offset = 0;
|
var remaining = length;
|
||||||
return false;
|
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);
|
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
||||||
|
|||||||
@@ -26,6 +26,15 @@
|
|||||||
"Library.Loading": "Carregando biblioteca…",
|
"Library.Loading": "Carregando biblioteca…",
|
||||||
|
|
||||||
"Options.General": "Opções Gerais",
|
"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.Emulation": "EMULAÇÃO",
|
||||||
"Options.Section.Logging": "LOGS",
|
"Options.Section.Logging": "LOGS",
|
||||||
"Options.Section.Launcher": "INICIALIZADOR",
|
"Options.Section.Launcher": "INICIALIZADOR",
|
||||||
@@ -126,4 +135,4 @@
|
|||||||
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
||||||
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
||||||
"Dialog.LogFiles": "Arquivos de log"
|
"Dialog.LogFiles": "Arquivos de log"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!"
|
||||||
|
}
|
||||||
@@ -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!"
|
||||||
|
}
|
||||||
@@ -12,7 +12,8 @@ using Avalonia.Platform;
|
|||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
using SharpEmu.Libs.Pad;
|
using SharpEmu.HLE.Host;
|
||||||
|
using SharpEmu.HLE.Host.Windows;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
@@ -62,7 +63,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
// Controller navigation state.
|
// Controller navigation state.
|
||||||
private readonly DispatcherTimer _gamepadTimer;
|
private readonly DispatcherTimer _gamepadTimer;
|
||||||
private uint _previousPadButtons;
|
private HostGamepadButtons _previousPadButtons;
|
||||||
private long _navLeftNextAt;
|
private long _navLeftNextAt;
|
||||||
private long _navRightNextAt;
|
private long _navRightNextAt;
|
||||||
private long _navUpNextAt;
|
private long _navUpNextAt;
|
||||||
@@ -151,8 +152,8 @@ public partial class MainWindow : Window
|
|||||||
Opened += async (_, _) => await OnOpenedAsync();
|
Opened += async (_, _) => await OnOpenedAsync();
|
||||||
Closing += (_, _) => OnWindowClosing();
|
Closing += (_, _) => OnWindowClosing();
|
||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
WindowsDualSenseReader.EnsureStarted();
|
||||||
XInputReader.EnsureStarted();
|
WindowsXInputReader.EnsureStarted();
|
||||||
_gamepadTimer = new DispatcherTimer
|
_gamepadTimer = new DispatcherTimer
|
||||||
{
|
{
|
||||||
Interval = TimeSpan.FromMilliseconds(50),
|
Interval = TimeSpan.FromMilliseconds(50),
|
||||||
@@ -224,9 +225,9 @@ public partial class MainWindow : Window
|
|||||||
private void PollGamepad()
|
private void PollGamepad()
|
||||||
{
|
{
|
||||||
// DualSense wins when both are connected; XInput covers Xbox pads.
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,12 +240,12 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
|
||||||
if ((shoulderPressed & OrbisPadButton.L1) != 0)
|
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
|
||||||
{
|
{
|
||||||
SetActivePage(0);
|
SetActivePage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((shoulderPressed & OrbisPadButton.R1) != 0)
|
if ((shoulderPressed & HostGamepadButtons.R1) != 0)
|
||||||
{
|
{
|
||||||
SetActivePage(1);
|
SetActivePage(1);
|
||||||
}
|
}
|
||||||
@@ -256,10 +257,10 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var now = Environment.TickCount64;
|
var now = Environment.TickCount64;
|
||||||
var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64;
|
var left = (pad.Buttons & HostGamepadButtons.Left) != 0 || pad.LeftX < 64;
|
||||||
var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192;
|
var right = (pad.Buttons & HostGamepadButtons.Right) != 0 || pad.LeftX > 192;
|
||||||
var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64;
|
var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
|
||||||
var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192;
|
var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
|
||||||
|
|
||||||
if (ShouldNavigate(left, ref _navLeftNextAt, now))
|
if (ShouldNavigate(left, ref _navLeftNextAt, now))
|
||||||
{
|
{
|
||||||
@@ -282,12 +283,12 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pressed = pad.Buttons & ~_previousPadButtons;
|
var pressed = pad.Buttons & ~_previousPadButtons;
|
||||||
if ((pressed & 0x4000) != 0) // Cross
|
if ((pressed & HostGamepadButtons.Cross) != 0)
|
||||||
{
|
{
|
||||||
LaunchSelected();
|
LaunchSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((pressed & 0x2000) != 0) // Circle
|
if ((pressed & HostGamepadButtons.Circle) != 0)
|
||||||
{
|
{
|
||||||
StopEmulator();
|
StopEmulator();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
<Version>0.0.1</Version>
|
<Version>0.0.1</Version>
|
||||||
|
<!-- Required by the source-generated LibraryImport stubs in the linked
|
||||||
|
controller readers below. -->
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Dependency-free; provides the BuildInfo provenance shown in the
|
<!-- 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
|
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||||
with the emulator's pad HLE. They are dependency-free, so they are
|
with the emulator's host input backend. They are dependency-free, so
|
||||||
compiled in directly rather than pulling a reference to all of
|
they are compiled in directly rather than pulling a reference to all
|
||||||
SharpEmu.Libs into the launcher. -->
|
of SharpEmu.HLE into the launcher. -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
|
<Compile Include="..\SharpEmu.HLE\Host\HostGamepadState.cs" Link="Input/HostGamepadState.cs" />
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
|
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsHidNative.cs" Link="Input/WindowsHidNative.cs" />
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsDualSenseReader.cs" Link="Input/WindowsDualSenseReader.cs" />
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
|
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsXInputReader.cs" Link="Input/WindowsXInputReader.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -238,23 +239,63 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytes = new byte[capacity];
|
const int StackBufferLength = 512;
|
||||||
for (var index = 0; index < bytes.Length; index++)
|
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);
|
ArrayPool<byte>.Shared.Return(rented);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
value = Encoding.UTF8.GetString(bytes);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool PushUInt64(ulong value)
|
public bool PushUInt64(ulong value)
|
||||||
|
|||||||
@@ -23,6 +23,20 @@ public readonly record struct GuestThreadSnapshot(
|
|||||||
ulong LastReturnRip,
|
ulong LastReturnRip,
|
||||||
string? BlockReason);
|
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
|
public interface IGuestThreadScheduler
|
||||||
{
|
{
|
||||||
bool SupportsGuestContextTransfer { get; }
|
bool SupportsGuestContextTransfer { get; }
|
||||||
@@ -106,10 +120,7 @@ public static class GuestThreadExecution
|
|||||||
private static string? _pendingBlockWakeKey;
|
private static string? _pendingBlockWakeKey;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static Func<int>? _pendingBlockResumeHandler;
|
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static Func<bool>? _pendingBlockWakeHandler;
|
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static long _pendingBlockDeadlineTimestamp;
|
private static long _pendingBlockDeadlineTimestamp;
|
||||||
@@ -157,8 +168,7 @@ public static class GuestThreadExecution
|
|||||||
_pendingBlockContinuationValid = false;
|
_pendingBlockContinuationValid = false;
|
||||||
_pendingBlockContinuation = default;
|
_pendingBlockContinuation = default;
|
||||||
_pendingBlockWakeKey = null;
|
_pendingBlockWakeKey = null;
|
||||||
_pendingBlockResumeHandler = null;
|
_pendingBlockWaiter = null;
|
||||||
_pendingBlockWakeHandler = null;
|
|
||||||
_pendingBlockDeadlineTimestamp = 0;
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitValue = 0;
|
_pendingEntryExitValue = 0;
|
||||||
@@ -179,8 +189,7 @@ public static class GuestThreadExecution
|
|||||||
_pendingBlockContinuationValid = false;
|
_pendingBlockContinuationValid = false;
|
||||||
_pendingBlockContinuation = default;
|
_pendingBlockContinuation = default;
|
||||||
_pendingBlockWakeKey = null;
|
_pendingBlockWakeKey = null;
|
||||||
_pendingBlockResumeHandler = null;
|
_pendingBlockWaiter = null;
|
||||||
_pendingBlockWakeHandler = null;
|
|
||||||
_pendingBlockDeadlineTimestamp = 0;
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitValue = 0;
|
_pendingEntryExitValue = 0;
|
||||||
@@ -211,8 +220,7 @@ public static class GuestThreadExecution
|
|||||||
CpuContext? context,
|
CpuContext? context,
|
||||||
string reason,
|
string reason,
|
||||||
string? wakeKey = null,
|
string? wakeKey = null,
|
||||||
Func<int>? resumeHandler = null,
|
IGuestThreadBlockWaiter? waiter = null,
|
||||||
Func<bool>? wakeHandler = null,
|
|
||||||
long blockDeadlineTimestamp = 0)
|
long blockDeadlineTimestamp = 0)
|
||||||
{
|
{
|
||||||
if (!IsGuestThread)
|
if (!IsGuestThread)
|
||||||
@@ -222,8 +230,7 @@ public static class GuestThreadExecution
|
|||||||
|
|
||||||
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
||||||
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
||||||
_pendingBlockResumeHandler = resumeHandler;
|
_pendingBlockWaiter = waiter;
|
||||||
_pendingBlockWakeHandler = wakeHandler;
|
|
||||||
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||||
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
||||||
{
|
{
|
||||||
@@ -255,7 +262,6 @@ public static class GuestThreadExecution
|
|||||||
out hasContinuation,
|
out hasContinuation,
|
||||||
out _,
|
out _,
|
||||||
out _,
|
out _,
|
||||||
out _,
|
|
||||||
out _);
|
out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,16 +270,14 @@ public static class GuestThreadExecution
|
|||||||
out GuestCpuContinuation continuation,
|
out GuestCpuContinuation continuation,
|
||||||
out bool hasContinuation,
|
out bool hasContinuation,
|
||||||
out string wakeKey,
|
out string wakeKey,
|
||||||
out Func<int>? resumeHandler,
|
out IGuestThreadBlockWaiter? waiter)
|
||||||
out Func<bool>? wakeHandler)
|
|
||||||
{
|
{
|
||||||
return TryConsumeCurrentThreadBlock(
|
return TryConsumeCurrentThreadBlock(
|
||||||
out reason,
|
out reason,
|
||||||
out continuation,
|
out continuation,
|
||||||
out hasContinuation,
|
out hasContinuation,
|
||||||
out wakeKey,
|
out wakeKey,
|
||||||
out resumeHandler,
|
out waiter,
|
||||||
out wakeHandler,
|
|
||||||
out _);
|
out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,8 +286,7 @@ public static class GuestThreadExecution
|
|||||||
out GuestCpuContinuation continuation,
|
out GuestCpuContinuation continuation,
|
||||||
out bool hasContinuation,
|
out bool hasContinuation,
|
||||||
out string wakeKey,
|
out string wakeKey,
|
||||||
out Func<int>? resumeHandler,
|
out IGuestThreadBlockWaiter? waiter,
|
||||||
out Func<bool>? wakeHandler,
|
|
||||||
out long blockDeadlineTimestamp)
|
out long blockDeadlineTimestamp)
|
||||||
{
|
{
|
||||||
reason = _pendingBlockReason ?? string.Empty;
|
reason = _pendingBlockReason ?? string.Empty;
|
||||||
@@ -292,8 +295,7 @@ public static class GuestThreadExecution
|
|||||||
continuation = default;
|
continuation = default;
|
||||||
hasContinuation = false;
|
hasContinuation = false;
|
||||||
wakeKey = string.Empty;
|
wakeKey = string.Empty;
|
||||||
resumeHandler = null;
|
waiter = null;
|
||||||
wakeHandler = null;
|
|
||||||
blockDeadlineTimestamp = 0;
|
blockDeadlineTimestamp = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -301,15 +303,13 @@ public static class GuestThreadExecution
|
|||||||
continuation = _pendingBlockContinuation;
|
continuation = _pendingBlockContinuation;
|
||||||
hasContinuation = _pendingBlockContinuationValid;
|
hasContinuation = _pendingBlockContinuationValid;
|
||||||
wakeKey = _pendingBlockWakeKey ?? reason;
|
wakeKey = _pendingBlockWakeKey ?? reason;
|
||||||
resumeHandler = _pendingBlockResumeHandler;
|
waiter = _pendingBlockWaiter;
|
||||||
wakeHandler = _pendingBlockWakeHandler;
|
|
||||||
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
||||||
_pendingBlockReason = null;
|
_pendingBlockReason = null;
|
||||||
_pendingBlockContinuation = default;
|
_pendingBlockContinuation = default;
|
||||||
_pendingBlockContinuationValid = false;
|
_pendingBlockContinuationValid = false;
|
||||||
_pendingBlockWakeKey = null;
|
_pendingBlockWakeKey = null;
|
||||||
_pendingBlockResumeHandler = null;
|
_pendingBlockWaiter = null;
|
||||||
_pendingBlockWakeHandler = null;
|
|
||||||
_pendingBlockDeadlineTimestamp = 0;
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -16,4 +16,8 @@ public interface IHostPlatform
|
|||||||
IHostThreading Threading { get; }
|
IHostThreading Threading { get; }
|
||||||
|
|
||||||
IHostSymbolResolver Symbols { get; }
|
IHostSymbolResolver Symbols { get; }
|
||||||
|
|
||||||
|
IHostAudioOutput Audio { get; }
|
||||||
|
|
||||||
|
IHostInput Input { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ public interface IHostThreading
|
|||||||
|
|
||||||
bool TrySetCurrentThreadAffinity(nuint affinityMask);
|
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>
|
/// <summary>
|
||||||
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
|
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
|
||||||
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
|
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
|
||||||
|
|||||||
+51
-49
@@ -3,21 +3,21 @@
|
|||||||
|
|
||||||
using Microsoft.Win32.SafeHandles;
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a DualSense controller over raw HID on a background thread.
|
/// Reads a DualSense controller over raw HID on a background thread.
|
||||||
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
||||||
/// activated by requesting feature report 0x05), with hot-plug retry.
|
/// activated by requesting feature report 0x05), with hot-plug retry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class DualSenseReader
|
internal static class WindowsDualSenseReader
|
||||||
{
|
{
|
||||||
private const ushort SonyVendorId = 0x054C;
|
private const ushort SonyVendorId = 0x054C;
|
||||||
private const ushort DualSenseProductId = 0x0CE6;
|
private const ushort DualSenseProductId = 0x0CE6;
|
||||||
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
private static readonly object Gate = new();
|
||||||
private static PadState _state;
|
private static HostGamepadState _state;
|
||||||
private static bool _started;
|
private static bool _started;
|
||||||
|
|
||||||
// Output (rumble/lightbar) state, all guarded by Gate.
|
// 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>
|
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||||
internal static void EnsureStarted()
|
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())
|
if (!OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
return;
|
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)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -69,7 +71,7 @@ internal static class DualSenseReader
|
|||||||
return state.Connected;
|
return state.Connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetState(in PadState state)
|
private static void SetState(in HostGamepadState state)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -148,11 +150,11 @@ internal static class DualSenseReader
|
|||||||
{
|
{
|
||||||
if (_outputStream is null)
|
if (_outputStream is null)
|
||||||
{
|
{
|
||||||
var handle = HidNative.CreateFile(
|
var handle = WindowsHidNative.CreateFile(
|
||||||
_devicePath,
|
_devicePath,
|
||||||
HidNative.GenericRead | HidNative.GenericWrite,
|
WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
|
||||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
|
||||||
0, HidNative.OpenExisting, 0, 0);
|
0, WindowsHidNative.OpenExisting, 0, 0);
|
||||||
if (handle.IsInvalid)
|
if (handle.IsInvalid)
|
||||||
{
|
{
|
||||||
handle.Dispose();
|
handle.Dispose();
|
||||||
@@ -262,7 +264,7 @@ internal static class DualSenseReader
|
|||||||
// to the full 0x31 input report. Harmless over USB.
|
// to the full 0x31 input report. Harmless over USB.
|
||||||
var feature = new byte[41];
|
var feature = new byte[41];
|
||||||
feature[0] = 0x05;
|
feature[0] = 0x05;
|
||||||
_ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
|
_ = WindowsHidNative.HidD_GetFeature(handle, feature, feature.Length);
|
||||||
|
|
||||||
if (!announcedConnect)
|
if (!announcedConnect)
|
||||||
{
|
{
|
||||||
@@ -320,18 +322,18 @@ internal static class DualSenseReader
|
|||||||
private static SafeFileHandle? OpenDualSense(out string? devicePath)
|
private static SafeFileHandle? OpenDualSense(out string? devicePath)
|
||||||
{
|
{
|
||||||
devicePath = null;
|
devicePath = null;
|
||||||
foreach (var path in HidNative.EnumerateHidDevicePaths())
|
foreach (var path in WindowsHidNative.EnumerateHidDevicePaths())
|
||||||
{
|
{
|
||||||
// Open without access rights just to query VID/PID.
|
// Open without access rights just to query VID/PID.
|
||||||
using var probe = HidNative.CreateFile(
|
using var probe = WindowsHidNative.CreateFile(
|
||||||
path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0);
|
path, 0, WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite, 0, WindowsHidNative.OpenExisting, 0, 0);
|
||||||
if (probe.IsInvalid)
|
if (probe.IsInvalid)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var attributes = new HidNative.HiddAttributes { Size = 12 };
|
var attributes = new WindowsHidNative.HiddAttributes { Size = 12 };
|
||||||
if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
|
if (!WindowsHidNative.HidD_GetAttributes(probe, ref attributes) ||
|
||||||
attributes.VendorId != SonyVendorId ||
|
attributes.VendorId != SonyVendorId ||
|
||||||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
|
(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.
|
// Read+write so feature reports work; fall back to read-only.
|
||||||
var handle = HidNative.CreateFile(
|
var handle = WindowsHidNative.CreateFile(
|
||||||
path,
|
path,
|
||||||
HidNative.GenericRead | HidNative.GenericWrite,
|
WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
|
||||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
|
||||||
0, HidNative.OpenExisting, 0, 0);
|
0, WindowsHidNative.OpenExisting, 0, 0);
|
||||||
if (handle.IsInvalid)
|
if (handle.IsInvalid)
|
||||||
{
|
{
|
||||||
handle.Dispose();
|
handle.Dispose();
|
||||||
handle = HidNative.CreateFile(
|
handle = WindowsHidNative.CreateFile(
|
||||||
path,
|
path,
|
||||||
HidNative.GenericRead,
|
WindowsHidNative.GenericRead,
|
||||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
|
||||||
0, HidNative.OpenExisting, 0, 0);
|
0, WindowsHidNative.OpenExisting, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!handle.IsInvalid)
|
if (!handle.IsInvalid)
|
||||||
@@ -366,7 +368,7 @@ internal static class DualSenseReader
|
|||||||
return null;
|
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].
|
// USB: report id 0x01, payload starts at [1].
|
||||||
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
|
// 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 buttons1 = report[offset + 8];
|
||||||
var buttons2 = report[offset + 9];
|
var buttons2 = report[offset + 9];
|
||||||
|
|
||||||
uint buttons = 0;
|
var buttons = HostGamepadButtons.None;
|
||||||
buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0;
|
buttons |= (buttons0 & 0x10) != 0 ? HostGamepadButtons.Square : 0;
|
||||||
buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0;
|
buttons |= (buttons0 & 0x20) != 0 ? HostGamepadButtons.Cross : 0;
|
||||||
buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0;
|
buttons |= (buttons0 & 0x40) != 0 ? HostGamepadButtons.Circle : 0;
|
||||||
buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0;
|
buttons |= (buttons0 & 0x80) != 0 ? HostGamepadButtons.Triangle : 0;
|
||||||
buttons |= HatToButtons(buttons0 & 0x0F);
|
buttons |= HatToButtons(buttons0 & 0x0F);
|
||||||
buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0;
|
buttons |= (buttons1 & 0x01) != 0 ? HostGamepadButtons.L1 : 0;
|
||||||
buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0;
|
buttons |= (buttons1 & 0x02) != 0 ? HostGamepadButtons.R1 : 0;
|
||||||
buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0;
|
buttons |= (buttons1 & 0x04) != 0 ? HostGamepadButtons.L2 : 0;
|
||||||
buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0;
|
buttons |= (buttons1 & 0x08) != 0 ? HostGamepadButtons.R2 : 0;
|
||||||
buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0;
|
buttons |= (buttons1 & 0x20) != 0 ? HostGamepadButtons.Options : 0;
|
||||||
buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0;
|
buttons |= (buttons1 & 0x40) != 0 ? HostGamepadButtons.L3 : 0;
|
||||||
buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0;
|
buttons |= (buttons1 & 0x80) != 0 ? HostGamepadButtons.R3 : 0;
|
||||||
buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0;
|
buttons |= (buttons2 & 0x02) != 0 ? HostGamepadButtons.TouchPad : 0;
|
||||||
|
|
||||||
state = new PadState(
|
state = new HostGamepadState(
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Buttons: buttons,
|
Buttons: buttons,
|
||||||
LeftX: leftX,
|
LeftX: leftX,
|
||||||
LeftY: leftY,
|
LeftY: leftY,
|
||||||
RightX: rightX,
|
RightX: rightX,
|
||||||
RightY: rightY,
|
RightY: rightY,
|
||||||
L2: l2,
|
LeftTrigger: l2,
|
||||||
R2: r2);
|
RightTrigger: r2);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint HatToButtons(int hat) => hat switch
|
private static HostGamepadButtons HatToButtons(int hat) => hat switch
|
||||||
{
|
{
|
||||||
0 => OrbisPadButton.Up,
|
0 => HostGamepadButtons.Up,
|
||||||
1 => OrbisPadButton.Up | OrbisPadButton.Right,
|
1 => HostGamepadButtons.Up | HostGamepadButtons.Right,
|
||||||
2 => OrbisPadButton.Right,
|
2 => HostGamepadButtons.Right,
|
||||||
3 => OrbisPadButton.Right | OrbisPadButton.Down,
|
3 => HostGamepadButtons.Right | HostGamepadButtons.Down,
|
||||||
4 => OrbisPadButton.Down,
|
4 => HostGamepadButtons.Down,
|
||||||
5 => OrbisPadButton.Down | OrbisPadButton.Left,
|
5 => HostGamepadButtons.Down | HostGamepadButtons.Left,
|
||||||
6 => OrbisPadButton.Left,
|
6 => HostGamepadButtons.Left,
|
||||||
7 => OrbisPadButton.Left | OrbisPadButton.Up,
|
7 => HostGamepadButtons.Left | HostGamepadButtons.Up,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+23
-18
@@ -4,13 +4,13 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Microsoft.Win32.SafeHandles;
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimal Win32 HID interop used to talk to a DualSense controller
|
/// Minimal Win32 HID interop used to talk to a DualSense controller
|
||||||
/// directly, without any external input library.
|
/// directly, without any external input library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static partial class HidNative
|
internal static partial class WindowsHidNative
|
||||||
{
|
{
|
||||||
internal const int DigcfPresent = 0x02;
|
internal const int DigcfPresent = 0x02;
|
||||||
internal const int DigcfDeviceInterface = 0x10;
|
internal const int DigcfDeviceInterface = 0x10;
|
||||||
@@ -38,28 +38,32 @@ internal static partial class HidNative
|
|||||||
public ushort VersionNumber;
|
public ushort VersionNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("hid.dll")]
|
[LibraryImport("hid.dll")]
|
||||||
internal static extern void HidD_GetHidGuid(out Guid hidGuid);
|
internal static partial void HidD_GetHidGuid(out Guid hidGuid);
|
||||||
|
|
||||||
[DllImport("hid.dll")]
|
[LibraryImport("hid.dll")]
|
||||||
internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
|
||||||
|
|
||||||
[DllImport("hid.dll")]
|
[LibraryImport("hid.dll")]
|
||||||
internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool HidD_GetFeature(SafeFileHandle hidDeviceObject, [In, Out] byte[] reportBuffer, int reportBufferLength);
|
||||||
|
|
||||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
[LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetClassDevsW")]
|
||||||
internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
|
internal static partial nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
|
||||||
|
|
||||||
[DllImport("setupapi.dll")]
|
[LibraryImport("setupapi.dll")]
|
||||||
internal static extern bool SetupDiEnumDeviceInterfaces(
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetupDiEnumDeviceInterfaces(
|
||||||
nint deviceInfoSet,
|
nint deviceInfoSet,
|
||||||
nint deviceInfoData,
|
nint deviceInfoData,
|
||||||
ref Guid interfaceClassGuid,
|
ref Guid interfaceClassGuid,
|
||||||
int memberIndex,
|
int memberIndex,
|
||||||
ref SpDeviceInterfaceData deviceInterfaceData);
|
ref SpDeviceInterfaceData deviceInterfaceData);
|
||||||
|
|
||||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
[LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetDeviceInterfaceDetailW")]
|
||||||
internal static extern bool SetupDiGetDeviceInterfaceDetail(
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetupDiGetDeviceInterfaceDetail(
|
||||||
nint deviceInfoSet,
|
nint deviceInfoSet,
|
||||||
ref SpDeviceInterfaceData deviceInterfaceData,
|
ref SpDeviceInterfaceData deviceInterfaceData,
|
||||||
nint deviceInterfaceDetailData,
|
nint deviceInterfaceDetailData,
|
||||||
@@ -67,11 +71,12 @@ internal static partial class HidNative
|
|||||||
out int requiredSize,
|
out int requiredSize,
|
||||||
nint deviceInfoData);
|
nint deviceInfoData);
|
||||||
|
|
||||||
[DllImport("setupapi.dll")]
|
[LibraryImport("setupapi.dll")]
|
||||||
internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
|
||||||
|
|
||||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
[LibraryImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
|
||||||
internal static extern SafeFileHandle CreateFile(
|
internal static partial SafeFileHandle CreateFile(
|
||||||
string fileName,
|
string fileName,
|
||||||
uint desiredAccess,
|
uint desiredAccess,
|
||||||
uint shareMode,
|
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 IHostThreading Threading { get; } = new WindowsHostThreading();
|
||||||
|
|
||||||
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
|
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 CtxRbp = 160;
|
||||||
private const int CtxRip = 248;
|
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 uint AllocateTlsSlot() => TlsAlloc();
|
||||||
|
|
||||||
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
|
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
|
||||||
@@ -162,4 +187,7 @@ internal sealed unsafe partial class WindowsHostThreading : IHostThreading
|
|||||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static partial bool CloseHandle(nint hObject);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
-34
@@ -3,15 +3,15 @@
|
|||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.HLE.Host.Windows;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
|
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
|
||||||
/// the Windows XInput API on a background thread, translated to the same
|
/// the Windows XInput API on a background thread, translated to
|
||||||
/// ORBIS pad conventions as <see cref="DualSenseReader"/>. Supports rumble
|
/// <see cref="HostGamepadState"/> conventions. Supports rumble and hot-plug
|
||||||
/// and hot-plug retry; the first connected slot (of four) is used.
|
/// retry; the first connected slot (of four) is used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class XInputReader
|
internal static partial class WindowsXInputReader
|
||||||
{
|
{
|
||||||
private const uint ErrorSuccess = 0;
|
private const uint ErrorSuccess = 0;
|
||||||
private const int SlotCount = 4;
|
private const int SlotCount = 4;
|
||||||
@@ -34,7 +34,7 @@ internal static class XInputReader
|
|||||||
private const ushort XinputY = 0x8000;
|
private const ushort XinputY = 0x8000;
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
private static readonly object Gate = new();
|
||||||
private static PadState _state;
|
private static HostGamepadState _state;
|
||||||
private static bool _started;
|
private static bool _started;
|
||||||
private static int _slot = -1; // connected XInput user index, -1 when none
|
private static int _slot = -1; // connected XInput user index, -1 when none
|
||||||
private static byte _motorLeft;
|
private static byte _motorLeft;
|
||||||
@@ -45,6 +45,8 @@ internal static class XInputReader
|
|||||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||||
internal static void EnsureStarted()
|
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())
|
if (!OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
return;
|
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)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -77,7 +79,7 @@ internal static class XInputReader
|
|||||||
return state.Connected;
|
return state.Connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetState(in PadState state)
|
private static void SetState(in HostGamepadState state)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -204,40 +206,40 @@ internal static class XInputReader
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PadState Translate(in XInputGamepad pad)
|
private static HostGamepadState Translate(in XInputGamepad pad)
|
||||||
{
|
{
|
||||||
uint buttons = 0;
|
var buttons = HostGamepadButtons.None;
|
||||||
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0;
|
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? HostGamepadButtons.Up : 0;
|
||||||
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0;
|
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? HostGamepadButtons.Down : 0;
|
||||||
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0;
|
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? HostGamepadButtons.Left : 0;
|
||||||
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0;
|
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? HostGamepadButtons.Right : 0;
|
||||||
buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0;
|
buttons |= (pad.Buttons & XinputStart) != 0 ? HostGamepadButtons.Options : 0;
|
||||||
buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0;
|
buttons |= (pad.Buttons & XinputBack) != 0 ? HostGamepadButtons.TouchPad : 0;
|
||||||
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0;
|
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? HostGamepadButtons.L3 : 0;
|
||||||
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0;
|
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? HostGamepadButtons.R3 : 0;
|
||||||
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0;
|
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? HostGamepadButtons.L1 : 0;
|
||||||
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0;
|
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? HostGamepadButtons.R1 : 0;
|
||||||
buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0;
|
buttons |= (pad.Buttons & XinputA) != 0 ? HostGamepadButtons.Cross : 0;
|
||||||
buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0;
|
buttons |= (pad.Buttons & XinputB) != 0 ? HostGamepadButtons.Circle : 0;
|
||||||
buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0;
|
buttons |= (pad.Buttons & XinputX) != 0 ? HostGamepadButtons.Square : 0;
|
||||||
buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0;
|
buttons |= (pad.Buttons & XinputY) != 0 ? HostGamepadButtons.Triangle : 0;
|
||||||
buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0;
|
buttons |= pad.LeftTrigger > TriggerThreshold ? HostGamepadButtons.L2 : 0;
|
||||||
buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0;
|
buttons |= pad.RightTrigger > TriggerThreshold ? HostGamepadButtons.R2 : 0;
|
||||||
|
|
||||||
return new PadState(
|
return new HostGamepadState(
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Buttons: buttons,
|
Buttons: buttons,
|
||||||
LeftX: AxisToByte(pad.ThumbLX),
|
LeftX: AxisToByte(pad.ThumbLX),
|
||||||
LeftY: AxisToByteInverted(pad.ThumbLY),
|
LeftY: AxisToByteInverted(pad.ThumbLY),
|
||||||
RightX: AxisToByte(pad.ThumbRX),
|
RightX: AxisToByte(pad.ThumbRX),
|
||||||
RightY: AxisToByteInverted(pad.ThumbRY),
|
RightY: AxisToByteInverted(pad.ThumbRY),
|
||||||
L2: pad.LeftTrigger,
|
LeftTrigger: pad.LeftTrigger,
|
||||||
R2: pad.RightTrigger);
|
RightTrigger: pad.RightTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
|
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));
|
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
@@ -267,9 +269,9 @@ internal static class XInputReader
|
|||||||
}
|
}
|
||||||
|
|
||||||
// xinput1_4.dll ships with Windows 8 and later.
|
// xinput1_4.dll ships with Windows 8 and later.
|
||||||
[DllImport("xinput1_4.dll")]
|
[LibraryImport("xinput1_4.dll")]
|
||||||
private static extern uint XInputGetState(uint userIndex, out XInputState state);
|
private static partial uint XInputGetState(uint userIndex, out XInputState state);
|
||||||
|
|
||||||
[DllImport("xinput1_4.dll")]
|
[LibraryImport("xinput1_4.dll")]
|
||||||
private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration);
|
private static partial uint XInputSetState(uint userIndex, ref XInputVibration vibration);
|
||||||
}
|
}
|
||||||
@@ -6,4 +6,6 @@ namespace SharpEmu.HLE;
|
|||||||
public interface IGuestMemoryAllocator
|
public interface IGuestMemoryAllocator
|
||||||
{
|
{
|
||||||
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
||||||
|
|
||||||
|
bool TryFreeGuestMemory(ulong address);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -27,7 +28,7 @@ public static class AudioOutExports
|
|||||||
int channels,
|
int channels,
|
||||||
int bytesPerSample,
|
int bytesPerSample,
|
||||||
bool isFloat,
|
bool isFloat,
|
||||||
WinMmAudioPort? backend)
|
IHostAudioStream? backend)
|
||||||
{
|
{
|
||||||
UserId = userId;
|
UserId = userId;
|
||||||
Type = type;
|
Type = type;
|
||||||
@@ -48,7 +49,7 @@ public static class AudioOutExports
|
|||||||
public int Channels { get; }
|
public int Channels { get; }
|
||||||
public int BytesPerSample { get; }
|
public int BytesPerSample { get; }
|
||||||
public bool IsFloat { get; }
|
public bool IsFloat { get; }
|
||||||
public WinMmAudioPort? Backend { get; }
|
public IHostAudioStream? Backend { get; }
|
||||||
public int BufferByteLength =>
|
public int BufferByteLength =>
|
||||||
checked((int)BufferLength * Channels * BytesPerSample);
|
checked((int)BufferLength * Channels * BytesPerSample);
|
||||||
|
|
||||||
@@ -103,12 +104,13 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
WinMmAudioPort? backend = null;
|
IHostAudioStream? backend = null;
|
||||||
string backendName;
|
string backendName;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
backend = new WinMmAudioPort(frequency);
|
var audio = HostPlatform.Current.Audio;
|
||||||
backendName = "winmm";
|
backend = audio.OpenStereoPcm16Stream(frequency);
|
||||||
|
backendName = audio.BackendName;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@@ -180,15 +182,31 @@ public static class AudioOutExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (port.Backend is null ||
|
if (port.Backend is null)
|
||||||
!port.Backend.Submit(
|
|
||||||
source,
|
|
||||||
port.BufferLength,
|
|
||||||
port.Channels,
|
|
||||||
port.BytesPerSample,
|
|
||||||
port.IsFloat))
|
|
||||||
{
|
{
|
||||||
port.PaceSilence();
|
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);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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();
|
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; }
|
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(
|
[SysAbiExport(
|
||||||
@@ -249,27 +283,22 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||||
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
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(
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEventFlag",
|
"sceKernelWaitEventFlag",
|
||||||
GetEventFlagWakeKey(handle),
|
GetEventFlagWakeKey(handle),
|
||||||
resumeHandler: () => CompleteBlockedTimedWait(
|
timedWaiter,
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
timedWaiter,
|
|
||||||
pattern,
|
|
||||||
waitMode,
|
|
||||||
resultAddress,
|
|
||||||
timeoutAddress,
|
|
||||||
deadline),
|
|
||||||
wakeHandler: () => TryCompleteBlockedTimedWait(
|
|
||||||
ctx,
|
|
||||||
state,
|
|
||||||
timedWaiter,
|
|
||||||
pattern,
|
|
||||||
waitMode,
|
|
||||||
resultAddress),
|
|
||||||
blockDeadlineTimestamp: deadline))
|
blockDeadlineTimestamp: deadline))
|
||||||
{
|
{
|
||||||
state.WaitingThreads++;
|
state.WaitingThreads++;
|
||||||
@@ -286,27 +315,17 @@ public static class KernelEventFlagCompatExports
|
|||||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
var managedThread = Environment.CurrentManagedThreadId;
|
var managedThread = Environment.CurrentManagedThreadId;
|
||||||
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEventFlag",
|
"sceKernelWaitEventFlag",
|
||||||
GetEventFlagWakeKey(handle),
|
GetEventFlagWakeKey(handle),
|
||||||
() => (int)blockedWaitResult,
|
new EventFlagWaiter
|
||||||
() =>
|
|
||||||
{
|
{
|
||||||
if (!TryPrepareBlockedWait(
|
Ctx = ctx,
|
||||||
ctx,
|
State = state,
|
||||||
state,
|
Pattern = pattern,
|
||||||
pattern,
|
WaitMode = waitMode,
|
||||||
waitMode,
|
ResultAddress = resultAddress,
|
||||||
resultAddress,
|
|
||||||
out var preparedResult))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockedWaitResult = preparedResult;
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
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-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)}");
|
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -17,7 +19,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
private static readonly object _eventQueueGate = new();
|
private static readonly object _eventQueueGate = new();
|
||||||
private static readonly HashSet<ulong> _eventQueues = 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 readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
||||||
private static long _nextEventQueueHandle = 1;
|
private static long _nextEventQueueHandle = 1;
|
||||||
|
|
||||||
@@ -34,6 +36,76 @@ public static class KernelEventQueueCompatExports
|
|||||||
short Filter,
|
short Filter,
|
||||||
ulong UserData);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "D0OdFMjp46I",
|
Nid = "D0OdFMjp46I",
|
||||||
ExportName = "sceKernelCreateEqueue",
|
ExportName = "sceKernelCreateEqueue",
|
||||||
@@ -51,7 +123,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
_eventQueues.Add(handle);
|
_eventQueues.Add(handle);
|
||||||
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
_pendingEvents[handle] = new KernelEventDeque();
|
||||||
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +151,8 @@ public static class KernelEventQueueCompatExports
|
|||||||
_registeredEvents.Remove(handle);
|
_registeredEvents.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_wakeKeys.TryRemove(handle, out _);
|
||||||
|
|
||||||
TraceEventQueue(ctx, "delete", handle);
|
TraceEventQueue(ctx, "delete", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -343,8 +417,14 @@ public static class KernelEventQueueCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitEqueue",
|
"sceKernelWaitEqueue",
|
||||||
GetEventQueueWakeKey(handle),
|
GetEventQueueWakeKey(handle),
|
||||||
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
|
new EqueueWaiter
|
||||||
() => HasPendingEvents(handle)))
|
{
|
||||||
|
Ctx = ctx,
|
||||||
|
Handle = handle,
|
||||||
|
EventsAddress = eventsAddress,
|
||||||
|
EventCapacity = eventCapacity,
|
||||||
|
OutCountAddress = outCountAddress,
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "wait-block", handle);
|
TraceEventQueue(ctx, "wait-block", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -409,7 +489,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new LinkedList<KernelQueuedEvent>();
|
queue = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +560,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new LinkedList<KernelQueuedEvent>();
|
queue = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +607,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new LinkedList<KernelQueuedEvent>();
|
queue = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,15 +643,15 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var events))
|
if (!_pendingEvents.TryGetValue(handle, out var events))
|
||||||
{
|
{
|
||||||
events = new LinkedList<KernelQueuedEvent>();
|
events = new KernelEventDeque();
|
||||||
_pendingEvents[handle] = events;
|
_pendingEvents[handle] = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = 1UL;
|
var count = 1UL;
|
||||||
var pendingNode = FindPendingEvent(events, ident, filter);
|
var pendingIndex = events.FindIndex(ident, filter);
|
||||||
if (pendingNode is not null)
|
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;
|
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
|
||||||
@@ -584,9 +664,9 @@ public static class KernelEventQueueCompatExports
|
|||||||
eventData,
|
eventData,
|
||||||
userData);
|
userData);
|
||||||
|
|
||||||
if (pendingNode is not null)
|
if (pendingIndex >= 0)
|
||||||
{
|
{
|
||||||
pendingNode.Value = triggeredEvent;
|
events[pendingIndex] = triggeredEvent;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -631,40 +711,28 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void QueueOrUpdateEvent(
|
private static void QueueOrUpdateEvent(
|
||||||
LinkedList<KernelQueuedEvent> queue,
|
KernelEventDeque queue,
|
||||||
KernelQueuedEvent queuedEvent)
|
KernelQueuedEvent queuedEvent)
|
||||||
{
|
{
|
||||||
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter);
|
var pendingIndex = queue.FindIndex(queuedEvent.Ident, queuedEvent.Filter);
|
||||||
if (pendingNode is null)
|
if (pendingIndex < 0)
|
||||||
{
|
{
|
||||||
queue.AddLast(queuedEvent);
|
queue.AddLast(queuedEvent);
|
||||||
return;
|
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(
|
// Wake keys are formatted once per handle: WakeEventQueue runs on every event
|
||||||
LinkedList<KernelQueuedEvent> queue,
|
// enqueue (vblank/flip edges included), so formatting there is steady string churn.
|
||||||
ulong ident,
|
private static readonly ConcurrentDictionary<ulong, string> _wakeKeys = new();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetEventQueueWakeKey(ulong handle) =>
|
private static string GetEventQueueWakeKey(ulong handle) =>
|
||||||
$"sceKernelWaitEqueue:{handle:X16}";
|
_wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}");
|
||||||
|
|
||||||
private static void WakeEventQueue(ulong handle)
|
private static void WakeEventQueue(ulong handle)
|
||||||
{
|
{
|
||||||
@@ -678,7 +746,10 @@ public static class KernelEventQueueCompatExports
|
|||||||
return 0;
|
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;
|
KernelQueuedEvent[] events;
|
||||||
|
int count;
|
||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
||||||
@@ -686,24 +757,30 @@ public static class KernelEventQueueCompatExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = Math.Min(eventCapacity, queue.Count);
|
count = Math.Min(eventCapacity, queue.Count);
|
||||||
events = new KernelQueuedEvent[count];
|
events = ArrayPool<KernelQueuedEvent>.Shared.Rent(count);
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
events[i] = queue.First!.Value;
|
events[i] = queue.RemoveFirst();
|
||||||
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)
|
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
||||||
@@ -718,9 +795,12 @@ public static class KernelEventQueueCompatExports
|
|||||||
return ctx.Memory.TryWrite(address, eventBytes);
|
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)
|
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
|
if (!_logEqueue)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ public static class KernelMemoryCompatExports
|
|||||||
private const int MaxGuestStringLength = 4096;
|
private const int MaxGuestStringLength = 4096;
|
||||||
private const int WideCharSize = sizeof(ushort);
|
private const int WideCharSize = sizeof(ushort);
|
||||||
private const int MemsetChunkSize = 16 * 1024;
|
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 TlsModuleBlockSize = 0x10000;
|
||||||
private const int O_WRONLY = 0x1;
|
private const int O_WRONLY = 0x1;
|
||||||
private const int O_RDWR = 0x2;
|
private const int O_RDWR = 0x2;
|
||||||
@@ -255,11 +259,10 @@ public static class KernelMemoryCompatExports
|
|||||||
DirectStart: 0);
|
DirectStart: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var zeroes = new byte[(int)Math.Min(mappedLength, (ulong)MemsetChunkSize)];
|
|
||||||
for (ulong offset = 0; offset < mappedLength;)
|
for (ulong offset = 0; offset < mappedLength;)
|
||||||
{
|
{
|
||||||
var chunkLength = (int)Math.Min((ulong)zeroes.Length, mappedLength - offset);
|
var chunkLength = (int)Math.Min((ulong)_zeroChunk.Length, mappedLength - offset);
|
||||||
if (!ctx.Memory.TryWrite(address + offset, zeroes.AsSpan(0, chunkLength)))
|
if (!ctx.Memory.TryWrite(address + offset, _zeroChunk.AsSpan(0, chunkLength)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -422,33 +425,50 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunk = new byte[MemsetChunkSize];
|
// Rent may hand back a larger array than requested; only the first chunkLength
|
||||||
Array.Fill(chunk, value);
|
// bytes are filled, so the loop must cap at chunkLength rather than chunk.Length.
|
||||||
var remaining = length;
|
var chunkLength = (int)Math.Min(length, (ulong)MemsetChunkSize);
|
||||||
var cursor = destination;
|
var chunk = value == 0 ? _zeroChunk : ArrayPool<byte>.Shared.Rent(chunkLength);
|
||||||
while (remaining > 0)
|
if (value != 0)
|
||||||
{
|
{
|
||||||
var take = (int)Math.Min((ulong)chunk.Length, remaining);
|
chunk.AsSpan(0, chunkLength).Fill(value);
|
||||||
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
}
|
||||||
|
|
||||||
|
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 (length <= 0x40)
|
||||||
if (recoveryIndex <= 8)
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount);
|
||||||
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
|
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_ERROR_MEMORY_FAULT;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
ctx[CpuRegister.Rax] = destination;
|
||||||
@@ -1185,11 +1205,10 @@ public static class KernelMemoryCompatExports
|
|||||||
var rawCount = ctx[CpuRegister.Rdx];
|
var rawCount = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
// 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
|
// upstream bug that also feeds bad lengths to memset) must not turn into a multi-GB
|
||||||
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
|
// copy attempt from a guest-thread call context, which corrupted the CLR outright
|
||||||
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
|
// ("Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||||
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
|
// code") instead of throwing a normal exception. Reject anything above a sane bound.
|
||||||
// exception. Reject anything above a sane bound before allocating.
|
|
||||||
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
||||||
if (rawCount > maxSaneCount)
|
if (rawCount > maxSaneCount)
|
||||||
{
|
{
|
||||||
@@ -1198,15 +1217,52 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = (int)rawCount;
|
ctx[CpuRegister.Rax] = destination;
|
||||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
if (rawCount == 0)
|
||||||
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = destination;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
}
|
||||||
|
|
||||||
|
// 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;
|
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");
|
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 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 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
|
private sealed class PthreadCondState
|
||||||
@@ -629,6 +653,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
if (!InitializeMutexObject(ctx, handle, state))
|
if (!InitializeMutexObject(ctx, handle, state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -641,6 +666,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexStates.TryRemove(mutexAddress, out _);
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
_mutexStates.TryRemove(handle, out _);
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -668,6 +694,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -724,7 +751,6 @@ public static class KernelPthreadCompatExports
|
|||||||
if (!acquired)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
||||||
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
|
||||||
// Fibers retain the synchronous fallback to preserve switch state.
|
// Fibers retain the synchronous fallback to preserve switch state.
|
||||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
||||||
@@ -736,8 +762,14 @@ public static class KernelPthreadCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"pthread_mutex_lock",
|
"pthread_mutex_lock",
|
||||||
state.WakeKey,
|
state.WakeKey,
|
||||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
new PthreadMutexWaiter
|
||||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
{
|
||||||
|
ThreadId = currentThreadId,
|
||||||
|
Ctx = ctx,
|
||||||
|
MutexAddress = mutexAddress,
|
||||||
|
ResolvedAddress = resolvedAddress,
|
||||||
|
State = state,
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (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);
|
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||||
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,6 +945,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexAttrStates.Remove(handle);
|
_mutexAttrStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1195,6 +1230,7 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (_condStates.TryGetValue(condAddress, out var raced))
|
if (_condStates.TryGetValue(condAddress, out var raced))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = condAddress;
|
resolvedAddress = condAddress;
|
||||||
state = raced;
|
state = raced;
|
||||||
return true;
|
return true;
|
||||||
@@ -1212,6 +1248,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_condStates.Remove(handle);
|
_condStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,7 +1268,22 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
Span<byte> initialData = stackalloc byte[size];
|
Span<byte> initialData = stackalloc byte[size];
|
||||||
initialData.Clear();
|
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) =>
|
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||||
@@ -1269,6 +1321,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_condStates.Remove(handle);
|
_condStates.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,6 +1346,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
||||||
|
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1363,8 +1417,17 @@ public static class KernelPthreadCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||||
state.WakeKey,
|
state.WakeKey,
|
||||||
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
|
new PthreadCondWaiter
|
||||||
() => state.SignalEpoch != observedEpoch,
|
{
|
||||||
|
Ctx = ctx,
|
||||||
|
CondAddress = condAddress,
|
||||||
|
MutexAddress = mutexAddress,
|
||||||
|
State = state,
|
||||||
|
ObservedEpoch = observedEpoch,
|
||||||
|
Timed = timed,
|
||||||
|
ReleasedRecursion = releasedRecursion,
|
||||||
|
PosixResult = posixResult,
|
||||||
|
},
|
||||||
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||||
{
|
{
|
||||||
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
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))
|
if (!InitializeMutexObject(ctx, handle, createdState))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = 0;
|
resolvedAddress = 0;
|
||||||
state = null;
|
state = null;
|
||||||
return false;
|
return false;
|
||||||
@@ -1784,12 +1848,14 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = mutexAddress;
|
resolvedAddress = mutexAddress;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_mutexStates.TryGetValue(handle, out state))
|
if (_mutexStates.TryGetValue(handle, out state))
|
||||||
{
|
{
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = handle;
|
resolvedAddress = handle;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1803,6 +1869,7 @@ public static class KernelPthreadCompatExports
|
|||||||
_mutexStates.TryRemove(mutexAddress, out _);
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
_mutexStates.TryRemove(handle, out _);
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
TryFreeOpaqueObject(ctx, handle);
|
||||||
resolvedAddress = 0;
|
resolvedAddress = 0;
|
||||||
state = null;
|
state = null;
|
||||||
return false;
|
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 TlsKeyState(ulong Destructor);
|
||||||
|
|
||||||
private readonly record struct PthreadAttrState(
|
private readonly record struct PthreadAttrState(
|
||||||
@@ -1359,8 +1370,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"pthread_rwlock_wrlock",
|
"pthread_rwlock_wrlock",
|
||||||
rwlock.WakeKey,
|
rwlock.WakeKey,
|
||||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
|
||||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
|
|
||||||
{
|
{
|
||||||
transferredToScheduler = true;
|
transferredToScheduler = true;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -1396,8 +1406,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
ctx,
|
ctx,
|
||||||
"pthread_rwlock_rdlock",
|
"pthread_rwlock_rdlock",
|
||||||
rwlock.WakeKey,
|
rwlock.WakeKey,
|
||||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
|
||||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public static class KernelSemaphoreCompatExports
|
|||||||
private sealed class KernelSemaphoreState
|
private sealed class KernelSemaphoreState
|
||||||
{
|
{
|
||||||
public required string Name { get; init; }
|
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 InitialCount { get; init; }
|
||||||
public required int MaxCount { get; init; }
|
public required int MaxCount { get; init; }
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
@@ -28,14 +30,26 @@ public static class KernelSemaphoreCompatExports
|
|||||||
public object Gate { get; } = new();
|
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 NeedCount { get; init; }
|
||||||
public required int CancelEpochAtBlock { get; init; }
|
public required int CancelEpochAtBlock { get; init; }
|
||||||
public bool Timed { 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.
|
// Written and read only under the owning semaphore's Gate.
|
||||||
public int? Result { get; set; }
|
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}";
|
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
||||||
@@ -79,6 +93,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
var state = new KernelSemaphoreState
|
var state = new KernelSemaphoreState
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
|
WakeKey = GetSemaphoreWakeKey(handle),
|
||||||
InitialCount = initialCount,
|
InitialCount = initialCount,
|
||||||
MaxCount = maxCount,
|
MaxCount = maxCount,
|
||||||
Count = initialCount,
|
Count = initialCount,
|
||||||
@@ -96,11 +111,14 @@ public static class KernelSemaphoreCompatExports
|
|||||||
state.Deleted = true;
|
state.Deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +149,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
if (semaphore.Count >= needCount)
|
if (semaphore.Count >= needCount)
|
||||||
{
|
{
|
||||||
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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +166,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
if (timeoutMicros == 0)
|
if (timeoutMicros == 0)
|
||||||
{
|
{
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 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;
|
pollTimedOut = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -153,27 +177,36 @@ public static class KernelSemaphoreCompatExports
|
|||||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
||||||
var timedWaiter = new SemaphoreWaiter
|
var timedWaiter = new SemaphoreWaiter
|
||||||
{
|
{
|
||||||
|
Semaphore = semaphore,
|
||||||
NeedCount = needCount,
|
NeedCount = needCount,
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||||
Timed = true,
|
Timed = true,
|
||||||
|
Ctx = ctx,
|
||||||
|
TimeoutAddress = timeoutAddress,
|
||||||
|
DeadlineTimestamp = deadline,
|
||||||
};
|
};
|
||||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitSema",
|
"sceKernelWaitSema",
|
||||||
GetSemaphoreWakeKey(handle),
|
semaphore.WakeKey,
|
||||||
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
|
timedWaiter,
|
||||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
|
|
||||||
blockDeadlineTimestamp: deadline))
|
blockDeadlineTimestamp: deadline))
|
||||||
{
|
{
|
||||||
semaphore.WaitingThreads++;
|
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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
||||||
// immediate-timeout poll the callers already tolerate.
|
// immediate-timeout poll the callers already tolerate.
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 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;
|
pollTimedOut = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,22 +215,28 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
var waiter = new SemaphoreWaiter
|
var waiter = new SemaphoreWaiter
|
||||||
{
|
{
|
||||||
|
Semaphore = semaphore,
|
||||||
NeedCount = needCount,
|
NeedCount = needCount,
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||||
};
|
};
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"sceKernelWaitSema",
|
"sceKernelWaitSema",
|
||||||
GetSemaphoreWakeKey(handle),
|
semaphore.WakeKey,
|
||||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
waiter))
|
||||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, 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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.WaitingThreads++;
|
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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,12 +278,18 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
if (semaphore.Count < needCount)
|
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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count -= needCount;
|
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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,14 +322,17 @@ public static class KernelSemaphoreCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count += signalCount;
|
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 after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
||||||
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
||||||
// whose needCount exceeds the remaining count stays parked while a smaller
|
// whose needCount exceeds the remaining count stays parked while a smaller
|
||||||
// waiter can proceed.
|
// waiter can proceed.
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
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
|
// 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
|
// and silently absorb the increment of a waiter that parks between this
|
||||||
// gate release and the wake-all below.
|
// 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);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +400,11 @@ public static class KernelSemaphoreCompatExports
|
|||||||
semaphore.Deleted = true;
|
semaphore.Deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
if (_traceSema)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||||
|
}
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +430,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +441,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +453,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
semaphore.Count -= waiter.NeedCount;
|
semaphore.Count -= waiter.NeedCount;
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +497,10 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
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;
|
result = waiter.Result!.Value;
|
||||||
@@ -456,11 +522,13 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return result;
|
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)
|
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}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
@@ -38,8 +38,7 @@ public static class PadExports
|
|||||||
public static int PadInit(CpuContext ctx)
|
public static int PadInit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
DualSenseReader.EnsureStarted();
|
HostPlatform.Current.Input.EnsureStarted();
|
||||||
XInputReader.EnsureStarted();
|
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,15 +80,13 @@ public static class PadExports
|
|||||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
var input = HostPlatform.Current.Input;
|
||||||
XInputReader.EnsureStarted();
|
input.EnsureStarted();
|
||||||
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
|
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
Console.Error.WriteLine(input.DescribeConnectedGamepad() is { } gamepadName
|
||||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
? $"[LOADER][INFO] Controls: {gamepadName} connected (keyboard fallback also active)."
|
||||||
: XInputReader.TryGetState(out _)
|
: "[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.");
|
||||||
? "[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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(PrimaryPadHandle);
|
return ctx.SetReturn(PrimaryPadHandle);
|
||||||
@@ -282,7 +279,7 @@ public static class PadExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var triggerMask = parameter[0];
|
var triggerMask = parameter[0];
|
||||||
XInputReader.SetTriggerRumble(
|
HostPlatform.Current.Input.SetTriggerRumble(
|
||||||
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
|
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
|
||||||
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
|
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
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);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.SetRumble(parameter[0], parameter[1]);
|
HostPlatform.Current.Input.SetRumble(parameter[0], parameter[1]);
|
||||||
XInputReader.SetRumble(parameter[0], parameter[1]);
|
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +353,7 @@ public static class PadExports
|
|||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
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);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +370,7 @@ public static class PadExports
|
|||||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.ResetLightbar();
|
HostPlatform.Current.Input.ResetLightbar();
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,37 +416,30 @@ public static class PadExports
|
|||||||
return _cachedInputState;
|
return _cachedInputState;
|
||||||
}
|
}
|
||||||
|
|
||||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
var input = HostPlatform.Current.Input;
|
||||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
var acceptsKeyboardInput = input.IsHostWindowFocused();
|
||||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons(input) : 0;
|
||||||
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
var leftX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x41), input.IsKeyDown(0x44)) : (byte)128;
|
||||||
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
var leftY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x57), input.IsKeyDown(0x53)) : (byte)128;
|
||||||
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
var rightX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x4A), input.IsKeyDown(0x4C)) : (byte)128;
|
||||||
var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
var rightY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x49), input.IsKeyDown(0x4B)) : (byte)128;
|
||||||
var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
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
|
// The controller stick wins whenever it is deflected past a
|
||||||
// small deadzone; otherwise any keyboard value stays.
|
// small deadzone; otherwise any keyboard value stays.
|
||||||
leftX = MergeAxis(pad.LeftX, leftX);
|
leftX = MergeAxis(pad.LeftX, leftX);
|
||||||
leftY = MergeAxis(pad.LeftY, leftY);
|
leftY = MergeAxis(pad.LeftY, leftY);
|
||||||
rightX = MergeAxis(pad.RightX, rightX);
|
rightX = MergeAxis(pad.RightX, rightX);
|
||||||
rightY = MergeAxis(pad.RightY, rightY);
|
rightY = MergeAxis(pad.RightY, rightY);
|
||||||
l2 = Math.Max(l2, pad.L2);
|
l2 = Math.Max(l2, pad.LeftTrigger);
|
||||||
r2 = Math.Max(r2, pad.R2);
|
r2 = Math.Max(r2, pad.RightTrigger);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedInputState = new PadState(
|
_cachedInputState = new PadState(
|
||||||
@@ -466,50 +455,49 @@ public static class PadExports
|
|||||||
return _cachedInputState;
|
return _cachedInputState;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
/// <summary>Maps the host seam's neutral button flags onto SCE_PAD_BUTTON bits.</summary>
|
||||||
private static extern short GetAsyncKeyState(int vKey);
|
private static uint ToOrbisButtons(HostGamepadButtons buttons)
|
||||||
|
|
||||||
[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()
|
|
||||||
{
|
{
|
||||||
var foregroundWindow = GetForegroundWindow();
|
uint result = 0;
|
||||||
if (foregroundWindow == 0)
|
if ((buttons & HostGamepadButtons.Up) != 0) result |= OrbisPadButton.Up;
|
||||||
{
|
if ((buttons & HostGamepadButtons.Down) != 0) result |= OrbisPadButton.Down;
|
||||||
return false;
|
if ((buttons & HostGamepadButtons.Left) != 0) result |= OrbisPadButton.Left;
|
||||||
}
|
if ((buttons & HostGamepadButtons.Right) != 0) result |= OrbisPadButton.Right;
|
||||||
|
if ((buttons & HostGamepadButtons.Cross) != 0) result |= OrbisPadButton.Cross;
|
||||||
GetWindowThreadProcessId(foregroundWindow, out var processId);
|
if ((buttons & HostGamepadButtons.Circle) != 0) result |= OrbisPadButton.Circle;
|
||||||
return processId == (uint)Environment.ProcessId;
|
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;
|
uint buttons = 0;
|
||||||
// D-pad
|
// D-pad
|
||||||
if (IsKeyDown(0x25)) buttons |= 0x0080; // Left
|
if (input.IsKeyDown(0x25)) buttons |= OrbisPadButton.Left;
|
||||||
if (IsKeyDown(0x27)) buttons |= 0x0020; // Right
|
if (input.IsKeyDown(0x27)) buttons |= OrbisPadButton.Right;
|
||||||
if (IsKeyDown(0x26)) buttons |= 0x0010; // Up
|
if (input.IsKeyDown(0x26)) buttons |= OrbisPadButton.Up;
|
||||||
if (IsKeyDown(0x28)) buttons |= 0x0040; // Down
|
if (input.IsKeyDown(0x28)) buttons |= OrbisPadButton.Down;
|
||||||
// Face buttons
|
// Face buttons
|
||||||
if (IsKeyDown(0x5A) || IsKeyDown(0x0D)) buttons |= 0x4000; // Z / Enter = Cross
|
if (input.IsKeyDown(0x5A) || input.IsKeyDown(0x0D)) buttons |= OrbisPadButton.Cross; // Z / Enter
|
||||||
if (IsKeyDown(0x58) || IsKeyDown(0x1B)) buttons |= 0x2000; // X / Escape = Circle
|
if (input.IsKeyDown(0x58) || input.IsKeyDown(0x1B)) buttons |= OrbisPadButton.Circle; // X / Escape
|
||||||
if (IsKeyDown(0x43)) buttons |= 0x8000; // C = Square
|
if (input.IsKeyDown(0x43)) buttons |= OrbisPadButton.Square; // C
|
||||||
if (IsKeyDown(0x56)) buttons |= 0x1000; // V = Triangle
|
if (input.IsKeyDown(0x56)) buttons |= OrbisPadButton.Triangle; // V
|
||||||
// Shoulder buttons
|
// Shoulder buttons
|
||||||
if (IsKeyDown(0x51)) buttons |= 0x0400; // Q = L1
|
if (input.IsKeyDown(0x51)) buttons |= OrbisPadButton.L1; // Q
|
||||||
if (IsKeyDown(0x45)) buttons |= 0x0800; // E = R1
|
if (input.IsKeyDown(0x45)) buttons |= OrbisPadButton.R1; // E
|
||||||
if (IsKeyDown(0x52)) buttons |= 0x0100; // R = L2 (digital)
|
if (input.IsKeyDown(0x52)) buttons |= OrbisPadButton.L2; // R (digital)
|
||||||
if (IsKeyDown(0x46)) buttons |= 0x0200; // F = R2 (digital)
|
if (input.IsKeyDown(0x46)) buttons |= OrbisPadButton.R2; // F (digital)
|
||||||
// Options (Start)
|
// 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;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.HLE.Host;
|
||||||
using SharpEmu.Libs.Audio;
|
using SharpEmu.Libs.Audio;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@@ -67,7 +69,7 @@ public static class VideoOutExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HostTimerResolution.Request();
|
HostPlatform.Current.Threading.RequestTimerResolution();
|
||||||
|
|
||||||
_vblankPumpThread = new Thread(VblankPumpLoop)
|
_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()
|
private static void PumpVblanks()
|
||||||
{
|
{
|
||||||
lock (_vblankEdgeGate)
|
lock (_vblankEdgeGate)
|
||||||
@@ -188,7 +194,7 @@ public static class VideoOutExports
|
|||||||
Monitor.PulseAll(_vblankEdgeGate);
|
Monitor.PulseAll(_vblankEdgeGate);
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoOutPortState[] ports;
|
_vblankPumpPorts.Clear();
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
if (_ports.Count == 0)
|
if (_ports.Count == 0)
|
||||||
@@ -198,10 +204,16 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
||||||
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
// 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);
|
SignalVblank(port);
|
||||||
}
|
}
|
||||||
@@ -221,6 +233,12 @@ public static class VideoOutExports
|
|||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
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(
|
private static readonly bool _dumpVideoOut = string.Equals(
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
||||||
"1",
|
"1",
|
||||||
@@ -586,7 +604,10 @@ public static class VideoOutExports
|
|||||||
// Some engines wait on this queue before issuing their first flip. Provide a first
|
// 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.
|
// edge now; later calls to WaitVblank advance the same notification sequence.
|
||||||
SignalVblank(port);
|
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;
|
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;
|
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 + 0x18, unchecked((ulong)flipArg));
|
||||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1077,33 +1104,53 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
private static void SignalVblank(VideoOutPortState port)
|
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;
|
ulong eventHint;
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
port.VblankCount++;
|
port.VblankCount++;
|
||||||
eventHint = SceVideoOutInternalEventVblank |
|
eventHint = SceVideoOutInternalEventVblank |
|
||||||
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
((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);
|
var signalCount = Interlocked.Increment(ref _vblankSignalCount);
|
||||||
|
|
||||||
foreach (var vblankEvent in vblankEvents)
|
if (vblankEvents is not null)
|
||||||
{
|
{
|
||||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
try
|
||||||
vblankEvent.Equeue,
|
{
|
||||||
SceVideoOutInternalEventVblank,
|
for (var i = 0; i < vblankEventCount; i++)
|
||||||
OrbisKernelEventFilterVideoOut,
|
{
|
||||||
eventHint,
|
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||||
vblankEvent.UserData);
|
vblankEvents[i].Equeue,
|
||||||
|
SceVideoOutInternalEventVblank,
|
||||||
|
OrbisKernelEventFilterVideoOut,
|
||||||
|
eventHint,
|
||||||
|
vblankEvents[i].UserData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<FlipEventRegistration>.Shared.Return(vblankEvents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
|
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
|
$"[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;
|
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;
|
ulong eventHint;
|
||||||
List<FlipEventRegistration> flipEvents;
|
FlipEventRegistration[]? flipEvents = null;
|
||||||
|
int flipEventCount;
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
|
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
|
||||||
@@ -1138,7 +1188,12 @@ public static class VideoOutExports
|
|||||||
port.FlipCount++;
|
port.FlipCount++;
|
||||||
eventHint = SceVideoOutInternalEventFlip |
|
eventHint = SceVideoOutInternalEventFlip |
|
||||||
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
((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;
|
var guestImageSubmitted = false;
|
||||||
@@ -1160,14 +1215,24 @@ public static class VideoOutExports
|
|||||||
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var flipEvent in flipEvents)
|
if (flipEvents is not null)
|
||||||
{
|
{
|
||||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
try
|
||||||
flipEvent.Equeue,
|
{
|
||||||
SceVideoOutInternalEventFlip,
|
for (var i = 0; i < flipEventCount; i++)
|
||||||
OrbisKernelEventFilterVideoOut,
|
{
|
||||||
eventHint,
|
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||||
flipEvent.UserData);
|
flipEvents[i].Equeue,
|
||||||
|
SceVideoOutInternalEventFlip,
|
||||||
|
OrbisKernelEventFilterVideoOut,
|
||||||
|
eventHint,
|
||||||
|
flipEvents[i].UserData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<FlipEventRegistration>.Shared.Return(flipEvents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
|
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
|
||||||
@@ -1176,10 +1241,13 @@ public static class VideoOutExports
|
|||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
|
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
|
||||||
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
|
$"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);
|
ReportFrameRate(presented: false);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -1261,8 +1329,11 @@ public static class VideoOutExports
|
|||||||
slot.AddressRight = 0;
|
slot.AddressRight = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceVideoOut(
|
if (_logVideoOut)
|
||||||
$"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}");
|
{
|
||||||
|
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);
|
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
||||||
|
|
||||||
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
|
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
|
||||||
@@ -1428,7 +1499,10 @@ public static class VideoOutExports
|
|||||||
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
|
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
|
||||||
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
|
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
|
||||||
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1467,7 +1541,10 @@ public static class VideoOutExports
|
|||||||
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
|
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
|
||||||
File.WriteAllBytes(basePath + ".raw", bytes);
|
File.WriteAllBytes(basePath + ".raw", bytes);
|
||||||
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1694,11 +1771,6 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
private static void TraceVideoOut(string message)
|
private static void TraceVideoOut(string message)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
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>
|
<PropertyGroup>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||||
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -149,6 +149,15 @@
|
|||||||
"xunit.extensibility.core": "[2.9.3]"
|
"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": {
|
"sharpemu.hle": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -168,6 +177,12 @@
|
|||||||
"sharpemu.logging": {
|
"sharpemu.logging": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
},
|
},
|
||||||
|
"Iced": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[1.21.0, )",
|
||||||
|
"resolved": "1.21.0",
|
||||||
|
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||||
|
},
|
||||||
"Silk.NET.Vulkan": {
|
"Silk.NET.Vulkan": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
|
|||||||
Reference in New Issue
Block a user