diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 3c36b57..65edb06 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -3978,6 +3978,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I public bool SupportsGuestContextTransfer => true; + // The handle and context this host thread registered last; a repeat registration (every + // scePthreadSelf on an external guest thread, tens of thousands a second) then skips the + // scheduler gate. + [ThreadStatic] private static ulong _registeredExternalHandle; + [ThreadStatic] private static CpuContext? _registeredExternalContext; public bool TryGetGuestThreadStackBounds( ulong threadHandle, out ulong stackBase, @@ -4012,6 +4017,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return; } + if (_registeredExternalHandle == threadHandle && ReferenceEquals(_registeredExternalContext, context)) + { + Volatile.Write(ref _currentExternalGuestThreadHandle, threadHandle); + return; + } + lock (_guestThreadGate) { _currentExternalGuestThreadHandle = threadHandle; @@ -4025,15 +4036,19 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { existing.Context = context; Volatile.Write(ref existing.HostThreadId, hostThreadId); - return; + } + else + { + _externalGuestThreads[threadHandle] = new ExternalGuestThreadState + { + Context = context, + Name = $"External-{threadHandle:X}", + HostThreadId = hostThreadId, + }; } - _externalGuestThreads[threadHandle] = new ExternalGuestThreadState - { - Context = context, - Name = $"External-{threadHandle:X}", - HostThreadId = hostThreadId, - }; + _registeredExternalHandle = threadHandle; + _registeredExternalContext = context; } } @@ -6382,19 +6397,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I return GuestNativeCallExitReason.Exception; } const uint stubSize = 512u; - void* ptr = VirtualAlloc(null, stubSize, 12288u, 4u); - if (ptr == null) + // The stub page and host-RSP slot are kept per host thread and nesting depth: allocating + // and releasing them on every resume cost an mmap/munmap pair and two acquisitions of the + // host-memory gate, which the GPU write tracker holds across its own protection changes. + var depth = _continuationDepth; + if (!TryAcquireContinuationStub(depth, stubSize, out var ptr, out var hostRspStorage, out reason)) { - reason = "failed to allocate executable memory for guest thread stub"; - return GuestNativeCallExitReason.Exception; - } - void* hostRspStorage = NativeMemory.AllocZeroed((nuint)HostStackStateBytes); - if (hostRspStorage == null) - { - VirtualFree(ptr, 0u, 32768u); - reason = "failed to allocate writable host-RSP storage for guest continuation stub"; return GuestNativeCallExitReason.Exception; } + _continuationDepth = depth + 1; var previousActiveBackend = _activeExecutionBackend; var previousActiveContext = _activeCpuContext; var previousSentinel = _activeEntryReturnSentinelRip; @@ -6468,8 +6479,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I reason = $"failed to patch guest continuation return slot at 0x{returnSlotAddress:X16}"; return GuestNativeCallExitReason.Exception; } - uint oldProtect = default(uint); - if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect)) + if (!ProtectContinuationStub(ptr, stubSize, executable: true)) { reason = "failed to seal guest continuation stub execute-read"; return GuestNativeCallExitReason.Exception; @@ -6529,11 +6539,85 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I previousForcedExit, previousYieldRequested, previousYieldReason); - NativeMemory.Free(hostRspStorage); - VirtualFree(ptr, 0u, 32768u); + _continuationDepth = depth; } } + [ThreadStatic] private static nint[]? _continuationStubPages; + [ThreadStatic] private static nint[]? _continuationHostRspSlots; + [ThreadStatic] private static int _continuationDepth; + + // Returns this thread's stub page for the given nesting depth, writable and with a zeroed + // host-RSP slot; the first use at a depth allocates them, and they live as long as the thread. + private unsafe bool TryAcquireContinuationStub(int depth, uint stubSize, out void* stub, out void* hostRspStorage, out string? reason) + { + reason = null; + stub = null; + hostRspStorage = null; + var pages = _continuationStubPages ??= new nint[4]; + var slots = _continuationHostRspSlots ??= new nint[4]; + if (depth >= pages.Length) + { + Array.Resize(ref pages, depth + 2); + Array.Resize(ref slots, depth + 2); + _continuationStubPages = pages; + _continuationHostRspSlots = slots; + } + + if (pages[depth] == 0) + { + var page = VirtualAlloc(null, stubSize, 12288u, 4u); + if (page == null) + { + reason = "failed to allocate executable memory for guest thread stub"; + return false; + } + + var slot = NativeMemory.AllocZeroed((nuint)HostStackStateBytes); + if (slot == null) + { + VirtualFree(page, 0u, 32768u); + reason = "failed to allocate writable host-RSP storage for guest continuation stub"; + return false; + } + + pages[depth] = (nint)page; + slots[depth] = (nint)slot; + } + else + { + if (!ProtectContinuationStub((void*)pages[depth], stubSize, executable: false)) + { + reason = "failed to reopen guest continuation stub for writing"; + return false; + } + + NativeMemory.Clear((void*)slots[depth], (nuint)HostStackStateBytes); + } + + stub = (void*)pages[depth]; + hostRspStorage = (void*)slots[depth]; + return true; + } + + // The stub page is emulator-private, so on POSIX its protection changes bypass the tracked + // host-memory layer and its gate. + private static unsafe bool ProtectContinuationStub(void* stub, uint stubSize, bool executable) + { + if (OperatingSystem.IsWindows()) + { + uint oldProtect = 0; + return VirtualProtect(stub, stubSize, executable ? 32u : 4u, &oldProtect); + } + + var pageSize = (nint)Environment.SystemPageSize; + var page = (nint)stub & ~(pageSize - 1); + return PosixMprotect(page, (nuint)pageSize, executable ? 1 | 4 : 1 | 2) == 0; + } + + [DllImport("libc", EntryPoint = "mprotect")] + private static extern int PosixMprotect(nint address, nuint length, int protection); + // The continuation trampoline is rebuilt on every blocked-thread resume. // Keep its tiny writer on the stack: capturing local emit functions create a // managed display-class allocation on this extremely hot path. diff --git a/src/SharpEmu.Core/Cpu/Native/PosixHostStubs.cs b/src/SharpEmu.Core/Cpu/Native/PosixHostStubs.cs index 99726f2..8005aeb 100644 --- a/src/SharpEmu.Core/Cpu/Native/PosixHostStubs.cs +++ b/src/SharpEmu.Core/Cpu/Native/PosixHostStubs.cs @@ -322,20 +322,43 @@ internal static unsafe class PosixHostStubs private static nint EmitSleep(byte* page, ref int offset) { - // void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi). - var usleep = ResolveLibcExport("usleep"); + // void Sleep(DWORD milliseconds in ecx) -> nanosleep(&req, &rem), resumed on EINTR. usleep + // would be shorter, but macOS rejects usleep(>= 1 s) with EINVAL, and a signal (the + // runtime's thread activations, for one) cuts either short: a sleep that returned at once + // on an ignored error turned UE's idle HTTP thread into a hot spin. + var nanosleep = ResolveLibcExport("nanosleep"); + var errorLocation = ResolveLibcExport(OperatingSystem.IsMacOS() ? "__error" : "__errno_location"); var start = (nint)(page + offset); Emit(page, ref offset, 0x56); // push rsi Emit(page, ref offset, 0x57); // push rdi - Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8 - Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx - Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF - Emit(page, ref offset, 0x76, 0x05); // jbe +5 - Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s) - Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000 - EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64 + Emit(page, ref offset, 0x53); // push rbx + Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x20); // sub rsp, 0x20: req at [rsp], rem at [rsp+16] + Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx + Emit(page, ref offset, 0x31, 0xD2); // xor edx, edx + Emit(page, ref offset, 0xB9, 0xE8, 0x03, 0x00, 0x00); // mov ecx, 1000 + Emit(page, ref offset, 0xF7, 0xF1); // div ecx: eax = seconds, edx = milliseconds + Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax (tv_sec) + Emit(page, ref offset, 0x69, 0xD2, 0x40, 0x42, 0x0F, 0x00); // imul edx, edx, 1000000 + Emit(page, ref offset, 0x48, 0x89, 0x54, 0x24, 0x08); // mov [rsp+8], rdx (tv_nsec) + // loop: + Emit(page, ref offset, 0x48, 0x89, 0xE7); // mov rdi, rsp + Emit(page, ref offset, 0x48, 0x8D, 0x74, 0x24, 0x10); // lea rsi, [rsp+16] + EmitMovRaxImm64(page, ref offset, nanosleep); // mov rax, imm64 Emit(page, ref offset, 0xFF, 0xD0); // call rax - Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8 + Emit(page, ref offset, 0x85, 0xC0); // test eax, eax + Emit(page, ref offset, 0x74, 0x26); // jz done + EmitMovRaxImm64(page, ref offset, errorLocation); // mov rax, imm64 + Emit(page, ref offset, 0xFF, 0xD0); // call rax + Emit(page, ref offset, 0x83, 0x38, 0x04); // cmp dword [rax], EINTR + Emit(page, ref offset, 0x75, 0x15); // jne done + Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x10); // mov rax, [rsp+16] + Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax + Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x18); // mov rax, [rsp+24] + Emit(page, ref offset, 0x48, 0x89, 0x44, 0x24, 0x08); // mov [rsp+8], rax + Emit(page, ref offset, 0xEB, 0xC2); // jmp loop + // done: + Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x20); // add rsp, 0x20 + Emit(page, ref offset, 0x5B); // pop rbx Emit(page, ref offset, 0x5F); // pop rdi Emit(page, ref offset, 0x5E); // pop rsi Emit(page, ref offset, 0xC3); // ret diff --git a/src/SharpEmu.HLE/Host/Posix/PosixHostStubs.cs b/src/SharpEmu.HLE/Host/Posix/PosixHostStubs.cs index 22ae8ec..c19ef0c 100644 --- a/src/SharpEmu.HLE/Host/Posix/PosixHostStubs.cs +++ b/src/SharpEmu.HLE/Host/Posix/PosixHostStubs.cs @@ -462,20 +462,43 @@ internal static unsafe class PosixHostStubs private static nint EmitSleep(byte* page, ref int offset) { - // void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi). - var usleep = ResolveLibcExport("usleep"); + // void Sleep(DWORD milliseconds in ecx) -> nanosleep(&req, &rem), resumed on EINTR. usleep + // would be shorter, but macOS rejects usleep(>= 1 s) with EINVAL, and a signal (the + // runtime's thread activations, for one) cuts either short: a sleep that returned at once + // on an ignored error turned UE's idle HTTP thread into a hot spin. + var nanosleep = ResolveLibcExport("nanosleep"); + var errorLocation = ResolveLibcExport(OperatingSystem.IsMacOS() ? "__error" : "__errno_location"); var start = (nint)(page + offset); Emit(page, ref offset, 0x56); // push rsi Emit(page, ref offset, 0x57); // push rdi - Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8 - Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx - Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF - Emit(page, ref offset, 0x76, 0x05); // jbe +5 - Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s) - Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000 - EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64 + Emit(page, ref offset, 0x53); // push rbx + Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x20); // sub rsp, 0x20: req at [rsp], rem at [rsp+16] + Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx + Emit(page, ref offset, 0x31, 0xD2); // xor edx, edx + Emit(page, ref offset, 0xB9, 0xE8, 0x03, 0x00, 0x00); // mov ecx, 1000 + Emit(page, ref offset, 0xF7, 0xF1); // div ecx: eax = seconds, edx = milliseconds + Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax (tv_sec) + Emit(page, ref offset, 0x69, 0xD2, 0x40, 0x42, 0x0F, 0x00); // imul edx, edx, 1000000 + Emit(page, ref offset, 0x48, 0x89, 0x54, 0x24, 0x08); // mov [rsp+8], rdx (tv_nsec) + // loop: + Emit(page, ref offset, 0x48, 0x89, 0xE7); // mov rdi, rsp + Emit(page, ref offset, 0x48, 0x8D, 0x74, 0x24, 0x10); // lea rsi, [rsp+16] + EmitMovRaxImm64(page, ref offset, nanosleep); // mov rax, imm64 Emit(page, ref offset, 0xFF, 0xD0); // call rax - Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8 + Emit(page, ref offset, 0x85, 0xC0); // test eax, eax + Emit(page, ref offset, 0x74, 0x26); // jz done + EmitMovRaxImm64(page, ref offset, errorLocation); // mov rax, imm64 + Emit(page, ref offset, 0xFF, 0xD0); // call rax + Emit(page, ref offset, 0x83, 0x38, 0x04); // cmp dword [rax], EINTR + Emit(page, ref offset, 0x75, 0x15); // jne done + Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x10); // mov rax, [rsp+16] + Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax + Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x18); // mov rax, [rsp+24] + Emit(page, ref offset, 0x48, 0x89, 0x44, 0x24, 0x08); // mov [rsp+8], rax + Emit(page, ref offset, 0xEB, 0xC2); // jmp loop + // done: + Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x20); // add rsp, 0x20 + Emit(page, ref offset, 0x5B); // pop rbx Emit(page, ref offset, 0x5F); // pop rdi Emit(page, ref offset, 0x5E); // pop rsi Emit(page, ref offset, 0xC3); // ret diff --git a/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs b/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs index 24869ab..ff5f998 100644 --- a/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs +++ b/src/SharpEmu.HLE/Host/Posix/PosixViewRegions.cs @@ -1,6 +1,8 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.Threading; + namespace SharpEmu.HLE.Host.Posix; // View mappings bypass the anonymous-allocation tables. Keep their query state together. @@ -11,6 +13,12 @@ internal static class PosixViewRegions uint[]? PageProtections, ulong PageBase); private static readonly List Regions = new(); + // The regions in address order, replaced whenever the list changes. Queries read it without + // Gate: every HLE import queries the guest stack's protection first, and Gate is held across + // mprotect by the GPU write tracker, so a locked query made each import wait on page traffic. + // Page protections change in place and are read racily, as a concurrent change always was. + private static Region[] _snapshot = []; + internal static void Replace(ulong address, ulong size, uint state, uint protection) { HostMemory.OnMappingChanged(); @@ -46,6 +54,8 @@ internal static class PosixViewRegions else Regions.Add(region); } + + Volatile.Write(ref _snapshot, [.. Regions]); } // Mapping creates the storage. Fault-time protection updates must not allocate. @@ -66,9 +76,8 @@ internal static class PosixViewRegions internal static bool TryQuery(ulong address, out HostMemory.BasicInfo info) { - lock (Gate) { - foreach (var region in Regions) + foreach (var region in Volatile.Read(ref _snapshot)) { if (region.Start > address) break; if (address >= region.End) continue; diff --git a/src/SharpEmu.HLE/HostMemory.cs b/src/SharpEmu.HLE/HostMemory.cs index 6586bf5..3e6dffa 100644 --- a/src/SharpEmu.HLE/HostMemory.cs +++ b/src/SharpEmu.HLE/HostMemory.cs @@ -149,6 +149,14 @@ public static unsafe class HostMemory private static readonly object Gate = new(); private static readonly SortedList Regions = new(); + // The regions in address order, replaced under Gate whenever a region is added or removed. + // Queries read it without the lock: Protect holds Gate across its mprotect call, and GPU + // write tracking protects pages constantly, so a locked query (one per HLE call reading + // stack arguments) waited tens of microseconds behind those syscalls. + private static Region[] RegionSnapshot = []; + + private static void PublishRegionsLocked() => Volatile.Write(ref RegionSnapshot, [.. Regions.Values]); + private sealed class Region { public ulong Base; @@ -156,6 +164,10 @@ public static unsafe class HostMemory public uint DefaultProtect; public Dictionary? PageProtects; + // The overridden pages in address order, so a query finds where a protection run ends + // without visiting every override: write tracking leaves tens of thousands of them. + public SortedSet? OverridePages; + public ulong End => Base + Size; public uint ProtectAt(ulong pageAddress) @@ -269,6 +281,7 @@ public static unsafe class HostMemory Size = alignedSize, DefaultProtect = protect }; + PublishRegionsLocked(); return (void*)result; } @@ -288,6 +301,7 @@ public static unsafe class HostMemory HostMemory.OnMappingChanged(); Regions.Remove((ulong)address); + PublishRegionsLocked(); return munmap((nint)address, (nuint)region.Size) == 0; } } @@ -325,6 +339,10 @@ public static unsafe class HostMemory { info = default; var pageAddress = AlignDown((ulong)address, PageSize); + if (TryQueryUnlocked(pageAddress, ref info)) + { + return (nuint)sizeof(BasicInfo); + } lock (Gate) { @@ -340,30 +358,37 @@ public static unsafe class HostMemory var pageProtects = region.PageProtects; uint protect; ulong runEnd; - if (pageProtects is null || pageProtects.Count == 0) + if (pageProtects is null || pageProtects.Count == 0 || region.OverridePages is not { } overridePages) { protect = region.DefaultProtect; runEnd = region.End; } else if (pageProtects.TryGetValue(pageAddress, out protect)) { + // Overrides run while the next page is overridden with the same protection. runEnd = pageAddress + PageSize; - while (runEnd < region.End && - pageProtects.TryGetValue(runEnd, out var nextProtect) && - nextProtect == protect) + foreach (var overrideAddress in runEnd < region.End ? overridePages.GetViewBetween(runEnd, region.End - 1) : []) { + if (overrideAddress != runEnd || pageProtects[overrideAddress] != protect) + { + break; + } + runEnd += PageSize; } } else { + // The default protection runs to the next override. protect = region.DefaultProtect; runEnd = region.End; - foreach (var overrideAddress in pageProtects.Keys) + if (pageAddress + PageSize < region.End) { - if (overrideAddress > pageAddress && overrideAddress < runEnd) + // The first element only: a view's Count walks the whole view. + foreach (var overrideAddress in overridePages.GetViewBetween(pageAddress + PageSize, region.End - 1)) { runEnd = overrideAddress; + break; } } } @@ -382,12 +407,18 @@ public static unsafe class HostMemory // reported as a free block reaching to the next tracked region // so scanning callers keep advancing. var nextBase = ulong.MaxValue; - foreach (var regionBase in Regions.Keys) + var keys = Regions.Keys; + for (int low = 0, high = keys.Count - 1; low <= high;) { - if (regionBase > pageAddress) + var middle = low + ((high - low) >> 1); + if (keys[middle] > pageAddress) { - nextBase = regionBase; - break; + nextBase = keys[middle]; + high = middle - 1; + } + else + { + low = middle + 1; } } @@ -416,6 +447,56 @@ public static unsafe class HostMemory return false; } + // Answers without Gate for untracked memory and for regions without per-page overrides; + // a region with overrides needs their dictionary, which only Gate protects. A concurrent + // Protect may land just before or after either way, as with the locked query. + private static bool TryQueryUnlocked(ulong pageAddress, ref BasicInfo info) + { + var regions = Volatile.Read(ref RegionSnapshot); + Region? candidate = null; + var nextBase = ulong.MaxValue; + for (int low = 0, high = regions.Length - 1; low <= high;) + { + var middle = low + ((high - low) >> 1); + if (regions[middle].Base <= pageAddress) + { + candidate = regions[middle]; + low = middle + 1; + } + else + { + nextBase = regions[middle].Base; + high = middle - 1; + } + } + + if (candidate is not null && pageAddress < candidate.End) + { + if (Volatile.Read(ref candidate.PageProtects) is { Count: > 0 }) + { + return false; + } + + info.BaseAddress = pageAddress; + info.AllocationBase = candidate.Base; + info.AllocationProtect = candidate.DefaultProtect; + info.RegionSize = candidate.End - pageAddress; + info.State = MEM_COMMIT; + info.Protect = candidate.DefaultProtect; + info.Type = MEM_PRIVATE; + return true; + } + + info.BaseAddress = pageAddress; + info.AllocationBase = 0; + info.AllocationProtect = PAGE_NOACCESS; + info.RegionSize = (nextBase == ulong.MaxValue ? pageAddress + PageSize : nextBase) - pageAddress; + info.State = MEM_FREE_STATE; + info.Protect = PAGE_NOACCESS; + info.Type = 0; + return true; + } + private static bool TryFindRegionLocked(ulong address, out Region region) { region = null!; @@ -454,20 +535,26 @@ public static unsafe class HostMemory { region.DefaultProtect = protect; region.PageProtects = null; + region.OverridePages = null; return; } region.PageProtects ??= new Dictionary(); + region.OverridePages ??= new SortedSet(); var end = start + size; for (var pageAddress = start; pageAddress < end; pageAddress += PageSize) { if (protect == region.DefaultProtect) { - region.PageProtects.Remove(pageAddress); + if (region.PageProtects.Remove(pageAddress)) + { + region.OverridePages.Remove(pageAddress); + } } else { region.PageProtects[pageAddress] = protect; + region.OverridePages.Add(pageAddress); } } } diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 3cea481..10b66b4 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -1988,7 +1988,7 @@ public static class KernelPthreadCompatExports break; } - var waitDuration = TimeSpan.FromMilliseconds(10); + var waitMilliseconds = 10; if (timed) { var remaining = GetRemainingTimeout(deadline); @@ -1998,13 +1998,14 @@ public static class KernelPthreadCompatExports break; } - if (remaining < waitDuration) - { - waitDuration = remaining; - } + // Monitor.Wait truncates to whole milliseconds, so a sub-millisecond remainder + // became a zero timeout and the wait spun until the deadline. UE's game thread + // waits for the render fence in 1 ms slices, which kept a core busy. A timed wait + // may wake late, so round up instead. + waitMilliseconds = (int)Math.Min(waitMilliseconds, Math.Ceiling(remaining.TotalMilliseconds)); } - _ = Monitor.Wait(state.SyncRoot, waitDuration); + _ = Monitor.Wait(state.SyncRoot, waitMilliseconds); if (waiter.CompletionState == 0 && timed && GetRemainingTimeout(deadline) <= TimeSpan.Zero) diff --git a/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs index 17d81da..7dff0eb 100644 --- a/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelSyncOnAddressCompatExports.cs @@ -144,7 +144,8 @@ public static class KernelSyncOnAddressCompatExports { if (Volatile.Read(ref _wakeRequested) == 0) { - Monitor.Wait(_hostGate, waitSlice); + // Round up so a sub-millisecond remainder does not spin. + Monitor.Wait(_hostGate, (int)Math.Ceiling(waitSlice.TotalMilliseconds)); } } diff --git a/tests/SharpEmu.Libs.Tests/Cpu/PosixSleepStubTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/PosixSleepStubTests.cs new file mode 100644 index 0000000..63b8d20 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/PosixSleepStubTests.cs @@ -0,0 +1,27 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Diagnostics; +using SharpEmu.Core.Cpu.Native; +using Xunit; + +namespace SharpEmu.Libs.Tests.Cpu; + +// The POSIX stand-in for kernel32!Sleep takes milliseconds in ecx. It must sleep for a second or +// more as well: macOS usleep rejects such lengths with EINVAL, which made the stub return at once. +public sealed unsafe class PosixSleepStubTests +{ + [NativeX64Theory] + [InlineData(1200u, 1.0, 4.0)] + [InlineData(20u, 0.015, 1.0)] + public void SleepStubSleepsForTheRequestedMilliseconds(uint milliseconds, double minSeconds, double maxSeconds) + { + if (OperatingSystem.IsWindows()) return; + // SysV passes the fourth integer argument in rcx, where the stub reads its milliseconds. + var sleep = (delegate* unmanaged)PosixHostStubs.SleepStubAddress; + var started = Stopwatch.GetTimestamp(); + sleep(0, 0, 0, milliseconds); + var elapsed = Stopwatch.GetElapsedTime(started).TotalSeconds; + Assert.InRange(elapsed, minSeconds, maxSeconds); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Memory/HostMemoryQueryRunTests.cs b/tests/SharpEmu.Libs.Tests/Memory/HostMemoryQueryRunTests.cs new file mode 100644 index 0000000..12c0700 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Memory/HostMemoryQueryRunTests.cs @@ -0,0 +1,53 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using Xunit; + +namespace SharpEmu.Libs.Tests.Memory; + +// A POSIX query reports the run of pages sharing the queried page's protection, ending at the +// next protection change, as Win32 VirtualQuery does. +public sealed unsafe class HostMemoryQueryRunTests +{ + [Fact] + public void QueryRunsEndAtTheNextProtectionChange() + { + if (OperatingSystem.IsWindows()) return; + var page = (ulong)Environment.SystemPageSize; + var memory = (byte*)HostMemory.Alloc(null, (nuint)(page * 16), HostMemory.MEM_RESERVE | HostMemory.MEM_COMMIT, HostMemory.PAGE_READWRITE); + Assert.True(memory != null); + var start = (ulong)memory; + try + { + Assert.True(HostMemory.Protect((void*)(start + 3 * page), (nuint)(3 * page), HostMemory.PAGE_READONLY, out _)); + Assert.True(HostMemory.Protect((void*)(start + 9 * page), (nuint)page, HostMemory.PAGE_NOACCESS, out _)); + Assert.True(HostMemory.Protect((void*)(start + 15 * page), (nuint)page, HostMemory.PAGE_READONLY, out _)); + + AssertRun(start, 0, 3, HostMemory.PAGE_READWRITE); + AssertRun(start, 3, 3, HostMemory.PAGE_READONLY); + AssertRun(start, 4, 2, HostMemory.PAGE_READONLY); + AssertRun(start, 6, 3, HostMemory.PAGE_READWRITE); + AssertRun(start, 9, 1, HostMemory.PAGE_NOACCESS); + AssertRun(start, 10, 5, HostMemory.PAGE_READWRITE); + // A run at the last page of the mapping. + AssertRun(start, 15, 1, HostMemory.PAGE_READONLY); + + // Restoring the default protection removes the overrides from the run. + Assert.True(HostMemory.Protect((void*)(start + 3 * page), (nuint)(7 * page), HostMemory.PAGE_READWRITE, out _)); + AssertRun(start, 0, 15, HostMemory.PAGE_READWRITE); + } + finally + { + Assert.True(HostMemory.Free(memory, 0, HostMemory.MEM_RELEASE)); + } + + void AssertRun(ulong baseAddress, ulong firstPage, ulong pages, uint protect) + { + Assert.NotEqual(0u, (uint)HostMemory.Query((void*)(baseAddress + firstPage * page + 8), out var info)); + Assert.Equal(baseAddress + firstPage * page, info.BaseAddress); + Assert.Equal(pages * page, info.RegionSize); + Assert.Equal(protect, info.Protect); + } + } +}