sharpemu/tests/SharpEmu.Libs.Tests/Memory/GuestMemory/PhysicalVirtualMemoryBacked...

380 lines
17 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using Xunit;
using static SharpEmu.Libs.Tests.Memory.HostViews.HostViewTestSupport;
namespace SharpEmu.Libs.Tests.Memory.GuestMemory;
[Collection(GuestMemoryStateCollection.Name)]
public sealed unsafe class PhysicalVirtualMemoryBackedTests
{
private sealed class QueryCountingHostMemory(IHostMemory inner) : IHostMemory
{
public int QueryCount { get; set; }
public bool RejectExecutableProtection { get; init; }
public ulong Allocate(ulong address, ulong size, HostPageProtection protection) => inner.Allocate(address, size, protection);
public ulong Reserve(ulong address, ulong size, HostPageProtection protection) => inner.Reserve(address, size, protection);
public bool Commit(ulong address, ulong size, HostPageProtection protection) => inner.Commit(address, size, protection);
public bool Free(ulong address) => inner.Free(address);
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint previous)
{
previous = 0;
if (RejectExecutableProtection && protection == HostPageProtection.ReadWriteExecute)
return false;
return inner.Protect(address, size, protection, out previous);
}
public bool ProtectRaw(ulong address, ulong size, uint protection, out uint previous) => inner.ProtectRaw(address, size, protection, out previous);
public void FlushInstructionCache(ulong address, ulong size) => inner.FlushInstructionCache(address, size);
public bool Query(ulong address, out HostRegionInfo info)
{
QueryCount++;
return inner.Query(address, out info);
}
}
[Fact]
public void SearchSkipsAFreeGapThatCannotFitTheAllocation()
{
if (!OperatingSystem.IsWindows()) return;
var host = HostViewMemory.Create();
var memoryHost = new QueryCountingHostMemory(PlatformMemory);
using var memory = new PhysicalVirtualMemory(memoryHost, host, BackingSize);
const ulong gapSize = 0x100000;
var start = 0UL;
for (var candidate = 0x3_0000_0000UL; candidate < 0x4_0000_0000UL; candidate += 0x1000000)
{
if (host.ReserveHole(candidate, 4 * gapSize) != candidate) continue;
Assert.True(host.FreeHole(candidate, 4 * gapSize));
start = candidate;
break;
}
Assert.NotEqual(0UL, start);
Assert.Equal(start + gapSize, host.ReserveHole(start + gapSize, gapSize));
try
{
memoryHost.QueryCount = 0;
Assert.True(memory.TryHoldRangeAtOrAbove(start, 2 * gapSize, host.Granularity, out var address));
Assert.Equal(start + 2 * gapSize, address);
Assert.InRange(memoryHost.QueryCount, 1, 3);
}
finally
{
Assert.True(host.FreeHole(start + gapSize, gapSize));
}
}
[Fact]
public void SearchSkipsKnownMappingsWithoutHostQueries()
{
if (!Supported) return;
var host = HostViewMemory.Create();
var memoryHost = new QueryCountingHostMemory(PlatformMemory);
using var memory = new PhysicalVirtualMemory(memoryHost, host, BackingSize);
Assert.True(memory.TryHoldRangeAtOrAbove(0x3_0000_0000, 2 * Segment, Segment, out var address));
Assert.True(memory.TryMapBacked(address, Segment, 0, GuestPageProtection.Read, out _));
memoryHost.QueryCount = 0;
Assert.True(memory.TryHoldRangeAtOrAbove(address, Segment, Segment, out var selected));
Assert.Equal(address + Segment, selected);
Assert.Equal(0, memoryHost.QueryCount);
}
private static ulong Hold(PhysicalVirtualMemory memory, IHostViewMemory host)
{
for (var attempt = 0; attempt < 8; attempt++)
{
var address = ProbeGuestAddress(host, HoleSize(host));
if (memory.TryHoldRange(address, HoleSize(host)))
{
return address;
}
}
Assert.Fail("could not hold a test range");
return 0;
}
// A probed address can be taken before Map runs; retry like the hole helpers do.
private static ulong MapPrivate(PhysicalVirtualMemory memory, IHostViewMemory host, ulong size, ProgramHeaderFlags flags)
{
for (var attempt = 0; attempt < 8; attempt++)
{
var address = ProbeGuestAddress(host, size);
try
{
memory.Map(address, size, 0, ReadOnlySpan<byte>.Empty, flags);
return address;
}
catch (InvalidOperationException)
{
}
}
Assert.Fail("could not map a private test range");
return 0;
}
[Fact]
public void RegisterPacketCopiesValuesAcrossBackingViews()
{
if (!Supported) return;
var host = HostViewMemory.Create();
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
var sourceViewAddress = Hold(memory, host);
var destinationViewAddress = Hold(memory, host);
Assert.True(memory.TryMapBacked(sourceViewAddress, Segment, 0, GuestPageProtection.Read, out _));
Assert.True(memory.TryMapBacked(sourceViewAddress + Segment, Segment, 2 * Segment, GuestPageProtection.Read, out _));
Assert.True(memory.TryMapBacked(destinationViewAddress, Segment, 4 * Segment, GuestPageProtection.Read, out _));
Assert.True(memory.TryMapBacked(destinationViewAddress + Segment, Segment, 6 * Segment, GuestPageProtection.Read, out _));
var context = new CpuContext(memory, Generation.Gen5);
var commandBufferAddress = destinationViewAddress + 0x100;
var packetAddress = destinationViewAddress + Segment - 24;
var sourceAddress = sourceViewAddress + Segment - 8;
byte[] values = Enumerable.Range(1, 16).Select(value => (byte)value).ToArray();
Assert.True(memory.TryWrite(sourceAddress, values));
Assert.True(context.TryWriteUInt64(commandBufferAddress + 0x10, packetAddress));
Assert.True(context.TryWriteUInt64(commandBufferAddress + 0x18, destinationViewAddress + 2 * Segment));
context[CpuRegister.Rdi] = commandBufferAddress;
context[CpuRegister.Rsi] = 0x40;
context[CpuRegister.Rdx] = sourceAddress;
context[CpuRegister.Rcx] = 4;
Assert.Equal(0, SharpEmu.Libs.Agc.AgcExports.CbSetShRegisterRangeDirect(context));
Assert.Equal(packetAddress + 8, context[CpuRegister.Rax]);
var copiedValues = new byte[16];
Assert.True(memory.TryRead(packetAddress + 16, copiedValues));
Assert.Equal(values, copiedValues);
Assert.True(context.TryReadUInt64(commandBufferAddress + 0x10, out var commandCursor));
Assert.Equal(packetAddress + 32, commandCursor);
}
[Fact]
public void ViewsShareBackingAndKeepContentsAfterRemapping()
{
if (!Supported) return;
var host = HostViewMemory.Create();
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
var first = Hold(memory, host);
var second = Hold(memory, host);
Assert.True(memory.TryMapBacked(first, Segment, 0, GuestPageProtection.Read, out _), "map first");
Assert.True(memory.TryMapBacked(second, Segment, 0, GuestPageProtection.Read, out _), "map second");
*(ulong*)first = Marker;
Assert.Equal(Marker, *(ulong*)second);
Assert.True(memory.TryProtect(first, Segment, GuestPageProtection.None), "protect");
Assert.True(memory.TryWrite(first, BitConverter.GetBytes(~Marker)), "alias write");
Assert.True(memory.TryCompare(second, BitConverter.GetBytes(~Marker)), "compare second");
Assert.True(memory.TryCompare(first, BitConverter.GetBytes(~Marker)), "compare first");
Assert.True(memory.TryCopy(first + 16, second, 8), "alias copy");
Assert.Equal(~Marker, *(ulong*)(second + 16));
Assert.True(memory.TryUnmapBacked(first, Segment));
Assert.False(memory.IsBackedView(first));
Assert.True(memory.TryMapBacked(first, Segment, 0, GuestPageProtection.Read, out _));
Assert.Equal(~Marker, *(ulong*)first);
Assert.True(memory.IsBackedView(first));
Assert.Contains(memory.SnapshotRegions(), r => r.VirtualAddress == first);
}
[Fact]
public void ClearKeepsTheSameMemoryObjectUsable()
{
if (!Supported) return;
var host = new FailingHostViews(HostViewMemory.Create());
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
var address = Hold(memory, host);
Assert.True(memory.TryMapBacked(address, Segment, 0, GuestPageProtection.Read, out _));
*(ulong*)address = Marker;
memory.Clear();
Assert.False(memory.IsBackedView(address));
Assert.True(memory.TryMapBacked(address, Segment, 0, GuestPageProtection.Read, out _));
Assert.Equal(Marker, *(ulong*)address);
Assert.Equal(1, host.Log.Count(op => op == FailingHostViews.Op.CreateBacking));
memory.Dispose();
Assert.False(memory.TryHoldRange(address, HoleSize(host)));
Assert.False(memory.TryMapBacked(address, Segment, 0, GuestPageProtection.Read, out _));
}
[Fact]
public void CopyBetweenAliasesKeepsMoveSemanticsOnTheSharedBacking()
{
if (!Supported) return;
var host = HostViewMemory.Create();
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
var first = Hold(memory, host);
var second = Hold(memory, host);
Assert.True(memory.TryMapBacked(first, Segment, 0, GuestPageProtection.Read, out _));
Assert.True(memory.TryMapBacked(second, Segment, 0, GuestPageProtection.Read, out _));
for (var i = 0; i < 0x100; i++)
{
((byte*)first)[i] = (byte)i;
}
Assert.True(memory.TryCopy(second + 0x10, first, 0x100));
for (var i = 0; i < 0x100; i++)
{
Assert.Equal((byte)i, ((byte*)second)[0x10 + i]);
}
Assert.True(memory.TryCopy(first, second + 0x20, 0x100));
for (var i = 0; i < 0x100; i++)
{
Assert.Equal(i < 0xF0 ? (byte)(i + 0x10) : (byte)0, ((byte*)first)[i]);
}
var privateRange = memory.AllocateAt(0, 0x4000, false);
Assert.NotEqual(0UL, privateRange);
Assert.True(memory.TryCopy(privateRange, first, 0x100));
Assert.True(memory.TryCopy(second + 0x200, privateRange, 0x100));
Assert.Equal(((byte*)first)[7], ((byte*)second)[0x207]);
}
[Fact]
public void MixedCopyWithProtectedPrivateMemoryDoesNotUpgradeUnderTheReadLock()
{
if (!Supported) return;
var host = HostViewMemory.Create();
var memoryHost = new QueryCountingHostMemory(PlatformMemory) { RejectExecutableProtection = true };
using var memory = new PhysicalVirtualMemory(hostMemory: memoryHost, viewHost: host, backingBytes: BackingSize);
var view = Hold(memory, host);
Assert.True(memory.TryMapBacked(view, Segment, 0, GuestPageProtection.Read, out _));
for (var i = 0; i < 0x40; i++)
{
((byte*)view)[i] = (byte)(0x80 + i);
}
var readOnly = MapPrivate(memory, host, 2 * Segment, ProgramHeaderFlags.Read);
var noAccess = readOnly + Segment;
memory.Map(noAccess, Segment, 0, ReadOnlySpan<byte>.Empty, (ProgramHeaderFlags)0);
Assert.True(memory.TryCopy(readOnly, view, 0x40));
Assert.True(memory.TryRead(readOnly, new byte[0x40]));
Assert.True(memory.TryCompare(readOnly, new ReadOnlySpan<byte>((void*)view, 0x40)));
Assert.True(memory.TryWrite(noAccess, BitConverter.GetBytes(Marker)));
Assert.True(memory.TryCopy(view + 0x100, noAccess, 8));
Assert.Equal(Marker, *(ulong*)(view + 0x100));
}
[Fact]
public void CopyAcrossOverlappingDiscontinuousBackingStagesTheBytes()
{
if (!Supported) return;
var host = HostViewMemory.Create();
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
var a = Hold(memory, host);
var b = Hold(memory, host);
Assert.True(memory.TryMapBacked(a, 2 * Segment, 0, GuestPageProtection.Read, out _));
Assert.True(memory.TryMapBacked(b, Segment, Segment, GuestPageProtection.Read, out _));
Assert.True(memory.TryMapBacked(b + Segment, Segment, 0, GuestPageProtection.Read, out _));
*(ulong*)a = 1;
*(ulong*)(a + Segment) = 2;
Assert.True(memory.TryCopy(b, a, 2 * Segment));
Assert.Equal(1UL, *(ulong*)b);
Assert.Equal(2UL, *(ulong*)(b + Segment));
}
[Fact]
public void SearchStaysBelowTheReferenceUpperBound()
{
if (!Supported) return;
const ulong bound = 0xFC_0000_0000;
var host = HostViewMemory.Create();
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
Assert.False(memory.TryHoldRangeAtOrAbove(bound, Segment, Segment, out _));
Assert.False(memory.TryHoldRangeAtOrAbove(bound - Segment, 2 * Segment, Segment, out _));
if (memory.TryHoldRangeAtOrAbove(bound - HoleSize(host), Segment, Segment, out var address))
{
Assert.True(address + Segment <= bound);
}
}
[Fact]
public void SearchChecksLowerAddressesBeforeReusingAHigherHole()
{
if (!Supported) return;
var host = HostViewMemory.Create();
var size = HoleSize(host);
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
var lowerAddress = ProbeGuestAddress(host, 3 * size);
var higherAddress = lowerAddress + 2 * size;
Assert.True(memory.TryHoldRange(higherAddress, size));
Assert.True(memory.TryHoldRangeAtOrAbove(lowerAddress, size, size, out var selectedAddress));
Assert.Equal(lowerAddress, selectedAddress);
}
[Fact]
public void PartialUnmapPreservesTheRemainingRegions()
{
if (!Supported) return;
var host = HostViewMemory.Create();
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
var address = Hold(memory, host);
Assert.True(memory.TryMapBacked(address, 3 * Segment, 0, GuestPageProtection.Read, out _));
Assert.True(memory.TryUnmapBacked(address + Segment, Segment));
Assert.True(memory.IsBackedView(address));
Assert.False(memory.IsBackedView(address + Segment));
Assert.True(memory.IsBackedView(address + 2 * Segment));
Assert.True(memory.TryMapBacked(address + Segment, Segment, Segment, GuestPageProtection.Read, out _));
Assert.True(memory.TryWrite(address, new byte[3 * Segment]));
Assert.True(memory.TryRead(address, new byte[3 * Segment]));
}
[Fact]
public void SearchSkipsOccupiedMemoryWithoutChangingIt()
{
if (!Supported) return;
var host = HostViewMemory.Create();
var size = HoleSize(host);
var candidate = ProbeGuestAddress(host, size);
var occupied = PlatformMemory.Allocate(candidate, size, HostPageProtection.ReadWrite);
Assert.NotEqual(0UL, occupied);
try
{
*(ulong*)occupied = Marker;
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
Assert.True(memory.TryHoldRangeAtOrAbove(occupied, Segment, Segment, out var found));
Assert.True(found >= occupied + size);
Assert.Equal(Marker, *(ulong*)occupied);
Assert.True(memory.TryMapBacked(found, Segment, 0, GuestPageProtection.Read, out _));
}
finally
{
Assert.True(PlatformMemory.Free(occupied));
}
}
[Fact]
public void AdjacentReservationsCanSupplyOneSpanningView()
{
if (!Supported) return;
var host = HostViewMemory.Create();
var size = HoleSize(host);
using var memory = new PhysicalVirtualMemory(viewHost: host, backingBytes: BackingSize);
ulong address = 0;
for (var attempt = 0; attempt < 8; attempt++)
{
var candidate = ProbeGuestAddress(host, 2 * size);
if (memory.TryHoldRange(candidate, size) && memory.TryHoldRange(candidate + size, size))
{
address = candidate;
break;
}
}
Assert.NotEqual(0UL, address);
Assert.True(memory.TryMapBacked(address, 2 * size, 0, GuestPageProtection.Read, out _));
*(ulong*)(address + size) = Marker;
Assert.True(memory.TryUnmapBacked(address, 2 * size));
memory.Dispose();
Assert.Equal(address, host.ReserveHole(address, 2 * size));
Assert.True(host.FreeHole(address, 2 * size));
}
}