diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index b081dc0..596609c 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -633,25 +633,56 @@ public sealed partial class DirectExecutionBackend "ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult "rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer "eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer - "qvMUCyyaCSI"; // sceKernelAprSubmitCommandBufferAndGetId + "qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId + "Q2V+iqvjgC0" or // vsnprintf + "q1cHNfGycLI" or // scePadRead + "xk0AcarP3V4" or // scePadOpen + "yH17Q6NWtVg" or // sceUserServiceGetEvent + "D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting + "K-jXhbt2gn4"; // scePthreadMutexTrylock private bool ShouldLogImportResult(string nid, OrbisGen2Result result) { + var resultValue = unchecked((int)result); + if (resultValue > 0) + { + return false; + } + var expectedFileProbeMiss = result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND && IsExpectedFileProbeNotFoundNid(nid); var expectedTimedWaitTimeout = string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) && unchecked((int)result) == 60; + var expectedEqueueTimeout = + string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) && + result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; var expectedMutexTrylockBusy = string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) && result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY; - if (!expectedFileProbeMiss && !expectedTimedWaitTimeout && !expectedMutexTrylockBusy) + var expectedUserServiceNoEvent = + string.Equals(nid, "yH17Q6NWtVg", StringComparison.Ordinal) && + resultValue == unchecked((int)0x80960007); + var expectedPrivacyInvalidParameter = + string.Equals(nid, "D-CzAxQL0XI", StringComparison.Ordinal) && + resultValue == unchecked((int)0x80960009); + if (!expectedFileProbeMiss && + !expectedTimedWaitTimeout && + !expectedEqueueTimeout && + !expectedMutexTrylockBusy && + !expectedUserServiceNoEvent && + !expectedPrivacyInvalidParameter) { return true; } - var key = nid + "\0" + (int)result; + if (!ShouldLogExpectedImportResults()) + { + return false; + } + + var key = nid + "\0" + resultValue; int count; lock (_importResultLogSampleGate) { @@ -663,6 +694,12 @@ public sealed partial class DirectExecutionBackend return count <= 8 || count % 10000 == 0; } + private static bool ShouldLogExpectedImportResults() => + string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_LOG_EXPECTED_IMPORT_RESULTS"), + "1", + StringComparison.Ordinal); + private static bool IsExpectedFileProbeNotFoundNid(string nid) => nid is "eV9wAD2riIA" or // sceKernelStat diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index 7fa17e8..7d7b4b2 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -11,7 +11,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA { private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion); private readonly object _guestAllocationGate = new(); + private readonly object _allocationSearchHintGate = new(); private readonly List _regions = new(); + private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new(); private readonly Dictionary _pageProtections = new(); private bool _disposed; private const ulong PageSize = 0x1000; @@ -19,7 +21,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA private const ulong GuestAllocationArenaSize = 0x0100_0000; private const ulong GuestAllocationArenaStartOffset = PageSize; private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB - private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB + private const ulong FullCommitRegionLimit = 4UL << 30; + private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB private const uint MEM_COMMIT = 0x1000; @@ -35,6 +38,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA private ulong _guestAllocationArenaBase; private ulong _guestAllocationOffset; + private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes(); [DllImport("kernel32.dll", SetLastError = true)] private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect); @@ -96,7 +100,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA } var allocationKind = executable ? "executable memory" : "data memory"; - Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)"); + TraceVmem($"Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)"); return true; } @@ -110,7 +114,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE; var allocationType = MEM_COMMIT | MEM_RESERVE; var reservedOnly = false; - var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold; + var preferReserveOnly = !executable && + alignedSize >= LargeDataReserveThreshold && + alignedSize > FullCommitRegionLimit; void* result = null; if (preferReserveOnly) @@ -139,7 +145,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)"); } - Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address..."); + TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address..."); result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection); if (result == null) @@ -193,12 +199,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA lazyPrimeState = committedBytes == primeBytes ? $"ok:{committedBytes:X}" : $"partial:{committedBytes:X}/{primeBytes:X}"; - Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)"); + TraceVmem($"Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)"); } else { lazyPrimeState = $"fail:{primeBytes:X}"; - Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit"); + TraceVmem($"Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit"); } } else @@ -227,7 +233,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA var allocationKind = reservedOnly ? "reserved data memory (lazy commit)" : (executable ? "executable memory" : "data memory"); - Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}"); + TraceVmem($"Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}"); return actualAddress; } @@ -247,7 +253,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA var alignedSize = AlignUp(size, PageSize); var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment); - var cursor = AlignUp(desiredAddress, effectiveAlignment); + var requestedCursor = AlignUp(desiredAddress, effectiveAlignment); + var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable); for (var attempt = 0; attempt < 0x10000; attempt++) { @@ -267,6 +274,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false); if (actualAddress == cursor) { + UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize); return true; } @@ -334,6 +342,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA } _regions.Clear(); _pageProtections.Clear(); + lock (_allocationSearchHintGate) + { + _allocationSearchHints.Clear(); + } } finally { @@ -390,7 +402,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA ApplySegmentProtection(mapStart, mapEnd, protection); - Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})"); + TraceVmem($"Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})"); } finally { @@ -793,6 +805,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA foreach (var region in _regions) { var regionEnd = region.VirtualAddress + region.Size; + if (region.VirtualAddress >= end) + { + break; + } + + if (regionEnd <= address) + { + continue; + } + if (address < regionEnd && region.VirtualAddress < end) { overlapEnd = Math.Max(overlapEnd, regionEnd); @@ -807,6 +829,37 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA return overlapEnd != 0; } + private ulong GetAllocationSearchCursor( + ulong desiredAddress, + ulong requestedCursor, + ulong alignment, + bool executable) + { + lock (_allocationSearchHintGate) + { + var key = (desiredAddress, alignment, executable); + if (_allocationSearchHints.TryGetValue(key, out var hintedCursor) && + hintedCursor > requestedCursor) + { + return AlignUp(hintedCursor, alignment); + } + } + + return requestedCursor; + } + + private void UpdateAllocationSearchCursor( + ulong desiredAddress, + ulong alignment, + bool executable, + ulong nextCursor) + { + lock (_allocationSearchHintGate) + { + _allocationSearchHints[(desiredAddress, alignment, executable)] = AlignUp(nextCursor, alignment); + } + } + private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset) { offset = 0; @@ -975,6 +1028,29 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA return checked((value + mask) & ~mask); } + private static ulong ResolveLazyReservePrimeBytes() + { + var configured = Environment.GetEnvironmentVariable("SHARPEMU_LAZY_RESERVE_PRIME_MB"); + if (ulong.TryParse(configured, out var megabytes)) + { + return megabytes == 0 + ? 0 + : checked(Math.Min(megabytes, 4096UL) * 1024UL * 1024UL); + } + + return DefaultLazyReservePrimeBytes; + } + + private static void TraceVmem(string message) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal)) + { + return; + } + + Console.Error.WriteLine($"[VMEM] {message}"); + } + public void Dispose() { if (!_disposed) diff --git a/src/SharpEmu.Libs/Ampr/AmprExports.cs b/src/SharpEmu.Libs/Ampr/AmprExports.cs index 81f1a97..0d13c51 100644 --- a/src/SharpEmu.Libs/Ampr/AmprExports.cs +++ b/src/SharpEmu.Libs/Ampr/AmprExports.cs @@ -24,6 +24,7 @@ public static class AmprExports private const uint KernelEventQueueRecordType = 2; private const uint WriteAddressRecordType = 3; private static readonly ConcurrentDictionary _commandBuffers = new(); + private static readonly ConcurrentDictionary> _hostFileCache = new(StringComparer.OrdinalIgnoreCase); private static readonly bool _traceAmpr = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal); private static readonly bool _traceAmprReads = @@ -37,6 +38,23 @@ public static class AmprExports public ulong WriteOffset; } + private sealed class CachedHostFile + { + public CachedHostFile(string path) + { + Stream = new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete, + bufferSize: 1024 * 1024, + FileOptions.RandomAccess); + } + + public object Gate { get; } = new(); + public FileStream Stream { get; } + } + [SysAbiExport( Nid = "8aI7R7WaOlc", ExportName = "sceAmprCommandBufferConstructor", @@ -249,7 +267,7 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath)) + if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath)) { TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; @@ -634,24 +652,43 @@ public static class AmprExports try { - using var stream = new FileStream( - hostPath, - FileMode.Open, - FileAccess.Read, - FileShare.ReadWrite | FileShare.Delete, - ChunkSize, - FileOptions.SequentialScan); - if (fileOffset >= (ulong)stream.Length) + if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult)) + { + return openResult; + } + + long fileLength; + lock (cachedFile.Gate) + { + fileLength = cachedFile.Stream.Length; + } + + if (fileOffset >= (ulong)fileLength) { return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - stream.Position = unchecked((long)fileOffset); - while (bytesRead < size) { + if (bytesRead > ulong.MaxValue - fileOffset) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + var absoluteOffset = fileOffset + bytesRead; + if (absoluteOffset > long.MaxValue) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead); - var read = stream.Read(buffer, 0, request); + int read; + lock (cachedFile.Gate) + { + cachedFile.Stream.Position = unchecked((long)absoluteOffset); + read = cachedFile.Stream.Read(buffer, 0, request); + } + if (read <= 0) { break; @@ -681,6 +718,44 @@ public static class AmprExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + private static bool TryGetCachedHostFile(string hostPath, out CachedHostFile file, out int result) + { + file = null!; + result = (int)OrbisGen2Result.ORBIS_GEN2_OK; + + string cachePath; + try + { + cachePath = Path.GetFullPath(hostPath); + } + catch + { + cachePath = hostPath; + } + + var lazy = _hostFileCache.GetOrAdd( + cachePath, + static path => new Lazy(() => new CachedHostFile(path), isThreadSafe: true)); + + try + { + file = lazy.Value; + return true; + } + catch (UnauthorizedAccessException) + { + _hostFileCache.TryRemove(cachePath, out _); + result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED; + return false; + } + catch (IOException) + { + _hostFileCache.TryRemove(cachePath, out _); + result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + return false; + } + } + private static bool AppendReadFileRecord( CpuContext ctx, ulong commandBuffer, diff --git a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs index 4d41e6d..b23d0d6 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventQueueCompatExports.cs @@ -276,6 +276,22 @@ public static class KernelEventQueueCompatExports var outCountAddress = ctx[CpuRegister.Rcx]; var timeoutAddress = ctx[CpuRegister.R8]; + if (!IsValidEqueue(handle)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; + } + + if (eventsAddress == 0 || eventCapacity < 1) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; + } + + uint timeoutUsec = 0; + if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity); if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount)) { @@ -300,6 +316,41 @@ public static class KernelEventQueueCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + if (timeoutAddress != 0 && ctx.TryReadUInt64(timeoutAddress, out var timeoutRaw)) + { + var timeoutMicros = timeoutRaw & 0xFFFF_FFFFUL; + var deadline = Environment.TickCount64 + + Math.Max(1L, (long)Math.Min(timeoutMicros / 1000, int.MaxValue)); + lock (_eventQueueGate) + { + while (!HasPendingEvents(handle)) + { + var remaining = deadline - Environment.TickCount64; + if (remaining <= 0) + { + break; + } + + Monitor.Wait(_eventQueueGate, (int)Math.Min(remaining, 100)); + } + } + + deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity); + if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (deliveredCount > 0) + { + TraceEventQueue(ctx, "wait-timed-deliver", handle); + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + TraceEventQueue(ctx, "wait-timeout", handle); + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; + } + TraceEventQueue(ctx, "wait", handle); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -330,6 +381,7 @@ public static class KernelEventQueueCompatExports queue.AddLast(queuedEvent); queued = true; + Monitor.PulseAll(_eventQueueGate); } if (queued) @@ -494,7 +546,9 @@ public static class KernelEventQueueCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } - return (int)OrbisGen2Result.ORBIS_GEN2_OK; + return deliveredCount > 0 + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; } private static bool HasPendingEvents(ulong handle) @@ -612,4 +666,17 @@ public static class KernelEventQueueCompatExports BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); return ctx.Memory.TryWrite(address, buffer); } + + private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt32LittleEndian(buffer); + return true; + } } diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 16769f5..5fc37f8 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -5,9 +5,9 @@ using SharpEmu.HLE; using SharpEmu.Libs.Ampr; using System.Buffers; using System.Buffers.Binary; +using System.Collections.Concurrent; using System.Text; using System.Threading; -using System.Reflection; using System.Runtime.InteropServices; using System.Linq; using System.Globalization; @@ -108,6 +108,7 @@ public static class KernelMemoryCompatExports private static readonly Dictionary _guestMounts = new(StringComparer.OrdinalIgnoreCase); private static readonly HashSet _tracedStatResults = new(StringComparer.Ordinal); private static readonly HashSet _negativeStatCache = new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase); private static long _nextFileDescriptor = 2; private static ulong _nextPhysicalAddress; private static ulong _nextVirtualAddress; @@ -1342,6 +1343,7 @@ public static class KernelMemoryCompatExports if (IsMutatingOpen(flags)) { InvalidateNegativeStatCacheForPathAndAncestors(guestPath); + InvalidateAprFileSizeCache(hostPath); } LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}"); @@ -1550,6 +1552,7 @@ public static class KernelMemoryCompatExports File.Delete(hostPath); InvalidateNegativeStatCacheForPathAndAncestors(guestPath); + InvalidateAprFileSizeCache(hostPath); AddNegativeStatCacheForGuestPath(guestPath); LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'"); ctx[CpuRegister.Rax] = 0; @@ -2596,8 +2599,11 @@ public static class KernelMemoryCompatExports var length = ctx[CpuRegister.Rsi]; var protection = unchecked((int)ctx[CpuRegister.Rdx]); var flags = ctx[CpuRegister.Rcx]; - Console.Error.WriteLine( - $"[LOADER][TRACE] map_flexible: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16}"); + if (ShouldTraceDirectMemory()) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] map_flexible: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16}"); + } if (inOutAddressPointer == 0 || length == 0) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; @@ -2627,8 +2633,11 @@ public static class KernelMemoryCompatExports : AllocateMappedGuestAddress(ctx, length, 0x1000UL); } - Console.Error.WriteLine( - $"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}"); + if (ShouldTraceDirectMemory()) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}"); + } if (mappedAddress == 0) { @@ -3973,118 +3982,17 @@ public static class KernelMemoryCompatExports ulong alignment, out ulong mappedAddress) { - mappedAddress = 0; - if (length == 0) - { - return false; - } - - try - { - object memoryObject = ctx.Memory; - MethodInfo? allocateAt = null; - MethodInfo? allocateAtOrAbove = null; - var allocateAtHasAllowAlternativeArg = false; - for (var depth = 0; depth < 4; depth++) - { - foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) - { - var parameters = candidate.GetParameters(); - if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) && - parameters.Length == 5 && - parameters[0].ParameterType == typeof(ulong) && - parameters[1].ParameterType == typeof(ulong) && - parameters[2].ParameterType == typeof(bool) && - parameters[3].ParameterType == typeof(ulong) && - parameters[4].ParameterType == typeof(ulong).MakeByRefType()) - { - allocateAtOrAbove = candidate; - } - else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal)) - { - if (parameters.Length == 3 && - parameters[0].ParameterType == typeof(ulong) && - parameters[1].ParameterType == typeof(ulong) && - parameters[2].ParameterType == typeof(bool)) - { - allocateAt = candidate; - allocateAtHasAllowAlternativeArg = false; - } - else if (parameters.Length == 4 && - parameters[0].ParameterType == typeof(ulong) && - parameters[1].ParameterType == typeof(ulong) && - parameters[2].ParameterType == typeof(bool) && - parameters[3].ParameterType == typeof(bool)) - { - allocateAt = candidate; - allocateAtHasAllowAlternativeArg = true; - } - } - - if (allocateAtOrAbove is not null && allocateAt is not null) - { - break; - } - } - - if (allocateAtOrAbove is not null || allocateAt is not null) - { - break; - } - - var innerProperty = memoryObject.GetType().GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance); - if (innerProperty is null) - { - break; - } - - var innerValue = innerProperty.GetValue(memoryObject); - if (innerValue is null || ReferenceEquals(innerValue, memoryObject)) - { - break; - } - - memoryObject = innerValue; - } - - var executable = (protection & OrbisProtCpuExec) != 0; - if (allocateAtOrAbove is not null) - { - var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL }; - var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs); - if (searchResult is bool trueValue && trueValue && - searchArgs[4] is ulong searchedAddress && searchedAddress != 0) - { - mappedAddress = searchedAddress; - return true; - } - } - - if (allocateAt is null) - { - Console.Error.WriteLine($"[LOADER][TRACE] reserve range: AllocateAt missing on {ctx.Memory.GetType().FullName}"); - return false; - } - - var invokeArgs = allocateAtHasAllowAlternativeArg - ? new object[] { desiredAddress, length, executable, false } - : new object[] { desiredAddress, length, executable }; - var result = allocateAt.Invoke(memoryObject, invokeArgs); - if (result is not ulong allocated || allocated == 0) - { - var resultType = result?.GetType().FullName ?? "null"; - Console.Error.WriteLine($"[LOADER][TRACE] reserve range: AllocateAt returned {resultType} value={result ?? "null"}"); - return false; - } - - mappedAddress = allocated; - return true; - } - catch - { - Console.Error.WriteLine("[LOADER][TRACE] reserve range threw while invoking AllocateAt"); - return false; - } + var executable = (protection & OrbisProtCpuExec) != 0; + return KernelVirtualRangeAllocator.TryReserve( + ctx, + desiredAddress, + length, + executable, + alignment, + allowSearch: true, + allowAllocateAtAlternative: false, + "reserve range", + out mappedAddress); } private static bool IsMappedGuestRangeAvailable( @@ -4237,6 +4145,20 @@ public static class KernelMemoryCompatExports var app0Root = ResolveApp0Root(); if (!string.IsNullOrWhiteSpace(app0Root)) { + if (string.Equals(guestPath, "$", StringComparison.Ordinal) || + string.Equals(guestPath, "$/", StringComparison.Ordinal) || + string.Equals(guestPath, "$\\", StringComparison.Ordinal)) + { + return app0Root; + } + + if (guestPath.StartsWith("$/", StringComparison.Ordinal) || + guestPath.StartsWith("$\\", StringComparison.Ordinal)) + { + var relative = NormalizeMountRelativePath(guestPath[2..]); + return Path.Combine(app0Root, relative); + } + if (string.Equals(guestPath, "/app0", StringComparison.OrdinalIgnoreCase) || string.Equals(guestPath, "app0", StringComparison.OrdinalIgnoreCase)) { @@ -5899,22 +5821,40 @@ public static class KernelMemoryCompatExports private static bool TryGetAprFileSize(string hostPath, out ulong size) { size = 0; + + string cachePath; try { - var fileInfo = new FileInfo(hostPath); + cachePath = Path.GetFullPath(hostPath); + } + catch + { + cachePath = hostPath; + } + + if (_aprFileSizeCache.TryGetValue(cachePath, out size)) + { + return true; + } + + try + { + var fileInfo = new FileInfo(cachePath); if (fileInfo.Exists) { var length = fileInfo.Length; size = length < 0 ? 0UL : unchecked((ulong)length); + _aprFileSizeCache.TryAdd(cachePath, size); return true; } - if (!new DirectoryInfo(hostPath).Exists) + if (!new DirectoryInfo(cachePath).Exists) { return false; } size = 65536; + _aprFileSizeCache.TryAdd(cachePath, size); return true; } catch @@ -6213,6 +6153,20 @@ public static class KernelMemoryCompatExports } } + private static void InvalidateAprFileSizeCache(string hostPath) + { + try + { + hostPath = Path.GetFullPath(hostPath); + } + catch + { + // The cache key remains the original path when normalization fails. + } + + _aprFileSizeCache.TryRemove(hostPath, out _); + } + private static string PreviewIoBytes(byte[] buffer, int count, int maxBytes) { if (count <= 0) diff --git a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs index d210bc0..2e3dc45 100644 --- a/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelRuntimeCompatExports.cs @@ -7,7 +7,6 @@ using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Intrinsics.X86; using System.Security.Cryptography; @@ -87,11 +86,11 @@ public static class KernelRuntimeCompatExports if (micros < 1000) { - // Guest worker pools use usleep(1) as a polling backoff. Periodically - // relinquish a full host time slice so spin workers cannot starve producers. - if ((++_shortUsleepCount & 31) == 0) + // Guest worker pools use usleep(1) as a polling backoff. Do not turn + // PS microsecond waits into Windows millisecond sleeps on hot paths. + if ((++_shortUsleepCount & 255) == 0) { - Thread.Sleep(1); + Thread.Sleep(0); } else { @@ -739,8 +738,11 @@ public static class KernelRuntimeCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; } - Console.Error.WriteLine( - $"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16} flags=0x{flags:X8} align=0x{effectiveAlignment:X16}"); + if (ShouldTraceVirtualMemory()) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16} flags=0x{flags:X8} align=0x{effectiveAlignment:X16}"); + } if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress)) { @@ -1733,117 +1735,16 @@ public static class KernelRuntimeCompatExports bool allowSearch, out ulong mappedAddress) { - mappedAddress = 0; - if (length == 0) - { - return false; - } - - try - { - object memoryObject = ctx.Memory; - MethodInfo? allocateAt = null; - MethodInfo? allocateAtOrAbove = null; - var allocateAtHasAllowAlternativeArg = false; - for (var depth = 0; depth < 4; depth++) - { - foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) - { - var parameters = candidate.GetParameters(); - if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) && - parameters.Length == 5 && - parameters[0].ParameterType == typeof(ulong) && - parameters[1].ParameterType == typeof(ulong) && - parameters[2].ParameterType == typeof(bool) && - parameters[3].ParameterType == typeof(ulong) && - parameters[4].ParameterType == typeof(ulong).MakeByRefType()) - { - allocateAtOrAbove = candidate; - } - else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal)) - { - if (parameters.Length == 3 && - parameters[0].ParameterType == typeof(ulong) && - parameters[1].ParameterType == typeof(ulong) && - parameters[2].ParameterType == typeof(bool)) - { - allocateAt = candidate; - allocateAtHasAllowAlternativeArg = false; - } - else if (parameters.Length == 4 && - parameters[0].ParameterType == typeof(ulong) && - parameters[1].ParameterType == typeof(ulong) && - parameters[2].ParameterType == typeof(bool) && - parameters[3].ParameterType == typeof(bool)) - { - allocateAt = candidate; - allocateAtHasAllowAlternativeArg = true; - } - } - - if (allocateAtOrAbove is not null && allocateAt is not null) - { - break; - } - } - - if (allocateAtOrAbove is not null || allocateAt is not null) - { - break; - } - - var innerProperty = memoryObject.GetType().GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance); - if (innerProperty is null) - { - break; - } - - var innerValue = innerProperty.GetValue(memoryObject); - if (innerValue is null || ReferenceEquals(innerValue, memoryObject)) - { - break; - } - - memoryObject = innerValue; - } - - if (allowSearch && allocateAtOrAbove is not null) - { - var searchArgs = new object[] { desiredAddress, length, false, alignment, 0UL }; - var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs); - if (searchResult is bool trueValue && trueValue && - searchArgs[4] is ulong searchedAddress && searchedAddress != 0) - { - mappedAddress = searchedAddress; - return true; - } - } - - if (allocateAt is null) - { - Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt missing on {ctx.Memory.GetType().FullName}"); - return false; - } - - var invokeArgs = allocateAtHasAllowAlternativeArg - ? new object[] { desiredAddress, length, false, allowSearch } - : new object[] { desiredAddress, length, false }; - var result = allocateAt.Invoke(memoryObject, invokeArgs); - if (result is not ulong allocated || allocated == 0) - { - var resultType = result?.GetType().FullName ?? "null"; - Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt returned {resultType} value={result ?? "null"}"); - return false; - } - - mappedAddress = allocated; - return true; - } - catch - { - Console.Error.WriteLine("[LOADER][TRACE] reserve_virtual_range: AllocateAt invocation threw"); - return false; - } + return KernelVirtualRangeAllocator.TryReserve( + ctx, + desiredAddress, + length, + executable: false, + alignment, + allowSearch, + allowAllocateAtAlternative: allowSearch, + "reserve_virtual_range", + out mappedAddress); } private static ulong AlignUp(ulong value, ulong alignment) @@ -1856,4 +1757,9 @@ public static class KernelRuntimeCompatExports var mask = alignment - 1; return (value + mask) & ~mask; } + + private static bool ShouldTraceVirtualMemory() + { + return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIRTUAL_MEMORY"), "1", StringComparison.Ordinal); + } } diff --git a/src/SharpEmu.Libs/Kernel/KernelVirtualRangeAllocator.cs b/src/SharpEmu.Libs/Kernel/KernelVirtualRangeAllocator.cs new file mode 100644 index 0000000..b79e66b --- /dev/null +++ b/src/SharpEmu.Libs/Kernel/KernelVirtualRangeAllocator.cs @@ -0,0 +1,159 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System; +using System.Collections.Concurrent; +using System.Reflection; + +namespace SharpEmu.Libs.Kernel; + +internal static class KernelVirtualRangeAllocator +{ + private static readonly ConcurrentDictionary _accessors = new(); + + public static bool TryReserve( + CpuContext ctx, + ulong desiredAddress, + ulong length, + bool executable, + ulong alignment, + bool allowSearch, + bool allowAllocateAtAlternative, + string traceName, + out ulong mappedAddress) + { + mappedAddress = 0; + if (length == 0) + { + return false; + } + + try + { + if (!TryResolveAccessor(ctx.Memory, out var target, out var accessor)) + { + Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}"); + return false; + } + + if (allowSearch && accessor.AllocateAtOrAbove is not null) + { + var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL }; + var searchResult = accessor.AllocateAtOrAbove.Invoke(target, searchArgs); + if (searchResult is bool trueValue && trueValue && + searchArgs[4] is ulong searchedAddress && searchedAddress != 0) + { + mappedAddress = searchedAddress; + return true; + } + } + + if (accessor.AllocateAt is null) + { + Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {target.GetType().FullName}"); + return false; + } + + var invokeArgs = accessor.AllocateAtHasAllowAlternativeArg + ? new object[] { desiredAddress, length, executable, allowAllocateAtAlternative } + : new object[] { desiredAddress, length, executable }; + var result = accessor.AllocateAt.Invoke(target, invokeArgs); + if (result is not ulong allocated || allocated == 0) + { + var resultType = result?.GetType().FullName ?? "null"; + Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {resultType} value={result ?? "null"}"); + return false; + } + + mappedAddress = allocated; + return true; + } + catch + { + Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt invocation threw"); + return false; + } + } + + private static bool TryResolveAccessor(object rootMemory, out object target, out Accessor accessor) + { + target = rootMemory; + accessor = default; + + for (var depth = 0; depth < 4; depth++) + { + accessor = _accessors.GetOrAdd(target.GetType(), DiscoverAccessor); + if (accessor.AllocateAt is not null || accessor.AllocateAtOrAbove is not null) + { + return true; + } + + if (accessor.InnerProperty is null) + { + break; + } + + var innerValue = accessor.InnerProperty.GetValue(target); + if (innerValue is null || ReferenceEquals(innerValue, target)) + { + break; + } + + target = innerValue; + } + + return false; + } + + private static Accessor DiscoverAccessor(Type type) + { + MethodInfo? allocateAt = null; + MethodInfo? allocateAtOrAbove = null; + var allocateAtHasAllowAlternativeArg = false; + + foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) + { + var parameters = candidate.GetParameters(); + if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) && + parameters.Length == 5 && + parameters[0].ParameterType == typeof(ulong) && + parameters[1].ParameterType == typeof(ulong) && + parameters[2].ParameterType == typeof(bool) && + parameters[3].ParameterType == typeof(ulong) && + parameters[4].ParameterType == typeof(ulong).MakeByRefType()) + { + allocateAtOrAbove = candidate; + } + else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal)) + { + if (parameters.Length == 3 && + parameters[0].ParameterType == typeof(ulong) && + parameters[1].ParameterType == typeof(ulong) && + parameters[2].ParameterType == typeof(bool)) + { + allocateAt = candidate; + allocateAtHasAllowAlternativeArg = false; + } + else if (parameters.Length == 4 && + parameters[0].ParameterType == typeof(ulong) && + parameters[1].ParameterType == typeof(ulong) && + parameters[2].ParameterType == typeof(bool) && + parameters[3].ParameterType == typeof(bool)) + { + allocateAt = candidate; + allocateAtHasAllowAlternativeArg = true; + } + } + } + + var innerProperty = type.GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance); + return new Accessor(allocateAt, allocateAtOrAbove, allocateAtHasAllowAlternativeArg, innerProperty); + } + + private readonly record struct Accessor( + MethodInfo? AllocateAt, + MethodInfo? AllocateAtOrAbove, + bool AllocateAtHasAllowAlternativeArg, + PropertyInfo? InnerProperty); +} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 9d882f1..e2a2742 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -879,6 +879,8 @@ internal static unsafe class VulkanVideoPresenter private bool _swapchainRecreateDeferred; private bool _tracedPresentedSwapchain; private bool _swapchainReadbackPending; + private bool _deviceLost; + private bool _deviceLostLogged; private int _directPresentationCount; private readonly Dictionary _guestImages = new(); private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new(); @@ -2816,7 +2818,8 @@ internal static unsafe class VulkanVideoPresenter dstSelect: texture.DstSelect, out var view)) { - if (_tracedTextureCacheHits.Add( + if (ShouldTraceVulkanResources() && + _tracedTextureCacheHits.Add( (texture.Address, texture.Width, texture.Height, vkFormat))) { Console.Error.WriteLine( @@ -3395,7 +3398,8 @@ internal static unsafe class VulkanVideoPresenter out var memory); var size = (ulong)Math.Max(guestBuffer.Data.Length, sizeof(uint)); - if (_tracedGlobalBuffers.Add((guestBuffer.BaseAddress, guestBuffer.Data.Length))) + if (ShouldTraceVulkanResources() && + _tracedGlobalBuffers.Add((guestBuffer.BaseAddress, guestBuffer.Data.Length))) { Console.Error.WriteLine( $"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " + @@ -4030,6 +4034,11 @@ internal static unsafe class VulkanVideoPresenter private void ExecuteComputeDispatch(VulkanComputeGuestDispatch work) { + if (_deviceLost) + { + return; + } + if (AddressListContains("SHARPEMU_SKIP_COMPUTE_CS", work.ShaderAddress)) { TraceVulkanShader( @@ -4132,6 +4141,11 @@ internal static unsafe class VulkanVideoPresenter } catch (Exception exception) { + if (TryMarkDeviceLost(exception)) + { + return; + } + Console.Error.WriteLine( $"[LOADER][ERROR] Vulkan compute dispatch failed " + $"cs=0x{work.ShaderAddress:X16}: {exception.Message}"); @@ -4197,6 +4211,11 @@ internal static unsafe class VulkanVideoPresenter private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work) { + if (_deviceLost) + { + return; + } + var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType); if (format == Format.Undefined) { @@ -4354,6 +4373,11 @@ internal static unsafe class VulkanVideoPresenter } catch (Exception exception) { + if (TryMarkDeviceLost(exception)) + { + return; + } + if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) || !failedTarget.Initialized) { @@ -4809,7 +4833,10 @@ internal static unsafe class VulkanVideoPresenter } _commandBuffer = _presentationCommandBuffer; - CollectCompletedGuestSubmissions(waitForOldest: false); + if (!_deviceLost) + { + CollectCompletedGuestSubmissions(waitForOldest: false); + } var completedWork = 0; while (completedWork < MaxGuestWorkPerRender && @@ -5667,6 +5694,12 @@ internal static unsafe class VulkanVideoPresenter string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase); } + private static bool ShouldTraceVulkanResources() => + string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_LOG_VK_RESOURCES"), + "1", + StringComparison.Ordinal); + private void RecordTranslatedGraphicsPass( TranslatedDrawResources resources, RenderPass renderPass, @@ -6502,6 +6535,25 @@ internal static unsafe class VulkanVideoPresenter } } + private bool TryMarkDeviceLost(Exception exception) + { + if (!exception.Message.Contains(nameof(Result.ErrorDeviceLost), StringComparison.Ordinal)) + { + return false; + } + + _deviceLost = true; + if (!_deviceLostLogged) + { + _deviceLostLogged = true; + Console.Error.WriteLine( + "[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " + + exception.Message); + } + + return true; + } + private static void TraceVulkanShader(string message) { if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) &&