mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ad9836d13 | |||
| 62e1775c5c | |||
| 6dacd59a08 | |||
| 9d88542efd | |||
| 373100a6b0 |
@@ -535,8 +535,7 @@ public sealed partial class DirectExecutionBackend
|
||||
out var blockContinuation,
|
||||
out var hasBlockContinuation,
|
||||
out var blockWakeKey,
|
||||
out var blockResumeHandler,
|
||||
out var blockWakeHandler,
|
||||
out var blockWaiter,
|
||||
out var blockDeadlineTimestamp) &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
||||
{
|
||||
@@ -546,8 +545,7 @@ public sealed partial class DirectExecutionBackend
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
blockContinuation,
|
||||
blockWakeKey,
|
||||
blockResumeHandler,
|
||||
blockWakeHandler,
|
||||
blockWaiter,
|
||||
blockDeadlineTimestamp);
|
||||
}
|
||||
|
||||
@@ -678,8 +676,7 @@ public sealed partial class DirectExecutionBackend
|
||||
out var blockContinuation,
|
||||
out var hasBlockContinuation,
|
||||
out var blockWakeKey,
|
||||
out var blockResumeHandler,
|
||||
out var blockWakeHandler,
|
||||
out var blockWaiter,
|
||||
out var blockDeadlineTimestamp) &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
||||
{
|
||||
@@ -689,8 +686,7 @@ public sealed partial class DirectExecutionBackend
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
blockContinuation,
|
||||
blockWakeKey,
|
||||
blockResumeHandler,
|
||||
blockWakeHandler,
|
||||
blockWaiter,
|
||||
blockDeadlineTimestamp);
|
||||
}
|
||||
|
||||
@@ -1970,23 +1966,36 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
||||
Span<byte> destination = stackalloc byte[1];
|
||||
for (int i = 0; i < maxLength; i++)
|
||||
// Reads stay byte-by-byte through TryReadByteCompat (its Marshal.ReadByte
|
||||
// fallback must probe exactly up to the terminator), but the bytes land in a
|
||||
// stack buffer instead of a List<byte> + ToArray per symbol resolution.
|
||||
const int StackBufferLength = 512;
|
||||
byte[]? rented = maxLength > StackBufferLength ? System.Buffers.ArrayPool<byte>.Shared.Rent(maxLength) : null;
|
||||
Span<byte> buffer = rented is null ? stackalloc byte[StackBufferLength] : rented;
|
||||
try
|
||||
{
|
||||
if (!TryReadByteCompat(address + (ulong)i, destination))
|
||||
for (int i = 0; i < maxLength; i++)
|
||||
{
|
||||
return false;
|
||||
if (!TryReadByteCompat(address + (ulong)i, buffer.Slice(i, 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (buffer[i] == 0)
|
||||
{
|
||||
value = System.Text.Encoding.ASCII.GetString(buffer[..i]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
value = System.Text.Encoding.ASCII.GetString(buffer[..maxLength]);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rented is not null)
|
||||
{
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(rented);
|
||||
}
|
||||
if (destination[0] == 0)
|
||||
{
|
||||
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
||||
return true;
|
||||
}
|
||||
list.Add(destination[0]);
|
||||
}
|
||||
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryReadByteCompat(ulong address, Span<byte> destination)
|
||||
|
||||
@@ -434,9 +434,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public string? BlockWakeKey { get; set; }
|
||||
|
||||
public Func<int>? BlockResumeHandler { get; set; }
|
||||
|
||||
public Func<bool>? BlockWakeHandler { get; set; }
|
||||
// Stays set through the wake transition; Resume() consumes it when the thread pumps.
|
||||
public IGuestThreadBlockWaiter? BlockWaiter { get; set; }
|
||||
|
||||
public long BlockDeadlineTimestamp { get; set; }
|
||||
|
||||
@@ -2797,14 +2796,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
continue;
|
||||
}
|
||||
|
||||
if (thread.BlockWakeHandler is not null && !thread.BlockWakeHandler())
|
||||
if (thread.BlockWaiter is not null && !thread.BlockWaiter.TryWake())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
thread.State = GuestThreadRunState.Ready;
|
||||
thread.BlockReason = null;
|
||||
thread.BlockWakeHandler = null;
|
||||
thread.BlockDeadlineTimestamp = 0;
|
||||
_readyGuestThreads.Enqueue(thread);
|
||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||
@@ -2855,8 +2853,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ulong guestThreadHandle,
|
||||
GuestCpuContinuation continuation,
|
||||
string wakeKey,
|
||||
Func<int>? resumeHandler,
|
||||
Func<bool>? wakeHandler,
|
||||
IGuestThreadBlockWaiter? waiter,
|
||||
long blockDeadlineTimestamp)
|
||||
{
|
||||
if (guestThreadHandle == 0 || continuation.Rip < 65536 || continuation.Rsp == 0)
|
||||
@@ -2874,8 +2871,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
thread.BlockedContinuation = continuation;
|
||||
thread.HasBlockedContinuation = true;
|
||||
thread.BlockWakeKey = wakeKey;
|
||||
thread.BlockResumeHandler = resumeHandler;
|
||||
thread.BlockWakeHandler = wakeHandler;
|
||||
thread.BlockWaiter = waiter;
|
||||
thread.BlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||
}
|
||||
}
|
||||
@@ -2898,7 +2894,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
thread.State = GuestThreadRunState.Ready;
|
||||
thread.BlockReason = null;
|
||||
thread.BlockWakeHandler = null;
|
||||
thread.BlockDeadlineTimestamp = 0;
|
||||
_readyGuestThreads.Enqueue(thread);
|
||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||
@@ -3575,7 +3570,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
LastError = null;
|
||||
GuestCpuContinuation continuation = default;
|
||||
Func<int>? resumeHandler = null;
|
||||
IGuestThreadBlockWaiter? blockWaiter = null;
|
||||
var resumeContinuation = false;
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
@@ -3585,17 +3580,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
thread.BlockedContinuation = default;
|
||||
thread.HasBlockedContinuation = false;
|
||||
thread.BlockWakeKey = null;
|
||||
resumeHandler = thread.BlockResumeHandler;
|
||||
thread.BlockResumeHandler = null;
|
||||
thread.BlockWakeHandler = null;
|
||||
blockWaiter = thread.BlockWaiter;
|
||||
thread.BlockWaiter = null;
|
||||
thread.BlockDeadlineTimestamp = 0;
|
||||
resumeContinuation = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (resumeHandler is not null)
|
||||
if (blockWaiter is not null)
|
||||
{
|
||||
continuation = continuation with { Rax = unchecked((ulong)(long)resumeHandler()) };
|
||||
continuation = continuation with { Rax = unchecked((ulong)(long)blockWaiter.Resume()) };
|
||||
}
|
||||
|
||||
if (_logGuestThreads)
|
||||
@@ -3620,12 +3614,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
thread.State = GuestThreadRunState.Blocked;
|
||||
thread.BlockReason = blockReason;
|
||||
if (thread.HasBlockedContinuation &&
|
||||
thread.BlockWakeHandler is not null &&
|
||||
thread.BlockWakeHandler())
|
||||
thread.BlockWaiter is not null &&
|
||||
thread.BlockWaiter.TryWake())
|
||||
{
|
||||
thread.State = GuestThreadRunState.Ready;
|
||||
thread.BlockReason = null;
|
||||
thread.BlockWakeHandler = null;
|
||||
thread.BlockDeadlineTimestamp = 0;
|
||||
_readyGuestThreads.Enqueue(thread);
|
||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||
|
||||
@@ -50,4 +50,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
|
||||
address = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryFreeGuestMemory(ulong address)
|
||||
{
|
||||
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private readonly IHostMemory _hostMemory;
|
||||
private ulong _guestAllocationArenaBase;
|
||||
private ulong _guestAllocationOffset;
|
||||
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
|
||||
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
|
||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||
|
||||
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
|
||||
@@ -301,7 +302,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
GuestAllocationArenaSize,
|
||||
executable: false,
|
||||
allowAlternative: true);
|
||||
_guestAllocationOffset = GuestAllocationArenaStartOffset;
|
||||
_guestAllocationFreeRanges.Add(
|
||||
GuestAllocationArenaStartOffset,
|
||||
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -309,14 +312,89 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
|
||||
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
|
||||
ulong rangeOffset = 0;
|
||||
ulong rangeSize = 0;
|
||||
ulong alignedOffset = 0;
|
||||
var found = false;
|
||||
foreach (var range in _guestAllocationFreeRanges)
|
||||
{
|
||||
alignedOffset = AlignUp(range.Key, alignment);
|
||||
if (alignedOffset >= range.Key &&
|
||||
alignedOffset - range.Key <= range.Value &&
|
||||
size <= range.Value - (alignedOffset - range.Key))
|
||||
{
|
||||
rangeOffset = range.Key;
|
||||
rangeSize = range.Value;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_guestAllocationFreeRanges.Remove(rangeOffset);
|
||||
if (alignedOffset > rangeOffset)
|
||||
{
|
||||
_guestAllocationFreeRanges.Add(rangeOffset, alignedOffset - rangeOffset);
|
||||
}
|
||||
|
||||
var allocationEnd = alignedOffset + size;
|
||||
var rangeEnd = rangeOffset + rangeSize;
|
||||
if (allocationEnd < rangeEnd)
|
||||
{
|
||||
_guestAllocationFreeRanges.Add(allocationEnd, rangeEnd - allocationEnd);
|
||||
}
|
||||
|
||||
address = _guestAllocationArenaBase + alignedOffset;
|
||||
_guestAllocationOffset = alignedOffset + size;
|
||||
_guestAllocations.Add(address, (alignedOffset, size));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryFreeGuestMemory(ulong address)
|
||||
{
|
||||
lock (_guestAllocationGate)
|
||||
{
|
||||
if (!_guestAllocations.Remove(address, out var allocation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var freeOffset = allocation.Offset;
|
||||
var freeSize = allocation.Size;
|
||||
ulong? previousOffset = null;
|
||||
ulong? nextOffset = null;
|
||||
|
||||
foreach (var range in _guestAllocationFreeRanges)
|
||||
{
|
||||
if (range.Key < freeOffset)
|
||||
{
|
||||
previousOffset = range.Key;
|
||||
continue;
|
||||
}
|
||||
|
||||
nextOffset = range.Key;
|
||||
break;
|
||||
}
|
||||
|
||||
if (previousOffset is { } previous &&
|
||||
previous + _guestAllocationFreeRanges[previous] == freeOffset)
|
||||
{
|
||||
freeOffset = previous;
|
||||
freeSize += _guestAllocationFreeRanges[previous];
|
||||
_guestAllocationFreeRanges.Remove(previous);
|
||||
}
|
||||
|
||||
if (nextOffset is { } next && freeOffset + freeSize == next)
|
||||
{
|
||||
freeSize += _guestAllocationFreeRanges[next];
|
||||
_guestAllocationFreeRanges.Remove(next);
|
||||
}
|
||||
|
||||
_guestAllocationFreeRanges.Add(freeOffset, freeSize);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -380,7 +458,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
|
||||
_guestAllocationArenaBase = 0;
|
||||
_guestAllocationOffset = 0;
|
||||
_guestAllocationFreeRanges.Clear();
|
||||
_guestAllocations.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,12 +518,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
||||
{
|
||||
var runStart = mapStart;
|
||||
var runFlags = ProgramHeaderFlags.None;
|
||||
var hasRun = false;
|
||||
|
||||
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
|
||||
{
|
||||
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
|
||||
var mergedFlags = existingFlags | flags;
|
||||
_pageProtections[pageAddress] = mergedFlags;
|
||||
SetProtection(pageAddress, PageSize, mergedFlags);
|
||||
|
||||
if (!hasRun)
|
||||
{
|
||||
runStart = pageAddress;
|
||||
runFlags = mergedFlags;
|
||||
hasRun = true;
|
||||
}
|
||||
else if (mergedFlags != runFlags)
|
||||
{
|
||||
SetProtection(runStart, pageAddress - runStart, runFlags);
|
||||
runStart = pageAddress;
|
||||
runFlags = mergedFlags;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRun)
|
||||
{
|
||||
SetProtection(runStart, mapEnd - runStart, runFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,9 +889,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return FindRegion(virtualAddress, 1) is not null
|
||||
? (void*)virtualAddress
|
||||
: null;
|
||||
var region = FindRegion(virtualAddress, 1);
|
||||
if (region is null ||
|
||||
(region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (void*)virtualAddress;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -41,15 +41,14 @@ public sealed class VirtualMemory : IVirtualMemory
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var existing in _regions)
|
||||
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||
if ((insertionIndex > 0 && virtualAddress < _regions[insertionIndex - 1].EndAddress) ||
|
||||
(insertionIndex < _regions.Count && endAddress > _regions[insertionIndex].Region.VirtualAddress))
|
||||
{
|
||||
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
|
||||
{
|
||||
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
||||
}
|
||||
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
|
||||
}
|
||||
|
||||
_regions.Add(new MappedRegion(
|
||||
_regions.Insert(insertionIndex, new MappedRegion(
|
||||
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
|
||||
endAddress,
|
||||
backingMemory));
|
||||
@@ -74,12 +73,12 @@ public sealed class VirtualMemory : IVirtualMemory
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
|
||||
if (!TryValidateRange(virtualAddress, destination.Length, ProgramHeaderFlags.Read, out var regionIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||
CopyFromRegions(virtualAddress, destination, regionIndex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -88,39 +87,127 @@ public sealed class VirtualMemory : IVirtualMemory
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
|
||||
if (!TryValidateRange(virtualAddress, source.Length, ProgramHeaderFlags.Write, out var regionIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
|
||||
CopyToRegions(virtualAddress, source, regionIndex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
|
||||
private bool TryValidateRange(
|
||||
ulong virtualAddress,
|
||||
int length,
|
||||
ProgramHeaderFlags requiredProtection,
|
||||
out int regionIndex)
|
||||
{
|
||||
foreach (var candidate in _regions)
|
||||
regionIndex = FindContainingRegionIndex(virtualAddress);
|
||||
if (regionIndex < 0)
|
||||
{
|
||||
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
|
||||
if (candidateOffset + length > candidate.BackingMemory.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
region = candidate;
|
||||
offset = candidateOffset;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
region = default;
|
||||
offset = 0;
|
||||
return false;
|
||||
var currentAddress = virtualAddress;
|
||||
var remaining = length;
|
||||
var currentIndex = regionIndex;
|
||||
while (true)
|
||||
{
|
||||
if (currentIndex >= _regions.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var region = _regions[currentIndex];
|
||||
if (currentAddress < region.Region.VirtualAddress ||
|
||||
currentAddress >= region.EndAddress ||
|
||||
(region.Region.Protection & requiredProtection) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remaining == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var available = region.EndAddress - currentAddress;
|
||||
var chunkLength = (int)Math.Min((ulong)remaining, available);
|
||||
remaining -= chunkLength;
|
||||
if (remaining == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
currentAddress += (ulong)chunkLength;
|
||||
currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
private int FindContainingRegionIndex(ulong virtualAddress)
|
||||
{
|
||||
var insertionIndex = FindInsertionIndex(virtualAddress);
|
||||
if (insertionIndex < _regions.Count &&
|
||||
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
|
||||
{
|
||||
return insertionIndex;
|
||||
}
|
||||
|
||||
var candidateIndex = insertionIndex - 1;
|
||||
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
|
||||
? candidateIndex
|
||||
: -1;
|
||||
}
|
||||
|
||||
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
|
||||
{
|
||||
var copied = 0;
|
||||
var currentAddress = virtualAddress;
|
||||
while (copied < destination.Length)
|
||||
{
|
||||
var region = _regions[regionIndex++];
|
||||
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
|
||||
copied += chunkLength;
|
||||
currentAddress += (ulong)chunkLength;
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
|
||||
{
|
||||
var copied = 0;
|
||||
var currentAddress = virtualAddress;
|
||||
while (copied < source.Length)
|
||||
{
|
||||
var region = _regions[regionIndex++];
|
||||
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
|
||||
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
|
||||
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
|
||||
copied += chunkLength;
|
||||
currentAddress += (ulong)chunkLength;
|
||||
}
|
||||
}
|
||||
|
||||
private int FindInsertionIndex(ulong virtualAddress)
|
||||
{
|
||||
var lower = 0;
|
||||
var upper = _regions.Count;
|
||||
while (lower < upper)
|
||||
{
|
||||
var middle = lower + ((upper - lower) / 2);
|
||||
if (_regions[middle].Region.VirtualAddress < virtualAddress)
|
||||
{
|
||||
lower = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
upper = middle;
|
||||
}
|
||||
}
|
||||
|
||||
return lower;
|
||||
}
|
||||
|
||||
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
|
||||
|
||||
@@ -48,6 +48,9 @@ public sealed class GuiSettings
|
||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||
public bool DiscordRichPresence { get; set; } = true;
|
||||
|
||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||
public List<string> EnvironmentToggles { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Discord application ID used for Rich Presence; the default is the
|
||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
"Library.Loading": "Carregando biblioteca…",
|
||||
|
||||
"Options.General": "Opções Gerais",
|
||||
"Options.Env.Tab": "Ambiente",
|
||||
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
|
||||
"Options.Env.Desc": "Switches passados para o emulador como variáveis de ambiente na inicialização.",
|
||||
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica esperando indefinidamente.\nDeixe desativado normalmente. Alguns títulos travam quando a inicialização falha.",
|
||||
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada por tempo demais.\nExperimente isso quando um jogo fecha sozinho durante o carregamento.",
|
||||
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração de GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
|
||||
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
|
||||
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
|
||||
"Options.Section.Emulation": "EMULAÇÃO",
|
||||
"Options.Section.Logging": "LOGS",
|
||||
"Options.Section.Launcher": "INICIALIZADOR",
|
||||
@@ -126,4 +135,4 @@
|
||||
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
||||
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
||||
"Dialog.LogFiles": "Arquivos de log"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
"Library.Loading": "Loading library…",
|
||||
|
||||
"Options.General": "General",
|
||||
"Options.Env.Tab": "Environment",
|
||||
"Options.Section.Environment": "ENVIRONMENT VARIABLES",
|
||||
"Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
|
||||
"Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
|
||||
"Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
|
||||
"Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
|
||||
"Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
|
||||
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
|
||||
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
|
||||
"Options.Section.Emulation": "EMULATION",
|
||||
"Options.Section.Logging": "LOGGING",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
@@ -387,6 +387,88 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="EnvSectionTitle" Classes="sectionTitle" Text="ENVIRONMENT VARIABLES" />
|
||||
<TextBlock x:Name="EnvDesc"
|
||||
Text="Switches passed to the emulator as environment variables at launch."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_BTHID_UNAVAILABLE" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvBthidDesc"
|
||||
Text="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever. Leave off normally. Some titles freeze when init fails."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvBthidToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvLoopGuardDesc"
|
||||
Text="Do not force quit titles that repeat the same call for too long. Try this when a game exits on its own while loading."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvVkValidationDesc"
|
||||
Text="Enable Vulkan validation layers for GPU debugging. Slow. Requires the Vulkan SDK to be installed."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_DUMP_SPIRV" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvDumpSpirvDesc"
|
||||
Text="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder. Use when reporting shader or rendering bugs."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_LOG_DIRECT_MEMORY" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvLogDirectMemoryDesc"
|
||||
Text="Log direct memory allocations and failures to the console. Use when a game aborts or exits during boot."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
|
||||
<TextBlock x:Name="EnvLogNpDesc"
|
||||
Text="Log NP (PlayStation Network) library calls to the console."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</Panel>
|
||||
|
||||
@@ -125,6 +125,18 @@ public partial class MainWindow : Window
|
||||
UpdateDiscordPresence();
|
||||
};
|
||||
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
|
||||
EnvBthidToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
|
||||
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
|
||||
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
|
||||
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
|
||||
EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
|
||||
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
|
||||
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
|
||||
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
|
||||
|
||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||
@@ -403,6 +415,15 @@ public partial class MainWindow : Window
|
||||
LoadingStateText.Text = loc.Get("Library.Loading");
|
||||
|
||||
GeneralTabItem.Header = loc.Get("Options.General");
|
||||
EnvTabItem.Header = loc.Get("Options.Env.Tab");
|
||||
EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
|
||||
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
||||
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
|
||||
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
|
||||
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
|
||||
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
|
||||
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
|
||||
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
|
||||
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
||||
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
||||
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
||||
@@ -584,9 +605,34 @@ public partial class MainWindow : Window
|
||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
|
||||
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
|
||||
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
|
||||
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
|
||||
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
|
||||
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
|
||||
UpdateLogFilePathText();
|
||||
}
|
||||
|
||||
// Environment variables set on this process at the previous launch; children
|
||||
// inherit the process environment, so stale names must be cleared explicitly.
|
||||
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private void SetEnvironmentToggle(string name, bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
if (!_settings.EnvironmentToggles.Contains(name))
|
||||
{
|
||||
_settings.EnvironmentToggles.Add(name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_settings.EnvironmentToggles.Remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
private string SelectedLogLevel()
|
||||
{
|
||||
return LogLevelBox.SelectedIndex switch
|
||||
@@ -1402,6 +1448,24 @@ public partial class MainWindow : Window
|
||||
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
|
||||
DimLineBrush);
|
||||
|
||||
// Apply the enabled switches to this process; both emulator launch paths
|
||||
// (CreateProcessW and Process.Start) inherit it. Clear switches turned
|
||||
// off since the previous launch.
|
||||
foreach (var staleName in _appliedEnvironmentVariables)
|
||||
{
|
||||
if (!_settings.EnvironmentToggles.Contains(staleName))
|
||||
{
|
||||
Environment.SetEnvironmentVariable(staleName, null);
|
||||
}
|
||||
}
|
||||
|
||||
_appliedEnvironmentVariables.Clear();
|
||||
foreach (var name in _settings.EnvironmentToggles)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, "1");
|
||||
_appliedEnvironmentVariables.Add(name);
|
||||
}
|
||||
|
||||
var emulator = new EmulatorProcess();
|
||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
@@ -238,23 +239,63 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
const int StackBufferLength = 512;
|
||||
const int ReadChunkLength = 128;
|
||||
var rented = capacity > StackBufferLength ? ArrayPool<byte>.Shared.Rent(capacity) : null;
|
||||
Span<byte> bytes = rented is null ? stackalloc byte[StackBufferLength] : rented;
|
||||
try
|
||||
{
|
||||
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
|
||||
var length = 0;
|
||||
while (length < capacity)
|
||||
{
|
||||
return false;
|
||||
// Bulk-read in bounded chunks rather than the full capacity: the string
|
||||
// may end just before unmapped memory, and overreading past the
|
||||
// terminator by more than a chunk could fault where the old
|
||||
// byte-by-byte loop succeeded.
|
||||
var chunk = Math.Min(ReadChunkLength, capacity - length);
|
||||
var span = bytes.Slice(length, chunk);
|
||||
if (Memory.TryRead(address + (ulong)length, span))
|
||||
{
|
||||
var terminator = span.IndexOf((byte)0);
|
||||
if (terminator >= 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes[..(length + terminator)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
length += chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The chunk touches an unreadable range; fall back to per-byte reads so a
|
||||
// terminator sitting before the bad byte still yields the string.
|
||||
for (var i = 0; i < chunk; i++)
|
||||
{
|
||||
if (!Memory.TryRead(address + (ulong)(length + i), bytes.Slice(length + i, 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytes[length + i] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes[..(length + i)]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
length += chunk;
|
||||
}
|
||||
|
||||
if (bytes[index] == 0)
|
||||
value = Encoding.UTF8.GetString(bytes[..capacity]);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rented is not null)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
ArrayPool<byte>.Shared.Return(rented);
|
||||
}
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool PushUInt64(ulong value)
|
||||
|
||||
@@ -23,6 +23,20 @@ public readonly record struct GuestThreadSnapshot(
|
||||
ulong LastReturnRip,
|
||||
string? BlockReason);
|
||||
|
||||
/// <summary>
|
||||
/// Continuation state for a blocked guest thread, replacing the closure pair a blocking
|
||||
/// wait used to allocate. TryWake runs under the scheduler's guest-thread gate and
|
||||
/// returns true when the waiter has a final result and the thread should be re-readied;
|
||||
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
|
||||
/// its return value becomes the guest's RAX for the resumed call.
|
||||
/// </summary>
|
||||
public interface IGuestThreadBlockWaiter
|
||||
{
|
||||
int Resume();
|
||||
|
||||
bool TryWake();
|
||||
}
|
||||
|
||||
public interface IGuestThreadScheduler
|
||||
{
|
||||
bool SupportsGuestContextTransfer { get; }
|
||||
@@ -106,10 +120,7 @@ public static class GuestThreadExecution
|
||||
private static string? _pendingBlockWakeKey;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Func<int>? _pendingBlockResumeHandler;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Func<bool>? _pendingBlockWakeHandler;
|
||||
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _pendingBlockDeadlineTimestamp;
|
||||
@@ -157,8 +168,7 @@ public static class GuestThreadExecution
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockResumeHandler = null;
|
||||
_pendingBlockWakeHandler = null;
|
||||
_pendingBlockWaiter = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
_pendingEntryExit = false;
|
||||
_pendingEntryExitValue = 0;
|
||||
@@ -179,8 +189,7 @@ public static class GuestThreadExecution
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockResumeHandler = null;
|
||||
_pendingBlockWakeHandler = null;
|
||||
_pendingBlockWaiter = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
_pendingEntryExit = false;
|
||||
_pendingEntryExitValue = 0;
|
||||
@@ -211,8 +220,7 @@ public static class GuestThreadExecution
|
||||
CpuContext? context,
|
||||
string reason,
|
||||
string? wakeKey = null,
|
||||
Func<int>? resumeHandler = null,
|
||||
Func<bool>? wakeHandler = null,
|
||||
IGuestThreadBlockWaiter? waiter = null,
|
||||
long blockDeadlineTimestamp = 0)
|
||||
{
|
||||
if (!IsGuestThread)
|
||||
@@ -222,8 +230,7 @@ public static class GuestThreadExecution
|
||||
|
||||
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
||||
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
||||
_pendingBlockResumeHandler = resumeHandler;
|
||||
_pendingBlockWakeHandler = wakeHandler;
|
||||
_pendingBlockWaiter = waiter;
|
||||
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
||||
{
|
||||
@@ -255,7 +262,6 @@ public static class GuestThreadExecution
|
||||
out hasContinuation,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out _);
|
||||
}
|
||||
|
||||
@@ -264,16 +270,14 @@ public static class GuestThreadExecution
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation,
|
||||
out string wakeKey,
|
||||
out Func<int>? resumeHandler,
|
||||
out Func<bool>? wakeHandler)
|
||||
out IGuestThreadBlockWaiter? waiter)
|
||||
{
|
||||
return TryConsumeCurrentThreadBlock(
|
||||
out reason,
|
||||
out continuation,
|
||||
out hasContinuation,
|
||||
out wakeKey,
|
||||
out resumeHandler,
|
||||
out wakeHandler,
|
||||
out waiter,
|
||||
out _);
|
||||
}
|
||||
|
||||
@@ -282,8 +286,7 @@ public static class GuestThreadExecution
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation,
|
||||
out string wakeKey,
|
||||
out Func<int>? resumeHandler,
|
||||
out Func<bool>? wakeHandler,
|
||||
out IGuestThreadBlockWaiter? waiter,
|
||||
out long blockDeadlineTimestamp)
|
||||
{
|
||||
reason = _pendingBlockReason ?? string.Empty;
|
||||
@@ -292,8 +295,7 @@ public static class GuestThreadExecution
|
||||
continuation = default;
|
||||
hasContinuation = false;
|
||||
wakeKey = string.Empty;
|
||||
resumeHandler = null;
|
||||
wakeHandler = null;
|
||||
waiter = null;
|
||||
blockDeadlineTimestamp = 0;
|
||||
return false;
|
||||
}
|
||||
@@ -301,15 +303,13 @@ public static class GuestThreadExecution
|
||||
continuation = _pendingBlockContinuation;
|
||||
hasContinuation = _pendingBlockContinuationValid;
|
||||
wakeKey = _pendingBlockWakeKey ?? reason;
|
||||
resumeHandler = _pendingBlockResumeHandler;
|
||||
wakeHandler = _pendingBlockWakeHandler;
|
||||
waiter = _pendingBlockWaiter;
|
||||
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockResumeHandler = null;
|
||||
_pendingBlockWakeHandler = null;
|
||||
_pendingBlockWaiter = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@ namespace SharpEmu.HLE;
|
||||
public interface IGuestMemoryAllocator
|
||||
{
|
||||
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
||||
|
||||
bool TryFreeGuestMemory(ulong address);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public static class AgcExports
|
||||
private const uint ItDispatchDirect = 0x15;
|
||||
private const uint ItDispatchIndirect = 0x16;
|
||||
private const uint ItWaitRegMem = 0x3C;
|
||||
private const uint ItIndirectBuffer = 0x3F;
|
||||
private const uint ItEventWrite = 0x46;
|
||||
private const uint ItDmaData = 0x50;
|
||||
private const uint ItSetContextReg = 0x69;
|
||||
@@ -1943,6 +1944,96 @@ public static class AgcExports
|
||||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
// Guest draw-command builders: emit valid (skippable) packets and return a live
|
||||
// command pointer so the guest's command-buffer build succeeds; full draw processing is TODO.
|
||||
[SysAbiExport(
|
||||
Nid = "8N2tmT3jmC8",
|
||||
ExportName = "sceAgcDcbSetIndexCount",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbSetIndexCount(CpuContext ctx)
|
||||
{
|
||||
var dcb = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
if (dcb == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, dcb, 2, out var cmd) ||
|
||||
!ctx.TryWriteUInt32(cmd, Pm4(2, ItNop, RZero)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 4, indexCount))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
return ReturnPointer(ctx, cmd);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xSAR0LTcRKM",
|
||||
ExportName = "sceAgcDcbJump",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbJump(CpuContext ctx)
|
||||
{
|
||||
var dcb = ctx[CpuRegister.Rdi];
|
||||
var target = ctx[CpuRegister.Rsi];
|
||||
var sizeDwords = (uint)ctx[CpuRegister.Rdx];
|
||||
if (dcb == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
|
||||
!ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 12, sizeDwords & 0xFFFFF))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
return ReturnPointer(ctx, cmd);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bbFueFP+J4k",
|
||||
ExportName = "sceAgcDcbSetPredication",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbSetPredication(CpuContext ctx)
|
||||
{
|
||||
var dcb = ctx[CpuRegister.Rdi];
|
||||
var address = ctx[CpuRegister.Rsi];
|
||||
if (dcb == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) ||
|
||||
!ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) ||
|
||||
!ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32)))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
return ReturnPointer(ctx, cmd);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w6Dj1VJt5qY",
|
||||
ExportName = "sceAgcSetPacketPredication",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int SetPacketPredication(CpuContext ctx)
|
||||
{
|
||||
// Global predication toggle on a packet; a no-op is safe for rendering.
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w2rJhmD+dsE",
|
||||
ExportName = "sceAgcDriverAddEqEvent",
|
||||
|
||||
@@ -348,6 +348,18 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Zi3dBUjgyXI",
|
||||
ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int MeasureCommandSizeWriteKernelEventQueueOnCompletion(CpuContext ctx)
|
||||
{
|
||||
TraceAmpr(ctx, "measure_write_equeue_complete", 0, KernelEventQueueRecordSize, 0);
|
||||
ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "C+IEj+BsAFM",
|
||||
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
||||
@@ -440,6 +452,40 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "o67gODLFpls",
|
||||
ExportName = "sceAmprCommandBufferWriteKernelEventQueueOnCompletion",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int CommandBufferWriteKernelEventQueueOnCompletion(CpuContext ctx)
|
||||
{
|
||||
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||
var equeue = ctx[CpuRegister.Rsi];
|
||||
var ident = ctx[CpuRegister.Rdx];
|
||||
var completionToken = ctx[CpuRegister.Rcx];
|
||||
var userData = ctx[CpuRegister.R8];
|
||||
|
||||
if (commandBuffer == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!AppendKernelEventQueueRecord(
|
||||
ctx,
|
||||
commandBuffer,
|
||||
equeue,
|
||||
ident,
|
||||
completionToken,
|
||||
userData))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceAmpr(ctx, "write_equeue_complete", commandBuffer, ident, completionToken);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sJXyWHjP-F8",
|
||||
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
||||
|
||||
@@ -34,9 +34,43 @@ public static class KernelEventFlagCompatExports
|
||||
public object Gate { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class EventFlagWaiter
|
||||
private sealed class EventFlagWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required CpuContext Ctx { get; init; }
|
||||
public required EventFlagState State { get; init; }
|
||||
public required ulong Pattern { get; init; }
|
||||
public required uint WaitMode { get; init; }
|
||||
public required ulong ResultAddress { get; init; }
|
||||
public bool Timed { get; init; }
|
||||
|
||||
// Timed-wait completion state; unused when Timed is false.
|
||||
public ulong TimeoutAddress { get; init; }
|
||||
public long DeadlineTimestamp { get; init; }
|
||||
|
||||
public OrbisGen2Result? Result { get; set; }
|
||||
|
||||
// Untimed waits stash the prepared result here at wake and return it at resume.
|
||||
private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
||||
public int Resume() => Timed
|
||||
? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp)
|
||||
: (int)_blockedResult;
|
||||
|
||||
public bool TryWake()
|
||||
{
|
||||
if (Timed)
|
||||
{
|
||||
return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress);
|
||||
}
|
||||
|
||||
if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_blockedResult = preparedResult;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -249,27 +283,22 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
||||
var timedWaiter = new EventFlagWaiter();
|
||||
var timedWaiter = new EventFlagWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
State = state,
|
||||
Pattern = pattern,
|
||||
WaitMode = waitMode,
|
||||
ResultAddress = resultAddress,
|
||||
Timed = true,
|
||||
TimeoutAddress = timeoutAddress,
|
||||
DeadlineTimestamp = deadline,
|
||||
};
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEventFlag",
|
||||
GetEventFlagWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedTimedWait(
|
||||
ctx,
|
||||
state,
|
||||
timedWaiter,
|
||||
pattern,
|
||||
waitMode,
|
||||
resultAddress,
|
||||
timeoutAddress,
|
||||
deadline),
|
||||
wakeHandler: () => TryCompleteBlockedTimedWait(
|
||||
ctx,
|
||||
state,
|
||||
timedWaiter,
|
||||
pattern,
|
||||
waitMode,
|
||||
resultAddress),
|
||||
timedWaiter,
|
||||
blockDeadlineTimestamp: deadline))
|
||||
{
|
||||
state.WaitingThreads++;
|
||||
@@ -286,27 +315,17 @@ public static class KernelEventFlagCompatExports
|
||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||
var managedThread = Environment.CurrentManagedThreadId;
|
||||
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEventFlag",
|
||||
GetEventFlagWakeKey(handle),
|
||||
() => (int)blockedWaitResult,
|
||||
() =>
|
||||
new EventFlagWaiter
|
||||
{
|
||||
if (!TryPrepareBlockedWait(
|
||||
ctx,
|
||||
state,
|
||||
pattern,
|
||||
waitMode,
|
||||
resultAddress,
|
||||
out var preparedResult))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
blockedWaitResult = preparedResult;
|
||||
return true;
|
||||
Ctx = ctx,
|
||||
State = state,
|
||||
Pattern = pattern,
|
||||
WaitMode = waitMode,
|
||||
ResultAddress = resultAddress,
|
||||
});
|
||||
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -17,7 +19,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
private static readonly object _eventQueueGate = new();
|
||||
private static readonly HashSet<ulong> _eventQueues = new();
|
||||
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
|
||||
private static readonly Dictionary<ulong, KernelEventDeque> _pendingEvents = new();
|
||||
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
||||
private static long _nextEventQueueHandle = 1;
|
||||
|
||||
@@ -34,6 +36,76 @@ public static class KernelEventQueueCompatExports
|
||||
short Filter,
|
||||
ulong UserData);
|
||||
|
||||
// Grow-only ring buffer standing in for LinkedList<KernelQueuedEvent>, which
|
||||
// allocated a node per enqueue — steady churn at one enqueue per vblank/flip edge
|
||||
// per registered queue. Mutated only under _eventQueueGate.
|
||||
private sealed class KernelEventDeque
|
||||
{
|
||||
private KernelQueuedEvent[] _items = new KernelQueuedEvent[4];
|
||||
private int _head;
|
||||
|
||||
public int Count { get; private set; }
|
||||
|
||||
public KernelQueuedEvent this[int index]
|
||||
{
|
||||
get => _items[(_head + index) % _items.Length];
|
||||
set => _items[(_head + index) % _items.Length] = value;
|
||||
}
|
||||
|
||||
public void AddLast(in KernelQueuedEvent item)
|
||||
{
|
||||
if (Count == _items.Length)
|
||||
{
|
||||
var grown = new KernelQueuedEvent[_items.Length * 2];
|
||||
for (var i = 0; i < Count; i++)
|
||||
{
|
||||
grown[i] = this[i];
|
||||
}
|
||||
|
||||
_items = grown;
|
||||
_head = 0;
|
||||
}
|
||||
|
||||
_items[(_head + Count) % _items.Length] = item;
|
||||
Count++;
|
||||
}
|
||||
|
||||
public KernelQueuedEvent RemoveFirst()
|
||||
{
|
||||
var value = _items[_head];
|
||||
_head = (_head + 1) % _items.Length;
|
||||
Count--;
|
||||
return value;
|
||||
}
|
||||
|
||||
public int FindIndex(ulong ident, short filter)
|
||||
{
|
||||
for (var i = 0; i < Count; i++)
|
||||
{
|
||||
var candidate = this[i];
|
||||
if (candidate.Ident == ident && candidate.Filter == filter)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EqueueWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required CpuContext Ctx { get; init; }
|
||||
public required ulong Handle { get; init; }
|
||||
public required ulong EventsAddress { get; init; }
|
||||
public required int EventCapacity { get; init; }
|
||||
public required ulong OutCountAddress { get; init; }
|
||||
|
||||
public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress);
|
||||
|
||||
public bool TryWake() => HasPendingEvents(Handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "D0OdFMjp46I",
|
||||
ExportName = "sceKernelCreateEqueue",
|
||||
@@ -51,7 +123,7 @@ public static class KernelEventQueueCompatExports
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
_eventQueues.Add(handle);
|
||||
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
||||
_pendingEvents[handle] = new KernelEventDeque();
|
||||
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||
}
|
||||
|
||||
@@ -79,6 +151,8 @@ public static class KernelEventQueueCompatExports
|
||||
_registeredEvents.Remove(handle);
|
||||
}
|
||||
|
||||
_wakeKeys.TryRemove(handle, out _);
|
||||
|
||||
TraceEventQueue(ctx, "delete", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -343,8 +417,14 @@ public static class KernelEventQueueCompatExports
|
||||
ctx,
|
||||
"sceKernelWaitEqueue",
|
||||
GetEventQueueWakeKey(handle),
|
||||
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
|
||||
() => HasPendingEvents(handle)))
|
||||
new EqueueWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
Handle = handle,
|
||||
EventsAddress = eventsAddress,
|
||||
EventCapacity = eventCapacity,
|
||||
OutCountAddress = outCountAddress,
|
||||
}))
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-block", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -409,7 +489,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
queue = new KernelEventDeque();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
@@ -480,7 +560,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
queue = new KernelEventDeque();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
@@ -527,7 +607,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
queue = new KernelEventDeque();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
@@ -563,15 +643,15 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var events))
|
||||
{
|
||||
events = new LinkedList<KernelQueuedEvent>();
|
||||
events = new KernelEventDeque();
|
||||
_pendingEvents[handle] = events;
|
||||
}
|
||||
|
||||
var count = 1UL;
|
||||
var pendingNode = FindPendingEvent(events, ident, filter);
|
||||
if (pendingNode is not null)
|
||||
var pendingIndex = events.FindIndex(ident, filter);
|
||||
if (pendingIndex >= 0)
|
||||
{
|
||||
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
|
||||
count = Math.Min(((events[pendingIndex].Data >> 12) & 0xFUL) + 1, 0xFUL);
|
||||
}
|
||||
|
||||
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
|
||||
@@ -584,9 +664,9 @@ public static class KernelEventQueueCompatExports
|
||||
eventData,
|
||||
userData);
|
||||
|
||||
if (pendingNode is not null)
|
||||
if (pendingIndex >= 0)
|
||||
{
|
||||
pendingNode.Value = triggeredEvent;
|
||||
events[pendingIndex] = triggeredEvent;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -631,40 +711,28 @@ public static class KernelEventQueueCompatExports
|
||||
}
|
||||
|
||||
private static void QueueOrUpdateEvent(
|
||||
LinkedList<KernelQueuedEvent> queue,
|
||||
KernelEventDeque queue,
|
||||
KernelQueuedEvent queuedEvent)
|
||||
{
|
||||
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter);
|
||||
if (pendingNode is null)
|
||||
var pendingIndex = queue.FindIndex(queuedEvent.Ident, queuedEvent.Filter);
|
||||
if (pendingIndex < 0)
|
||||
{
|
||||
queue.AddLast(queuedEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
pendingNode.Value = queuedEvent with
|
||||
queue[pendingIndex] = queuedEvent with
|
||||
{
|
||||
Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags),
|
||||
Fflags = Math.Max(queue[pendingIndex].Fflags + 1, queuedEvent.Fflags),
|
||||
};
|
||||
}
|
||||
|
||||
private static LinkedListNode<KernelQueuedEvent>? FindPendingEvent(
|
||||
LinkedList<KernelQueuedEvent> queue,
|
||||
ulong ident,
|
||||
short filter)
|
||||
{
|
||||
for (var node = queue.First; node is not null; node = node.Next)
|
||||
{
|
||||
if (node.Value.Ident == ident && node.Value.Filter == filter)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
// Wake keys are formatted once per handle: WakeEventQueue runs on every event
|
||||
// enqueue (vblank/flip edges included), so formatting there is steady string churn.
|
||||
private static readonly ConcurrentDictionary<ulong, string> _wakeKeys = new();
|
||||
|
||||
private static string GetEventQueueWakeKey(ulong handle) =>
|
||||
$"sceKernelWaitEqueue:{handle:X16}";
|
||||
_wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}");
|
||||
|
||||
private static void WakeEventQueue(ulong handle)
|
||||
{
|
||||
@@ -678,7 +746,10 @@ public static class KernelEventQueueCompatExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Engines wait on the vblank/flip equeue every frame, so the delivery buffer
|
||||
// (usually a single event) comes from the pool instead of a per-call array.
|
||||
KernelQueuedEvent[] events;
|
||||
int count;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
||||
@@ -686,24 +757,30 @@ public static class KernelEventQueueCompatExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = Math.Min(eventCapacity, queue.Count);
|
||||
events = new KernelQueuedEvent[count];
|
||||
count = Math.Min(eventCapacity, queue.Count);
|
||||
events = ArrayPool<KernelQueuedEvent>.Shared.Rent(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
events[i] = queue.First!.Value;
|
||||
queue.RemoveFirst();
|
||||
events[i] = queue.RemoveFirst();
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < events.Length; i++)
|
||||
try
|
||||
{
|
||||
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
return i;
|
||||
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<KernelQueuedEvent>.Shared.Return(events);
|
||||
}
|
||||
|
||||
return events.Length;
|
||||
return count;
|
||||
}
|
||||
|
||||
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
||||
@@ -718,9 +795,12 @@ public static class KernelEventQueueCompatExports
|
||||
return ctx.Memory.TryWrite(address, eventBytes);
|
||||
}
|
||||
|
||||
private static readonly bool _logEqueue =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
|
||||
if (!_logEqueue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ public static class KernelMemoryCompatExports
|
||||
private const int MaxGuestStringLength = 4096;
|
||||
private const int WideCharSize = sizeof(ushort);
|
||||
private const int MemsetChunkSize = 16 * 1024;
|
||||
private const int MemcpyChunkSize = 256 * 1024;
|
||||
|
||||
// Shared all-zero scratch for chunked zero-fill loops; never written to.
|
||||
private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize];
|
||||
private const int TlsModuleBlockSize = 0x10000;
|
||||
private const int O_WRONLY = 0x1;
|
||||
private const int O_RDWR = 0x2;
|
||||
@@ -255,11 +259,10 @@ public static class KernelMemoryCompatExports
|
||||
DirectStart: 0);
|
||||
}
|
||||
|
||||
var zeroes = new byte[(int)Math.Min(mappedLength, (ulong)MemsetChunkSize)];
|
||||
for (ulong offset = 0; offset < mappedLength;)
|
||||
{
|
||||
var chunkLength = (int)Math.Min((ulong)zeroes.Length, mappedLength - offset);
|
||||
if (!ctx.Memory.TryWrite(address + offset, zeroes.AsSpan(0, chunkLength)))
|
||||
var chunkLength = (int)Math.Min((ulong)_zeroChunk.Length, mappedLength - offset);
|
||||
if (!ctx.Memory.TryWrite(address + offset, _zeroChunk.AsSpan(0, chunkLength)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -422,33 +425,50 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var chunk = new byte[MemsetChunkSize];
|
||||
Array.Fill(chunk, value);
|
||||
var remaining = length;
|
||||
var cursor = destination;
|
||||
while (remaining > 0)
|
||||
// Rent may hand back a larger array than requested; only the first chunkLength
|
||||
// bytes are filled, so the loop must cap at chunkLength rather than chunk.Length.
|
||||
var chunkLength = (int)Math.Min(length, (ulong)MemsetChunkSize);
|
||||
var chunk = value == 0 ? _zeroChunk : ArrayPool<byte>.Shared.Rent(chunkLength);
|
||||
if (value != 0)
|
||||
{
|
||||
var take = (int)Math.Min((ulong)chunk.Length, remaining);
|
||||
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
||||
chunk.AsSpan(0, chunkLength).Fill(value);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var remaining = length;
|
||||
var cursor = destination;
|
||||
while (remaining > 0)
|
||||
{
|
||||
if (length <= 0x40)
|
||||
var take = (int)Math.Min((ulong)chunkLength, remaining);
|
||||
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
|
||||
{
|
||||
var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount);
|
||||
if (recoveryIndex <= 8)
|
||||
if (length <= 0x40)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
|
||||
var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount);
|
||||
if (recoveryIndex <= 8)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
cursor += (ulong)take;
|
||||
remaining -= (ulong)take;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (value != 0)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(chunk);
|
||||
}
|
||||
|
||||
cursor += (ulong)take;
|
||||
remaining -= (ulong)take;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
@@ -1185,11 +1205,10 @@ public static class KernelMemoryCompatExports
|
||||
var rawCount = ctx[CpuRegister.Rdx];
|
||||
|
||||
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
||||
// upstream bug that also feeds bad lengths to memset) must not reach
|
||||
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
|
||||
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
|
||||
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
|
||||
// exception. Reject anything above a sane bound before allocating.
|
||||
// upstream bug that also feeds bad lengths to memset) must not turn into a multi-GB
|
||||
// copy attempt from a guest-thread call context, which corrupted the CLR outright
|
||||
// ("Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||
// code") instead of throwing a normal exception. Reject anything above a sane bound.
|
||||
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
||||
if (rawCount > maxSaneCount)
|
||||
{
|
||||
@@ -1198,15 +1217,52 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var count = (int)rawCount;
|
||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
||||
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
if (rawCount == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Cap iterations at the requested chunk size, not chunk.Length: Rent may hand
|
||||
// back a larger array, and the copy granularity should not depend on pool
|
||||
// bucketing internals.
|
||||
var chunkLength = (int)Math.Min(rawCount, (ulong)MemcpyChunkSize);
|
||||
var chunk = ArrayPool<byte>.Shared.Rent(chunkLength);
|
||||
try
|
||||
{
|
||||
// memmove aliases this export, so overlapping ranges must survive the chunked
|
||||
// copy: when the destination starts inside the source range, copy high-to-low
|
||||
// so no source byte is overwritten before it has been read.
|
||||
var copyBackward = destination > source && destination - source < rawCount;
|
||||
var remaining = rawCount;
|
||||
ulong offset = copyBackward ? rawCount : 0;
|
||||
while (remaining > 0)
|
||||
{
|
||||
var take = (int)Math.Min((ulong)chunkLength, remaining);
|
||||
if (copyBackward)
|
||||
{
|
||||
offset -= (ulong)take;
|
||||
}
|
||||
|
||||
var span = chunk.AsSpan(0, take);
|
||||
if (!TryReadCompat(ctx, source + offset, span) || !TryWriteCompat(ctx, destination + offset, span))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (!copyBackward)
|
||||
{
|
||||
offset += (ulong)take;
|
||||
}
|
||||
|
||||
remaining -= (ulong)take;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(chunk);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1834,6 +1890,45 @@ public static class KernelMemoryCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fgIsQ10xYVA",
|
||||
ExportName = "sceKernelChmod",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelChmod(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var mode = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
if (pathAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var hostPath = ResolveGuestPath(guestPath);
|
||||
if (IsReadOnlyGuestMutationPath(guestPath))
|
||||
{
|
||||
LogOpenTrace($"chmod readonly path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
if (!File.Exists(hostPath) && !Directory.Exists(hostPath))
|
||||
{
|
||||
AddNegativeStatCacheForGuestPath(guestPath);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
// POSIX permission bits have no host equivalent on Windows; accept the call
|
||||
// so guests that chmod their freshly created files/directories can proceed.
|
||||
LogOpenTrace($"chmod path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "NNtFaKJbPt0",
|
||||
ExportName = "_close",
|
||||
|
||||
@@ -61,10 +61,34 @@ public static class KernelPthreadCompatExports
|
||||
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
||||
}
|
||||
|
||||
private sealed class PthreadMutexWaiter
|
||||
private sealed class PthreadMutexWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required ulong ThreadId { get; init; }
|
||||
public required CpuContext Ctx { get; init; }
|
||||
public required ulong MutexAddress { get; init; }
|
||||
public required ulong ResolvedAddress { get; init; }
|
||||
public required PthreadMutexState State { get; init; }
|
||||
public int Reserved;
|
||||
|
||||
public int Resume() => CompleteBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
|
||||
|
||||
public bool TryWake() => TryReserveBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
|
||||
}
|
||||
|
||||
private sealed class PthreadCondWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required CpuContext Ctx { get; init; }
|
||||
public required ulong CondAddress { get; init; }
|
||||
public required ulong MutexAddress { get; init; }
|
||||
public required PthreadCondState State { get; init; }
|
||||
public required ulong ObservedEpoch { get; init; }
|
||||
public required bool Timed { get; init; }
|
||||
public required int ReleasedRecursion { get; init; }
|
||||
public required bool PosixResult { get; init; }
|
||||
|
||||
public int Resume() => ResumePthreadCondWait(Ctx, CondAddress, MutexAddress, State, ObservedEpoch, Timed, ReleasedRecursion, PosixResult);
|
||||
|
||||
public bool TryWake() => State.SignalEpoch != ObservedEpoch;
|
||||
}
|
||||
|
||||
private sealed class PthreadCondState
|
||||
@@ -629,6 +653,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
if (!InitializeMutexObject(ctx, handle, state))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
state.Semaphore.Dispose();
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -641,6 +666,7 @@ public static class KernelPthreadCompatExports
|
||||
_mutexStates.TryRemove(mutexAddress, out _);
|
||||
_mutexStates.TryRemove(handle, out _);
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
state.Semaphore.Dispose();
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -668,6 +694,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
||||
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||
state.Semaphore.Dispose();
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -724,7 +751,6 @@ public static class KernelPthreadCompatExports
|
||||
if (!acquired)
|
||||
{
|
||||
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
|
||||
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||
// Fibers retain the synchronous fallback to preserve switch state.
|
||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
|
||||
@@ -736,8 +762,14 @@ public static class KernelPthreadCompatExports
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
state.WakeKey,
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||
new PthreadMutexWaiter
|
||||
{
|
||||
ThreadId = currentThreadId,
|
||||
Ctx = ctx,
|
||||
MutexAddress = mutexAddress,
|
||||
ResolvedAddress = resolvedAddress,
|
||||
State = state,
|
||||
}))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -895,6 +927,7 @@ public static class KernelPthreadCompatExports
|
||||
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -912,6 +945,7 @@ public static class KernelPthreadCompatExports
|
||||
_mutexAttrStates.Remove(handle);
|
||||
}
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -935,6 +969,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1195,6 +1230,7 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
if (_condStates.TryGetValue(condAddress, out var raced))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = condAddress;
|
||||
state = raced;
|
||||
return true;
|
||||
@@ -1212,6 +1248,7 @@ public static class KernelPthreadCompatExports
|
||||
_condStates.Remove(handle);
|
||||
}
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1231,7 +1268,22 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
Span<byte> initialData = stackalloc byte[size];
|
||||
initialData.Clear();
|
||||
return ctx.Memory.TryWrite(address, initialData);
|
||||
if (ctx.Memory.TryWrite(address, initialData))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
allocator.TryFreeGuestMemory(address);
|
||||
address = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void TryFreeOpaqueObject(CpuContext ctx, ulong address)
|
||||
{
|
||||
if (ctx.Memory is IGuestMemoryAllocator allocator)
|
||||
{
|
||||
allocator.TryFreeGuestMemory(address);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||
@@ -1269,6 +1321,7 @@ public static class KernelPthreadCompatExports
|
||||
_condStates.Remove(handle);
|
||||
}
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -1293,6 +1346,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
||||
TryFreeOpaqueObject(ctx, resolvedAddress);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1363,8 +1417,17 @@ public static class KernelPthreadCompatExports
|
||||
ctx,
|
||||
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||
state.WakeKey,
|
||||
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
|
||||
() => state.SignalEpoch != observedEpoch,
|
||||
new PthreadCondWaiter
|
||||
{
|
||||
Ctx = ctx,
|
||||
CondAddress = condAddress,
|
||||
MutexAddress = mutexAddress,
|
||||
State = state,
|
||||
ObservedEpoch = observedEpoch,
|
||||
Timed = timed,
|
||||
ReleasedRecursion = releasedRecursion,
|
||||
PosixResult = posixResult,
|
||||
},
|
||||
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||
{
|
||||
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
||||
@@ -1775,6 +1838,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
if (!InitializeMutexObject(ctx, handle, createdState))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = 0;
|
||||
state = null;
|
||||
return false;
|
||||
@@ -1784,12 +1848,14 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = mutexAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_mutexStates.TryGetValue(handle, out state))
|
||||
{
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = handle;
|
||||
return true;
|
||||
}
|
||||
@@ -1803,6 +1869,7 @@ public static class KernelPthreadCompatExports
|
||||
_mutexStates.TryRemove(mutexAddress, out _);
|
||||
_mutexStates.TryRemove(handle, out _);
|
||||
|
||||
TryFreeOpaqueObject(ctx, handle);
|
||||
resolvedAddress = 0;
|
||||
state = null;
|
||||
return false;
|
||||
|
||||
@@ -164,6 +164,17 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required PthreadRwlockState Rwlock { get; init; }
|
||||
public required ulong ThreadId { get; init; }
|
||||
public required bool Write { get; init; }
|
||||
|
||||
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
||||
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
|
||||
}
|
||||
|
||||
private readonly record struct TlsKeyState(ulong Destructor);
|
||||
|
||||
private readonly record struct PthreadAttrState(
|
||||
@@ -382,6 +393,47 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "oIRFTjoILbg",
|
||||
ExportName = "scePthreadSetschedparam",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadSetschedparam(CpuContext ctx) => PosixPthreadSetschedparam(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "P41kTWUS3EI",
|
||||
ExportName = "scePthreadGetschedparam",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadGetschedparam(CpuContext ctx)
|
||||
{
|
||||
var thread = ctx[CpuRegister.Rdi];
|
||||
var outPolicyAddress = ctx[CpuRegister.Rsi];
|
||||
var outSchedParamAddress = ctx[CpuRegister.Rdx];
|
||||
if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
int policy;
|
||||
int priority;
|
||||
lock (_stateGate)
|
||||
{
|
||||
var state = GetOrCreateThreadStateLocked(thread);
|
||||
policy = state.Attributes.SchedPolicy;
|
||||
priority = state.Priority;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
|
||||
!ctx.TryWriteInt32(outSchedParamAddress, priority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "nsYoNRywwNg",
|
||||
ExportName = "scePthreadAttrInit",
|
||||
@@ -1318,8 +1370,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
ctx,
|
||||
"pthread_rwlock_wrlock",
|
||||
rwlock.WakeKey,
|
||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
|
||||
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
|
||||
{
|
||||
transferredToScheduler = true;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -1355,8 +1406,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
ctx,
|
||||
"pthread_rwlock_rdlock",
|
||||
rwlock.WakeKey,
|
||||
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
|
||||
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ public static class KernelSemaphoreCompatExports
|
||||
private sealed class KernelSemaphoreState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
// Formatted once at creation; signal/wait/cancel/delete all wake through this key.
|
||||
public required string WakeKey { get; init; }
|
||||
public required int InitialCount { get; init; }
|
||||
public required int MaxCount { get; init; }
|
||||
public int Count { get; set; }
|
||||
@@ -28,14 +30,26 @@ public static class KernelSemaphoreCompatExports
|
||||
public object Gate { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class SemaphoreWaiter
|
||||
private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter
|
||||
{
|
||||
public required KernelSemaphoreState Semaphore { get; init; }
|
||||
public required int NeedCount { get; init; }
|
||||
public required int CancelEpochAtBlock { get; init; }
|
||||
public bool Timed { get; init; }
|
||||
|
||||
// Timed-wait completion state; unused when Timed is false.
|
||||
public CpuContext? Ctx { get; init; }
|
||||
public ulong TimeoutAddress { get; init; }
|
||||
public long DeadlineTimestamp { get; init; }
|
||||
|
||||
// Written and read only under the owning semaphore's Gate.
|
||||
public int? Result { get; set; }
|
||||
|
||||
public int Resume() => Timed
|
||||
? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp)
|
||||
: CompleteBlockedSemaWait(Semaphore, this);
|
||||
|
||||
public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this);
|
||||
}
|
||||
|
||||
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
||||
@@ -79,6 +93,7 @@ public static class KernelSemaphoreCompatExports
|
||||
var state = new KernelSemaphoreState
|
||||
{
|
||||
Name = name,
|
||||
WakeKey = GetSemaphoreWakeKey(handle),
|
||||
InitialCount = initialCount,
|
||||
MaxCount = maxCount,
|
||||
Count = initialCount,
|
||||
@@ -96,11 +111,14 @@ public static class KernelSemaphoreCompatExports
|
||||
state.Deleted = true;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -131,7 +149,10 @@ public static class KernelSemaphoreCompatExports
|
||||
if (semaphore.Count >= needCount)
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -145,7 +166,10 @@ public static class KernelSemaphoreCompatExports
|
||||
if (timeoutMicros == 0)
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
pollTimedOut = true;
|
||||
}
|
||||
else
|
||||
@@ -153,27 +177,36 @@ public static class KernelSemaphoreCompatExports
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
||||
var timedWaiter = new SemaphoreWaiter
|
||||
{
|
||||
Semaphore = semaphore,
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
Timed = true,
|
||||
Ctx = ctx,
|
||||
TimeoutAddress = timeoutAddress,
|
||||
DeadlineTimestamp = deadline,
|
||||
};
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
|
||||
semaphore.WakeKey,
|
||||
timedWaiter,
|
||||
blockDeadlineTimestamp: deadline))
|
||||
{
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
||||
// immediate-timeout poll the callers already tolerate.
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
pollTimedOut = true;
|
||||
}
|
||||
}
|
||||
@@ -182,22 +215,28 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
var waiter = new SemaphoreWaiter
|
||||
{
|
||||
Semaphore = semaphore,
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
};
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
||||
semaphore.WakeKey,
|
||||
waiter))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
@@ -239,12 +278,18 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
if (semaphore.Count < needCount)
|
||||
{
|
||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
}
|
||||
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
@@ -277,14 +322,17 @@ public static class KernelSemaphoreCompatExports
|
||||
}
|
||||
|
||||
semaphore.Count += signalCount;
|
||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
}
|
||||
|
||||
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
||||
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
||||
// whose needCount exceeds the remaining count stays parked while a smaller
|
||||
// waiter can proceed.
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -322,10 +370,13 @@ public static class KernelSemaphoreCompatExports
|
||||
// exactly once in its wake handler. Zeroing here as well would double-count
|
||||
// and silently absorb the increment of a waiter that parks between this
|
||||
// gate release and the wake-all below.
|
||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -349,8 +400,11 @@ public static class KernelSemaphoreCompatExports
|
||||
semaphore.Deleted = true;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||
}
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
@@ -376,7 +430,10 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -384,7 +441,10 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -393,7 +453,10 @@ public static class KernelSemaphoreCompatExports
|
||||
semaphore.Count -= waiter.NeedCount;
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -434,7 +497,10 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
if (_traceSema)
|
||||
{
|
||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
}
|
||||
|
||||
result = waiter.Result!.Value;
|
||||
@@ -456,11 +522,13 @@ public static class KernelSemaphoreCompatExports
|
||||
return result;
|
||||
}
|
||||
|
||||
// Call sites must check this before building the interpolated message; the trace
|
||||
// strings would otherwise be allocated on every semaphore op even with tracing off.
|
||||
private static readonly bool _traceSema =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||
}
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ namespace SharpEmu.Libs.Mouse;
|
||||
|
||||
public static class MouseExports
|
||||
{
|
||||
[SysAbiExport(
|
||||
Nid = "Qs0wWulgl7U",
|
||||
ExportName = "sceMouseInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMouse")]
|
||||
public static int MouseInit(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
||||
// as an sceNgs2VoiceGetState alias.
|
||||
[SysAbiExport(
|
||||
|
||||
@@ -146,6 +146,13 @@ public static class NetCtlExports
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1NE9OWdBIww",
|
||||
ExportName = "sceNetCtlRegisterCallbackV6",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNetCtl")]
|
||||
public static int NetCtlRegisterCallbackV6(CpuContext ctx) => NetCtlRegisterCallback(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "obuxdTiwkF8",
|
||||
ExportName = "sceNetCtlGetInfo",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
|
||||
// Stub for sce::Np::CppWebApi: titles abort PS5-component startup if
|
||||
// Common::initialize returns a negative SCE error, so no-op success is required to boot.
|
||||
public static class NpCppWebApiExports
|
||||
{
|
||||
[SysAbiExport(
|
||||
Nid = "UYPxv8MIzGo",
|
||||
ExportName = "_ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpCppWebApi")]
|
||||
public static int CppWebApiCommonInitialize(CpuContext ctx)
|
||||
{
|
||||
// int Common::initialize(const InitParams&, LibContext&) — 0 on success.
|
||||
TraceCppWebApi("common_initialize", ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static void TraceCppWebApi(string operation, ulong arg0, ulong arg1)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] np_cppwebapi.{operation} arg0=0x{arg0:X16} arg1=0x{arg1:X16}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
// Minimal libSceBluetoothHid stub: no host Bluetooth passthrough, so report
|
||||
// success and let the Pad path provide input (SHARPEMU_BTHID_UNAVAILABLE=1 fails instead).
|
||||
public static class BluetoothHidExports
|
||||
{
|
||||
private const int BluetoothHidUnavailable = unchecked((int)0x80960001);
|
||||
|
||||
private static readonly bool _reportUnavailable = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_BTHID_UNAVAILABLE"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static int Result(CpuContext ctx) =>
|
||||
ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "tul3-GzejQc",
|
||||
ExportName = "sceBluetoothHidInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceBluetoothHid")]
|
||||
public static int BluetoothHidInit(CpuContext ctx) => Result(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4FUZ+c52d2k",
|
||||
ExportName = "sceBluetoothHidRegisterDevice",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceBluetoothHid")]
|
||||
public static int BluetoothHidRegisterDevice(CpuContext ctx) => Result(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4Ypfo9RIwfM",
|
||||
ExportName = "sceBluetoothHidRegisterCallback",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceBluetoothHid")]
|
||||
public static int BluetoothHidRegisterCallback(CpuContext ctx) => Result(ctx);
|
||||
}
|
||||
@@ -48,7 +48,18 @@ public static class PadExports
|
||||
ExportName = "scePadOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadOpen(CpuContext ctx)
|
||||
public static int PadOpen(CpuContext ctx) => PadOpenCore(ctx, extended: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WFIiSfXGUq8",
|
||||
ExportName = "scePadOpenExt",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
|
||||
|
||||
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
|
||||
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
|
||||
private static int PadOpenCore(CpuContext ctx, bool extended)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
@@ -64,7 +75,8 @@ public static class PadExports
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
||||
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
|
||||
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
@@ -83,6 +95,19 @@ public static class PadExports
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6ncge5+l5Qs",
|
||||
ExportName = "scePadClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadClose(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return handle == PrimaryPadHandle
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "clVvL4ZDntw",
|
||||
ExportName = "scePadSetMotionSensorState",
|
||||
@@ -131,6 +156,47 @@ public static class PadExports
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hGbf2QTBmqc",
|
||||
ExportName = "scePadGetExtControllerInformation",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadGetExtControllerInformation(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (informationAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// Base ScePadControllerInformation + device-class/connection fields: report a connected
|
||||
// DualSense so the guest's open -> get-ext-info -> close probe loop resolves.
|
||||
Span<byte> information = stackalloc byte[0x40];
|
||||
information.Clear();
|
||||
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
|
||||
information[0x08] = 30;
|
||||
information[0x09] = 30;
|
||||
information[0x0A] = StandardPortType;
|
||||
information[0x0B] = 1; // connected count
|
||||
information[0x0C] = 1; // connected
|
||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||
information[0x1C] = 0; // deviceClass: 0 = standard controller / DualSense
|
||||
information[0x1D] = 1; // connected (ext)
|
||||
information[0x1E] = 0; // connectionType: local
|
||||
|
||||
return ctx.Memory.TryWrite(informationAddress, information)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YndgXqQVV7c",
|
||||
ExportName = "scePadReadState",
|
||||
|
||||
@@ -185,6 +185,31 @@ public static class UserServiceExports
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "woNpu+45RLk",
|
||||
ExportName = "sceUserServiceGetAgeLevel",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetAgeLevel(CpuContext ctx)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var ageLevelAddress = ctx[CpuRegister.Rsi];
|
||||
if (userId != 1000)
|
||||
{
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
|
||||
if (ageLevelAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
// Report an adult account so titles skip parental-restriction paths.
|
||||
return ctx.TryWriteInt32(ageLevelAddress, 21)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static void TraceUserService(string message)
|
||||
{
|
||||
if (_traceUserService)
|
||||
|
||||
@@ -5,6 +5,7 @@ using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Audio;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Logging;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -180,6 +181,10 @@ public static class VideoOutExports
|
||||
}
|
||||
}
|
||||
|
||||
// Only ever touched by the vblank pump thread; reused across edges so the 60 Hz
|
||||
// pump does not allocate a fresh snapshot per edge.
|
||||
private static readonly List<VideoOutPortState> _vblankPumpPorts = new();
|
||||
|
||||
private static void PumpVblanks()
|
||||
{
|
||||
lock (_vblankEdgeGate)
|
||||
@@ -188,7 +193,7 @@ public static class VideoOutExports
|
||||
Monitor.PulseAll(_vblankEdgeGate);
|
||||
}
|
||||
|
||||
VideoOutPortState[] ports;
|
||||
_vblankPumpPorts.Clear();
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (_ports.Count == 0)
|
||||
@@ -198,10 +203,16 @@ public static class VideoOutExports
|
||||
|
||||
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
||||
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
||||
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
|
||||
foreach (var port in _ports.Values)
|
||||
{
|
||||
if (port.VblankEvents.Count != 0)
|
||||
{
|
||||
_vblankPumpPorts.Add(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var port in ports)
|
||||
foreach (var port in _vblankPumpPorts)
|
||||
{
|
||||
SignalVblank(port);
|
||||
}
|
||||
@@ -221,6 +232,12 @@ public static class VideoOutExports
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
// Call sites must check this before building the interpolated message; the trace
|
||||
// strings would otherwise be allocated on the per-frame flip path even with tracing off.
|
||||
private static readonly bool _logVideoOut = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _dumpVideoOut = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
||||
"1",
|
||||
@@ -403,6 +420,34 @@ public static class VideoOutExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "+I4K03i3EL0",
|
||||
ExportName = "sceVideoOutInitializeOutputOptions",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutInitializeOutputOptions(CpuContext ctx)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "w0hLuNarQxY",
|
||||
ExportName = "sceVideoOutConfigureOutput",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutConfigureOutput(CpuContext ctx)
|
||||
{
|
||||
// Accept the requested output configuration; the presenter always renders
|
||||
// at the display buffer's native size.
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!TryGetPort(handle, out _))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "utPrVdxio-8",
|
||||
ExportName = "sceVideoOutGetOutputStatus",
|
||||
@@ -558,7 +603,10 @@ public static class VideoOutExports
|
||||
// Some engines wait on this queue before issuing their first flip. Provide a first
|
||||
// edge now; later calls to WaitVblank advance the same notification sequence.
|
||||
SignalVblank(port);
|
||||
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -595,7 +643,10 @@ public static class VideoOutExports
|
||||
}
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -656,7 +707,10 @@ public static class VideoOutExports
|
||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg));
|
||||
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
||||
|
||||
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1049,33 +1103,53 @@ public static class VideoOutExports
|
||||
|
||||
private static void SignalVblank(VideoOutPortState port)
|
||||
{
|
||||
List<FlipEventRegistration> vblankEvents;
|
||||
// Snapshot the registrations into a pooled rental so the triggers can run outside
|
||||
// _stateGate without copying the list into a fresh allocation on every edge.
|
||||
// A per-port reusable buffer would race: the pump thread and a guest thread's
|
||||
// first-edge signal (AddVblankEvent) can signal the same port concurrently.
|
||||
FlipEventRegistration[]? vblankEvents = null;
|
||||
int vblankEventCount;
|
||||
ulong eventHint;
|
||||
lock (_stateGate)
|
||||
{
|
||||
port.VblankCount++;
|
||||
eventHint = SceVideoOutInternalEventVblank |
|
||||
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||
vblankEvents = new List<FlipEventRegistration>(port.VblankEvents);
|
||||
vblankEventCount = port.VblankEvents.Count;
|
||||
if (vblankEventCount != 0)
|
||||
{
|
||||
vblankEvents = ArrayPool<FlipEventRegistration>.Shared.Rent(vblankEventCount);
|
||||
port.VblankEvents.CopyTo(vblankEvents);
|
||||
}
|
||||
}
|
||||
|
||||
var signalCount = Interlocked.Increment(ref _vblankSignalCount);
|
||||
|
||||
foreach (var vblankEvent in vblankEvents)
|
||||
if (vblankEvents is not null)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
vblankEvent.Equeue,
|
||||
SceVideoOutInternalEventVblank,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
vblankEvent.UserData);
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < vblankEventCount; i++)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
vblankEvents[i].Equeue,
|
||||
SceVideoOutInternalEventVblank,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
vblankEvents[i].UserData);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<FlipEventRegistration>.Shared.Return(vblankEvents);
|
||||
}
|
||||
}
|
||||
|
||||
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
|
||||
$"queues={vblankEvents.Count} hint=0x{eventHint:X16}");
|
||||
$"queues={vblankEventCount} hint=0x{eventHint:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1097,8 +1171,11 @@ public static class VideoOutExports
|
||||
return OrbisVideoOutErrorInvalidIndex;
|
||||
}
|
||||
|
||||
// Pooled snapshot for the same reason as SignalVblank: triggers run outside
|
||||
// _stateGate, and SubmitFlip is per-frame so a fresh List copy is steady churn.
|
||||
ulong eventHint;
|
||||
List<FlipEventRegistration> flipEvents;
|
||||
FlipEventRegistration[]? flipEvents = null;
|
||||
int flipEventCount;
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
|
||||
@@ -1110,7 +1187,12 @@ public static class VideoOutExports
|
||||
port.FlipCount++;
|
||||
eventHint = SceVideoOutInternalEventFlip |
|
||||
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
||||
flipEventCount = port.FlipEvents.Count;
|
||||
if (flipEventCount != 0)
|
||||
{
|
||||
flipEvents = ArrayPool<FlipEventRegistration>.Shared.Rent(flipEventCount);
|
||||
port.FlipEvents.CopyTo(flipEvents);
|
||||
}
|
||||
}
|
||||
|
||||
var guestImageSubmitted = false;
|
||||
@@ -1132,14 +1214,24 @@ public static class VideoOutExports
|
||||
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
||||
}
|
||||
|
||||
foreach (var flipEvent in flipEvents)
|
||||
if (flipEvents is not null)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
flipEvent.Equeue,
|
||||
SceVideoOutInternalEventFlip,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
flipEvent.UserData);
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < flipEventCount; i++)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
flipEvents[i].Equeue,
|
||||
SceVideoOutInternalEventFlip,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
flipEvents[i].UserData);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<FlipEventRegistration>.Shared.Return(flipEvents);
|
||||
}
|
||||
}
|
||||
|
||||
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
|
||||
@@ -1148,10 +1240,13 @@ public static class VideoOutExports
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
|
||||
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
|
||||
$"flipQueues={flipEvents.Count}");
|
||||
$"flipQueues={flipEventCount}");
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEventCount}");
|
||||
}
|
||||
ReportFrameRate(presented: false);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -1233,8 +1328,11 @@ public static class VideoOutExports
|
||||
slot.AddressRight = 0;
|
||||
}
|
||||
|
||||
TraceVideoOut(
|
||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut(
|
||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||
}
|
||||
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
||||
|
||||
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
|
||||
@@ -1400,7 +1498,10 @@ public static class VideoOutExports
|
||||
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
|
||||
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
|
||||
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1439,7 +1540,10 @@ public static class VideoOutExports
|
||||
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
|
||||
File.WriteAllBytes(basePath + ".raw", bytes);
|
||||
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
|
||||
if (_logVideoOut)
|
||||
{
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1666,11 +1770,6 @@ public static class VideoOutExports
|
||||
|
||||
private static void TraceVideoOut(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE.Host;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Memory;
|
||||
|
||||
public sealed class GuestMemoryAllocatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void FreedRangesAreReusedAndCoalesced()
|
||||
{
|
||||
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
||||
const ulong usableArenaSize = 0x0100_0000 - 0x1000;
|
||||
|
||||
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
|
||||
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
|
||||
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize - 0xC000, 0x1000, out var third));
|
||||
Assert.False(memory.TryAllocateGuestMemory(1, 1, out _));
|
||||
|
||||
Assert.True(memory.TryFreeGuestMemory(second));
|
||||
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var reused));
|
||||
Assert.Equal(second, reused);
|
||||
|
||||
Assert.True(memory.TryFreeGuestMemory(first));
|
||||
Assert.True(memory.TryFreeGuestMemory(reused));
|
||||
Assert.True(memory.TryFreeGuestMemory(third));
|
||||
Assert.False(memory.TryFreeGuestMemory(third));
|
||||
|
||||
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize, 0x1000, out var coalesced));
|
||||
Assert.Equal(first, coalesced);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SegmentProtectionIsAppliedInContiguousRuns()
|
||||
{
|
||||
const ulong pageSize = 0x1000;
|
||||
using var host = new RecordingHostMemory(3 * pageSize);
|
||||
using var memory = new PhysicalVirtualMemory(host);
|
||||
|
||||
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Read);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
(host.Address, 3 * pageSize, HostPageProtection.ReadWrite),
|
||||
(host.Address, 3 * pageSize, HostPageProtection.ReadOnly),
|
||||
],
|
||||
host.ProtectionCalls);
|
||||
|
||||
host.ProtectionCalls.Clear();
|
||||
memory.Map(host.Address + pageSize, pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Write);
|
||||
host.ProtectionCalls.Clear();
|
||||
|
||||
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Execute);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
(host.Address, 3 * pageSize, HostPageProtection.ReadWriteExecute),
|
||||
(host.Address, pageSize, HostPageProtection.ReadExecute),
|
||||
(host.Address + pageSize, pageSize, HostPageProtection.ReadWriteExecute),
|
||||
(host.Address + (2 * pageSize), pageSize, HostPageProtection.ReadExecute),
|
||||
],
|
||||
host.ProtectionCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public unsafe void GetPointerCommitsLazyPageBeforeReturningIt()
|
||||
{
|
||||
const ulong address = 0x00005000_0000_0000;
|
||||
const ulong pageSize = 0x1000;
|
||||
using var host = new LazyHostMemory(address);
|
||||
using var memory = new PhysicalVirtualMemory(host);
|
||||
memory.AllocateAt(address, (4UL << 30) + pageSize, executable: false, allowAlternative: false);
|
||||
host.CommitCalls.Clear();
|
||||
|
||||
var pointer = memory.GetPointer(address + 0x123);
|
||||
|
||||
Assert.Equal(address + 0x123, (ulong)pointer);
|
||||
Assert.Equal([(address, pageSize, HostPageProtection.ReadWrite)], host.CommitCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public unsafe void GetPointerReturnsNullWhenLazyCommitFails()
|
||||
{
|
||||
const ulong address = 0x00005000_0000_0000;
|
||||
using var host = new LazyHostMemory(address);
|
||||
using var memory = new PhysicalVirtualMemory(host);
|
||||
memory.AllocateAt(address, (4UL << 30) + 0x1000, executable: false, allowAlternative: false);
|
||||
host.CommitCalls.Clear();
|
||||
host.CommitSucceeds = false;
|
||||
|
||||
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
|
||||
}
|
||||
|
||||
private sealed class FakeHostMemory : IHostMemory
|
||||
{
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
desiredAddress != 0 ? desiredAddress : 0x00007000_0000_0000;
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
Allocate(desiredAddress, size, protection);
|
||||
|
||||
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
||||
|
||||
public bool Free(ulong address) => true;
|
||||
|
||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Query(ulong address, out HostRegionInfo info)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong address, ulong size)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingHostMemory : IHostMemory, IDisposable
|
||||
{
|
||||
private readonly nint _allocation;
|
||||
private bool _freed;
|
||||
|
||||
public RecordingHostMemory(ulong size)
|
||||
{
|
||||
_allocation = System.Runtime.InteropServices.Marshal.AllocHGlobal(checked((nint)(size + 0xFFF)));
|
||||
Address = (unchecked((ulong)_allocation) + 0xFFF) & ~0xFFFUL;
|
||||
}
|
||||
|
||||
public ulong Address { get; }
|
||||
|
||||
public List<(ulong Address, ulong Size, HostPageProtection Protection)> ProtectionCalls { get; } = [];
|
||||
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
desiredAddress == Address ? Address : 0;
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
||||
|
||||
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
||||
|
||||
public bool Free(ulong address)
|
||||
{
|
||||
if (address != Address || _freed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
|
||||
_freed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||
{
|
||||
ProtectionCalls.Add((address, size, protection));
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Query(ulong address, out HostRegionInfo info)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong address, ulong size)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_freed)
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
|
||||
_freed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
|
||||
{
|
||||
public bool CommitSucceeds { get; set; } = true;
|
||||
|
||||
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
|
||||
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
desiredAddress == address ? address : 0;
|
||||
|
||||
public bool Commit(ulong commitAddress, ulong size, HostPageProtection protection)
|
||||
{
|
||||
CommitCalls.Add((commitAddress, size, protection));
|
||||
return CommitSucceeds;
|
||||
}
|
||||
|
||||
public bool Free(ulong freeAddress) => freeAddress == address;
|
||||
|
||||
public bool Protect(ulong protectAddress, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ProtectRaw(ulong protectAddress, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Query(ulong queryAddress, out HostRegionInfo info)
|
||||
{
|
||||
var pageAddress = queryAddress & ~0xFFFUL;
|
||||
info = new HostRegionInfo(
|
||||
pageAddress,
|
||||
address,
|
||||
0x1000,
|
||||
HostRegionState.Reserved,
|
||||
0,
|
||||
HostPageProtection.NoAccess,
|
||||
0,
|
||||
0);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong flushAddress, ulong size)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Memory;
|
||||
|
||||
public sealed class VirtualMemoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void OutOfOrderMappingsRemainSortedAndResolveAtBoundaries()
|
||||
{
|
||||
var memory = new VirtualMemory();
|
||||
memory.Map(0x3000, 0x100, 0, [3], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||
memory.Map(0x1000, 0x100, 0, [1], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||
memory.Map(0x2000, 0x100, 0, [2], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||
|
||||
Assert.Equal([0x1000UL, 0x2000UL, 0x3000UL], memory.SnapshotRegions().Select(region => region.VirtualAddress));
|
||||
|
||||
Span<byte> value = stackalloc byte[1];
|
||||
Assert.True(memory.TryRead(0x1000, value));
|
||||
Assert.Equal(1, value[0]);
|
||||
Assert.True(memory.TryRead(0x2000, value));
|
||||
Assert.Equal(2, value[0]);
|
||||
Assert.True(memory.TryRead(0x3000, value));
|
||||
Assert.Equal(3, value[0]);
|
||||
Assert.False(memory.TryRead(0x1100, value));
|
||||
Assert.False(memory.TryRead(0x0FFF, value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MappingRejectsOverlapWithEitherNeighbor()
|
||||
{
|
||||
var memory = new VirtualMemory();
|
||||
memory.Map(0x2000, 0x100, 0, [], ProgramHeaderFlags.Read);
|
||||
memory.Map(0x4000, 0x100, 0, [], ProgramHeaderFlags.Read);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
memory.Map(0x1FFF, 2, 0, [], ProgramHeaderFlags.Read));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
memory.Map(0x3FFF, 2, 0, [], ProgramHeaderFlags.Read));
|
||||
|
||||
memory.Map(0x2100, 0x1F00, 0, [], ProgramHeaderFlags.Read);
|
||||
Assert.Equal(3, memory.SnapshotRegions().Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadAndWriteSpanAdjacentRegions()
|
||||
{
|
||||
var memory = new VirtualMemory();
|
||||
const ProgramHeaderFlags protection = ProgramHeaderFlags.Read | ProgramHeaderFlags.Write;
|
||||
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], protection);
|
||||
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], protection);
|
||||
|
||||
Span<byte> read = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(0x1002, read));
|
||||
Assert.Equal([3, 4, 5, 6], read.ToArray());
|
||||
|
||||
Assert.True(memory.TryWrite(0x1002, [9, 10, 11, 12]));
|
||||
Assert.True(memory.TryRead(0x1000, read));
|
||||
Assert.Equal([1, 2, 9, 10], read.ToArray());
|
||||
Assert.True(memory.TryRead(0x1004, read));
|
||||
Assert.Equal([11, 12, 7, 8], read.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AccessRequiresPermissionAcrossEntireRange()
|
||||
{
|
||||
var memory = new VirtualMemory();
|
||||
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], ProgramHeaderFlags.Read);
|
||||
memory.Map(0x2000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Execute);
|
||||
memory.Map(0x3000, 4, 0, [], ProgramHeaderFlags.Write);
|
||||
|
||||
Assert.False(memory.TryWrite(0x1002, [9, 9, 9, 9]));
|
||||
Span<byte> unchanged = stackalloc byte[4];
|
||||
Assert.True(memory.TryRead(0x1000, unchanged));
|
||||
Assert.Equal([1, 2, 3, 4], unchanged.ToArray());
|
||||
|
||||
Assert.False(memory.TryRead(0x2000, unchanged));
|
||||
Assert.False(memory.TryWrite(0x2000, [9]));
|
||||
Assert.False(memory.TryRead(0x3000, unchanged));
|
||||
Assert.True(memory.TryWrite(0x3000, [9]));
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
|
||||
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -149,6 +149,15 @@
|
||||
"xunit.extensibility.core": "[2.9.3]"
|
||||
}
|
||||
},
|
||||
"sharpemu.core": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Iced": "[1.21.0, )",
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"SharpEmu.Libs": "[1.0.0, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
@@ -168,6 +177,12 @@
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.21.0, )",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
|
||||
Reference in New Issue
Block a user