diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index e496f98..f083159 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -1276,6 +1276,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA return true; } + // POSIX cannot query foreign host mappings. The free ranges were + // reserved at startup, so a failed candidate means the gap is taken. + if (!OperatingSystem.IsWindows() && reservedCandidate != 0) + { + address = reservedCandidate; + return true; + } + start = candidate + GuestMemoryLayout.GuestPage; if (OperatingSystem.IsWindows() && _hostMemory.Query(candidate, out var info) && info.BaseAddress <= candidate && info.BaseAddress < limit && diff --git a/src/SharpEmu.HLE/GuestMemory/GuestSpaceOwner.cs b/src/SharpEmu.HLE/GuestMemory/GuestSpaceOwner.cs index 883b5b8..006bc8c 100644 --- a/src/SharpEmu.HLE/GuestMemory/GuestSpaceOwner.cs +++ b/src/SharpEmu.HLE/GuestMemory/GuestSpaceOwner.cs @@ -30,6 +30,7 @@ public sealed class GuestSpaceOwner : IDisposable private readonly SortedList _free = new(); private readonly SortedList _mapped = new(); private readonly List<(ulong Address, ulong Size)> _owned = new(); + private readonly bool _preReserveGuestAddressSpace; private bool _disposed; public GuestSpaceOwner(IHostViewMemory host, ulong backingSize, bool preReserveGuestAddressSpace = false) @@ -39,14 +40,8 @@ public sealed class GuestSpaceOwner : IDisposable // Create lookup views before concurrent fault handlers can read the range table. _ = _mapped.Keys; _ = _mapped.Values; - if (preReserveGuestAddressSpace) - { - foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange)) - { - _owned.Add((range.Address, range.Size)); - AddFreeRange(range.Address, range.Size); - } - } + _preReserveGuestAddressSpace = preReserveGuestAddressSpace; + PreReserveGuestAddressSpace(); _views = new SharedBackingViews(host, backingSize); if (!_views.IsAvailable) { @@ -495,6 +490,22 @@ public sealed class GuestSpaceOwner : IDisposable _owned.Clear(); _free.Clear(); _mapped.Clear(); + // A new image load needs the guest address space reserved again. + PreReserveGuestAddressSpace(); + } + } + + private void PreReserveGuestAddressSpace() + { + if (!_preReserveGuestAddressSpace) + { + return; + } + + foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange)) + { + _owned.Add((range.Address, range.Size)); + AddFreeRange(range.Address, range.Size); } } diff --git a/src/SharpEmu.HLE/Host/Posix/PosixHostViews.cs b/src/SharpEmu.HLE/Host/Posix/PosixHostViews.cs index 1a48522..b2c7ce0 100644 --- a/src/SharpEmu.HLE/Host/Posix/PosixHostViews.cs +++ b/src/SharpEmu.HLE/Host/Posix/PosixHostViews.cs @@ -96,6 +96,60 @@ internal sealed unsafe partial class PosixHostViews : IHostViewMemory return address; } + // POSIX has no VirtualQuery for foreign host mappings, so find the free + // ranges by reserving halves until each attempt fits or falls below the + // minimum. Occupied host ranges are few, so this needs few attempts. + public IReadOnlyList ReserveFreeAddressRanges(ulong start, ulong end, ulong minimumSize) + { + var reserved = new List(); + start = AlignUp(start, Granularity); + end -= end % Granularity; + if (start >= end || minimumSize == 0) + { + return reserved; + } + + lock (PosixViewRegions.Gate) + { + ReserveFreeAddressRangesLocked(start, end - start, Math.Max(minimumSize, Granularity), reserved); + } + + return reserved; + } + + private void ReserveFreeAddressRangesLocked(ulong address, ulong size, ulong minimumSize, List reserved) + { + if (ReserveHoleCore(address, size) == address) + { + PosixViewRegions.Replace(address, size, HostMemory.MEM_RESERVE, HostMemory.PAGE_NOACCESS); + if (reserved.Count != 0 && reserved[^1].Address + reserved[^1].Size == address) + { + reserved[^1] = reserved[^1] with { Size = reserved[^1].Size + size }; + } + else + { + reserved.Add(new HostAddressRange(address, size)); + } + return; + } + + var half = size / 2; + half -= half % Granularity; + if (half < minimumSize) + { + return; + } + + ReserveFreeAddressRangesLocked(address, half, minimumSize, reserved); + ReserveFreeAddressRangesLocked(address + half, size - half, minimumSize, reserved); + } + + private static ulong AlignUp(ulong value, ulong alignment) + { + var remainder = value % alignment; + return remainder == 0 ? value : value + alignment - remainder; + } + public bool SplitHole(ulong address, ulong size) => size != 0; public bool JoinHoles(ulong address, ulong size) => size != 0; diff --git a/tests/SharpEmu.Libs.Tests/Memory/GuestMemory/GuestSpaceOwnerTests.cs b/tests/SharpEmu.Libs.Tests/Memory/GuestMemory/GuestSpaceOwnerTests.cs index 587be73..af140ad 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/GuestMemory/GuestSpaceOwnerTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/GuestMemory/GuestSpaceOwnerTests.cs @@ -422,4 +422,26 @@ public sealed unsafe partial class GuestSpaceOwnerTests Assert.Equal(baseAddress, raw.ReserveHole(baseAddress, hole)); Assert.True(raw.FreeHole(baseAddress, hole)); } + + [Fact] + public void ReleaseAddressRanges_ReservesTheGuestAddressSpaceAgain() + { + if (!Supported) + { + return; + } + + const ulong start = 0x10_0000_0000; + const ulong end = 0xFC_0000_0000; + const ulong size = 0x10_0000_0000; + const ulong alignment = 0x20_0000; + using var owner = new GuestSpaceOwner(HostViewMemory.Create(), BackingSize, preReserveGuestAddressSpace: true); + Assert.NotEqual(0UL, owner.FindFreeAddress(start, end, size, alignment)); + + owner.ReleaseAddressRanges(); + + var address = owner.FindFreeAddress(start, end, size, alignment); + Assert.NotEqual(0UL, address); + Assert.True(owner.ContainsFreeRange(address, size)); + } } diff --git a/tests/SharpEmu.Libs.Tests/Memory/HostViews/HostViewMemoryTests.cs b/tests/SharpEmu.Libs.Tests/Memory/HostViews/HostViewMemoryTests.cs index 0be5c50..e52994f 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/HostViews/HostViewMemoryTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/HostViews/HostViewMemoryTests.cs @@ -346,6 +346,43 @@ public sealed unsafe class HostViewMemoryTests public uint Alignment2; } + [Fact] + public void ReserveFreeAddressRanges_SkipsOccupiedHostRanges() + { + if (!Supported) + { + return; + } + + var views = HostViewMemory.Create(); + const ulong span = 0x400_0000; + const ulong minimum = 0x10_0000; + var start = ProbeGuestAddress(views, span); + var blocker = start + span / 4; + var blockerSize = AlignUp(minimum, views.Granularity); + Assert.Equal(blocker, views.ReserveHole(blocker, blockerSize)); + + var ranges = views.ReserveFreeAddressRanges(start, start + span, minimum); + try + { + Assert.NotEmpty(ranges); + Assert.Equal(start, ranges[0].Address); + Assert.All(ranges, range => Assert.True( + range.Address + range.Size <= blocker || range.Address >= blocker + blockerSize)); + var reserved = ranges.Aggregate(0UL, (total, range) => total + range.Size); + Assert.True(reserved >= span - blockerSize - 2 * minimum, $"Only 0x{reserved:X} bytes were reserved."); + } + finally + { + foreach (var range in ranges) + { + Assert.True(views.FreeHole(range.Address, range.Size)); + } + + Assert.True(views.FreeHole(blocker, blockerSize)); + } + } + [DllImport("kernel32.dll")] private static extern nuint VirtualQuery(void* address, out MemoryBasicInformation info, nuint length); }