diff --git a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs index 86ad912c..d609541d 100644 --- a/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs +++ b/src/SharpEmu.Core/Memory/PhysicalVirtualMemory.cs @@ -425,6 +425,87 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA return actualAddress; } + public bool TryBackFixedRange(ulong address, ulong size, bool executable) + { + if (size == 0) + { + return false; + } + + var start = AlignDown(address, PageSize); + var end = AlignUp(address + size, PageSize); + if (end <= start) + { + return false; + } + + var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite; + + // Walk the range page-run by page-run. VirtualQuery reports the largest run + // of same-state pages from the queried address, so a single query advances + // us over whole free or occupied stretches. Only free stretches get backed; + // stretches already reserved or committed by another allocation are left as + // they are, which is exactly what a fixed mapping does on hardware. + var cursor = start; + var backedAny = false; + while (cursor < end) + { + if (!_hostMemory.Query(cursor, out var info)) + { + return false; + } + + var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress + ? ulong.MaxValue + : info.BaseAddress + info.RegionSize; + var runEnd = Math.Min(end, queriedEnd); + if (runEnd <= cursor) + { + return false; + } + + if (info.State == HostRegionState.Free) + { + var runSize = runEnd - cursor; + var allocated = _hostMemory.Allocate(cursor, runSize, hostProtection); + if (allocated != cursor) + { + if (allocated != 0) + { + _hostMemory.Free(allocated); + } + + return false; + } + + var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE; + _gate.EnterWriteLock(); + try + { + InsertRegionSorted(new MemoryRegion + { + VirtualAddress = cursor, + Size = runSize, + IsExecutable = executable, + IsReservedOnly = false, + Protection = protection + }); + } + finally + { + _gate.ExitWriteLock(); + } + + TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)"); + backedAny = true; + } + + cursor = runEnd; + } + + return backedAny; + } + public bool TryAllocateAtOrAbove( ulong desiredAddress, ulong size, diff --git a/src/SharpEmu.HLE/IGuestAddressSpace.cs b/src/SharpEmu.HLE/IGuestAddressSpace.cs index f7a22c25..53626dd2 100644 --- a/src/SharpEmu.HLE/IGuestAddressSpace.cs +++ b/src/SharpEmu.HLE/IGuestAddressSpace.cs @@ -15,6 +15,17 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator { ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true); + /// + /// Backs an entire fixed-address range, matching the guest's + /// SCE_KERNEL_MAP_FIXED contract. Unlike , which + /// reserves the range in one all-or-nothing host call, this walks the range and + /// fills only the sub-ranges that are not already backed. That keeps a fixed + /// mapping whole when part of the requested window is already occupied — the + /// partial-overlap case where the single-call reservation fails outright and + /// leaves the remainder unmapped for the guest to fault into. + /// + bool TryBackFixedRange(ulong address, ulong size, bool executable); + bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress); bool TryProtect(ulong address, ulong size, GuestPageProtection protection); diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index bdc33ad2..17f4288d 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -4603,7 +4603,8 @@ public static partial class KernelMemoryCompatExports allowSearch: false, allowAllocateAtAlternative: false, "reserve fixed range", - out _); + out _, + backPartialOverlap: true); } internal static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length) diff --git a/src/SharpEmu.Libs/Kernel/KernelVirtualRangeAllocator.cs b/src/SharpEmu.Libs/Kernel/KernelVirtualRangeAllocator.cs index 50c8868b..cbb13c8c 100644 --- a/src/SharpEmu.Libs/Kernel/KernelVirtualRangeAllocator.cs +++ b/src/SharpEmu.Libs/Kernel/KernelVirtualRangeAllocator.cs @@ -18,7 +18,8 @@ internal static class KernelVirtualRangeAllocator bool allowSearch, bool allowAllocateAtAlternative, string traceName, - out ulong mappedAddress) + out ulong mappedAddress, + bool backPartialOverlap = false) { mappedAddress = 0; if (length == 0) @@ -42,6 +43,18 @@ internal static class KernelVirtualRangeAllocator return true; } + // Fixed mappings must cover the whole requested window even when part of + // it is already backed by another allocation. The single-call AllocateAt + // below is all-or-nothing and fails outright on partial overlap, leaving + // the untouched pages unmapped for the guest to fault into. Fill the free + // pages directly instead. + if (backPartialOverlap && + addressSpace.TryBackFixedRange(desiredAddress, length, executable)) + { + mappedAddress = desiredAddress; + return true; + } + var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative); if (allocated == 0) { diff --git a/tests/SharpEmu.Libs.Tests/Memory/GuestMemoryAllocatorTests.cs b/tests/SharpEmu.Libs.Tests/Memory/GuestMemoryAllocatorTests.cs index 435fd207..a0a83076 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/GuestMemoryAllocatorTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/GuestMemoryAllocatorTests.cs @@ -121,6 +121,113 @@ public sealed class GuestMemoryAllocatorTests Assert.Equal([alignedAddress + 0x1000], host.FreedAddresses); } + [Fact] + public void TryBackFixedRangeFillsOnlyTheFreePagesOfAPartiallyOccupiedRange() + { + const ulong rangeBase = 0x0000_0020_2F00_0000; + const ulong rangeSize = 0x40_0000; + const ulong occupiedSize = 0x4_0000; + using var host = new PartialOverlapHostMemory(rangeBase, occupiedSize, rangeSize); + using var memory = new PhysicalVirtualMemory(host); + + Assert.True(memory.TryBackFixedRange(rangeBase, rangeSize, executable: false)); + + // Only the free tail is allocated; the already-occupied head is untouched. + Assert.Equal( + [(rangeBase + occupiedSize, rangeSize - occupiedSize)], + host.AllocationCalls); + } + + [Fact] + public void TryBackFixedRangeReturnsFalseWhenRangeIsFullyOccupied() + { + const ulong rangeBase = 0x0000_0020_2F00_0000; + const ulong rangeSize = 0x40_0000; + using var host = new PartialOverlapHostMemory(rangeBase, rangeSize, rangeSize); + using var memory = new PhysicalVirtualMemory(host); + + Assert.False(memory.TryBackFixedRange(rangeBase, rangeSize, executable: false)); + Assert.Empty(host.AllocationCalls); + } + + private sealed class PartialOverlapHostMemory : IHostMemory, IDisposable + { + private readonly ulong _rangeBase; + private readonly ulong _occupiedEnd; + private readonly ulong _rangeEnd; + + public PartialOverlapHostMemory(ulong rangeBase, ulong occupiedSize, ulong rangeSize) + { + _rangeBase = rangeBase; + _occupiedEnd = rangeBase + occupiedSize; + _rangeEnd = rangeBase + rangeSize; + } + + public List<(ulong Address, ulong Size)> AllocationCalls { get; } = []; + + public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) + { + AllocationCalls.Add((desiredAddress, size)); + return desiredAddress; + } + + 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) => 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) + { + // Report contiguous runs of same-state pages, exactly as VirtualQuery + // does: the occupied head first, then the free tail. + if (address < _occupiedEnd) + { + info = new HostRegionInfo( + address, + _rangeBase, + _occupiedEnd - address, + HostRegionState.Committed, + RawState: 0x1000, + HostPageProtection.ReadWrite, + RawProtection: 0x04, + RawAllocationProtection: 0x04); + return true; + } + + info = new HostRegionInfo( + address, + AllocationBase: 0, + _rangeEnd - address, + HostRegionState.Free, + RawState: 0x10000, + HostPageProtection.NoAccess, + RawProtection: 0x01, + RawAllocationProtection: 0); + return true; + } + + public void FlushInstructionCache(ulong address, ulong size) + { + } + + public void Dispose() + { + } + } + private sealed class FakeHostMemory : IHostMemory { public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>