diff --git a/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs b/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs index 3f35b8c..71f3de6 100644 --- a/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs +++ b/src/SharpEmu.Core/Cpu/TrackedCpuMemory.cs @@ -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); + } } diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index fa75ce1..8ac6ca8 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -42,7 +42,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA private readonly IHostMemory _hostMemory; private ulong _guestAllocationArenaBase; - private ulong _guestAllocationOffset; + private readonly SortedDictionary _guestAllocationFreeRanges = new(); + private readonly Dictionary _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 { diff --git a/src/SharpEmu.Core/Memory/VirtualMemory.cs b/src/SharpEmu.Core/Memory/VirtualMemory.cs index ecb03e6..cb2ff4e 100644 --- a/src/SharpEmu.Core/Memory/VirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/VirtualMemory.cs @@ -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 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 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); diff --git a/src/SharpEmu.HLE/IGuestMemoryAllocator.cs b/src/SharpEmu.HLE/IGuestMemoryAllocator.cs index c67c778..9a84ae0 100644 --- a/src/SharpEmu.HLE/IGuestMemoryAllocator.cs +++ b/src/SharpEmu.HLE/IGuestMemoryAllocator.cs @@ -6,4 +6,6 @@ namespace SharpEmu.HLE; public interface IGuestMemoryAllocator { bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address); + + bool TryFreeGuestMemory(ulong address); } diff --git a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs index 6bfd038..6a61d8e 100644 --- a/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelPthreadCompatExports.cs @@ -629,6 +629,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 +642,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 +670,7 @@ public static class KernelPthreadCompatExports } _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0); + TryFreeOpaqueObject(ctx, resolvedAddress); state.Semaphore.Dispose(); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -895,6 +898,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 +916,7 @@ public static class KernelPthreadCompatExports _mutexAttrStates.Remove(handle); } + TryFreeOpaqueObject(ctx, handle); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -935,6 +940,7 @@ public static class KernelPthreadCompatExports } } + TryFreeOpaqueObject(ctx, resolvedAddress); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -1195,6 +1201,7 @@ public static class KernelPthreadCompatExports { if (_condStates.TryGetValue(condAddress, out var raced)) { + TryFreeOpaqueObject(ctx, handle); resolvedAddress = condAddress; state = raced; return true; @@ -1212,6 +1219,7 @@ public static class KernelPthreadCompatExports _condStates.Remove(handle); } + TryFreeOpaqueObject(ctx, handle); return false; } @@ -1231,7 +1239,22 @@ public static class KernelPthreadCompatExports Span 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 +1292,7 @@ public static class KernelPthreadCompatExports _condStates.Remove(handle); } + TryFreeOpaqueObject(ctx, handle); return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -1293,6 +1317,7 @@ public static class KernelPthreadCompatExports } _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0); + TryFreeOpaqueObject(ctx, resolvedAddress); return (int)OrbisGen2Result.ORBIS_GEN2_OK; } @@ -1775,6 +1800,7 @@ public static class KernelPthreadCompatExports } if (!InitializeMutexObject(ctx, handle, createdState)) { + TryFreeOpaqueObject(ctx, handle); resolvedAddress = 0; state = null; return false; @@ -1784,12 +1810,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 +1831,7 @@ public static class KernelPthreadCompatExports _mutexStates.TryRemove(mutexAddress, out _); _mutexStates.TryRemove(handle, out _); + TryFreeOpaqueObject(ctx, handle); resolvedAddress = 0; state = null; return false; diff --git a/tests/SharpEmu.Libs.Tests/Memory/GuestMemoryAllocatorTests.cs b/tests/SharpEmu.Libs.Tests/Memory/GuestMemoryAllocatorTests.cs new file mode 100644 index 0000000..23cd11d --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Memory/GuestMemoryAllocatorTests.cs @@ -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.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.Empty, ProgramHeaderFlags.Write); + host.ProtectionCalls.Clear(); + + memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan.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() + { + } + } +} diff --git a/tests/SharpEmu.Libs.Tests/Memory/VirtualMemoryTests.cs b/tests/SharpEmu.Libs.Tests/Memory/VirtualMemoryTests.cs new file mode 100644 index 0000000..204ddf2 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Memory/VirtualMemoryTests.cs @@ -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 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(() => + memory.Map(0x1FFF, 2, 0, [], ProgramHeaderFlags.Read)); + Assert.Throws(() => + 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 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 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])); + } +} diff --git a/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj b/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj index f96d936..7f5081a 100644 --- a/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj +++ b/tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj @@ -7,9 +7,11 @@ SPDX-License-Identifier: GPL-2.0-or-later false false + true + diff --git a/tests/SharpEmu.Libs.Tests/packages.lock.json b/tests/SharpEmu.Libs.Tests/packages.lock.json index 879d342..6114506 100644 --- a/tests/SharpEmu.Libs.Tests/packages.lock.json +++ b/tests/SharpEmu.Libs.Tests/packages.lock.json @@ -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, )",