// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE.GuestMemory; 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 SharedBackingTransferTests { [Theory] [InlineData(false)] [InlineData(true)] public void SingleMappingTransfersDoNotAllocateTemporarySegments(bool copy) { if (!Supported) return; using var mapping = new TransferMappings(); var data = Enumerable.Range(0, 32).Select(value => (byte)value).ToArray(); var source = mapping.Address + Segment - (ulong)data.Length; var destination = mapping.Address + 2 * Segment - (ulong)data.Length; Assert.True(mapping.Store.TryWriteBacking(source, data)); var succeeded = true; void Transfer(int count) { for (var iteration = 0; iteration < count; iteration++) { succeeded &= copy ? mapping.Store.TryCopyBacking(destination, source, (ulong)data.Length) : mapping.Store.TryWriteBacking(destination, data); } } var allocatedBytes = AllocationMeasurement.SteadyState(() => Transfer(256), () => Transfer(1024)); Assert.True(succeeded); Assert.Equal(0L, allocatedBytes); Assert.Equal(data, new ReadOnlySpan((void*)destination, data.Length).ToArray()); } [Theory] [InlineData(false, 0, 1)] [InlineData(false, 1, 0)] [InlineData(false, 0, 0)] [InlineData(true, 0, 1)] [InlineData(true, 1, 0)] [InlineData(true, 0, 0)] public void SingleMappingCopiesPreserveOverlappingBytes(bool separateViews, int sourceOffset, int destinationOffset) { if (!Supported) return; using var mapping = new TransferMappings(aliasSecondView: true); var expected = Enumerable.Range(0, 64).Select(value => (byte)value).ToArray(); Assert.True(mapping.Store.TryWriteBacking(mapping.Address, expected)); expected.AsSpan(sourceOffset, 32).CopyTo(expected.AsSpan(destinationOffset, 32)); var destination = mapping.Address + (separateViews ? Segment : 0) + (ulong)destinationOffset; Assert.True(mapping.Store.TryCopyBacking(destination, mapping.Address + (ulong)sourceOffset, 32)); Assert.Equal(expected, new ReadOnlySpan((void*)mapping.Address, expected.Length).ToArray()); } [Fact] public void CrossMappingTransfersPreserveBytes() { if (!Supported) return; using var mapping = new TransferMappings(); var expected = Enumerable.Range(0, 64).Select(value => (byte)value).ToArray(); var source = mapping.Address + Segment - 32; Assert.True(mapping.Store.TryWriteBacking(source, expected)); expected.AsSpan(0, 48).CopyTo(expected.AsSpan(4, 48)); Assert.True(mapping.Store.TryCopyBacking(source + 4, source, 48)); Assert.Equal(expected, new ReadOnlySpan((void*)source, expected.Length).ToArray()); } [Fact] public void InvalidTransfersLeaveBackingBytesUnchanged() { if (!Supported) return; using var mapping = new TransferMappings(); var data = Enumerable.Repeat((byte)0xA5, (int)(2 * Segment)).ToArray(); Assert.True(mapping.Store.TryWriteBacking(mapping.Address, data)); var end = mapping.Address + 2 * Segment; var replacement = new byte[8]; Assert.False(mapping.Store.TryWriteBacking(end - 4, replacement)); Assert.False(mapping.Store.TryWriteBacking(ulong.MaxValue - 3, replacement)); Assert.False(mapping.Store.TryWriteBacking(mapping.Address, ReadOnlySpan.Empty)); Assert.False(mapping.Store.TryCopyBacking(end - 4, mapping.Address, 8)); Assert.False(mapping.Store.TryCopyBacking(mapping.Address, end - 4, 8)); Assert.False(mapping.Store.TryCopyBacking(mapping.Address, ulong.MaxValue - 3, 8)); Assert.False(mapping.Store.TryCopyBacking(ulong.MaxValue - 3, mapping.Address, 8)); Assert.False(mapping.Store.TryCopyBacking(mapping.Address, mapping.Address, 0)); Assert.False(mapping.Store.TryCopyBacking(mapping.Address, mapping.Address, ulong.MaxValue)); Assert.Equal(data, new ReadOnlySpan((void*)mapping.Address, data.Length).ToArray()); } [Fact] public void SingleMappingCopyUsesBackingAliasForProtectedViews() { if (!Supported) return; using var mapping = new TransferMappings(); var data = BitConverter.GetBytes(Marker); Assert.True(mapping.Store.TryWriteBacking(mapping.Address, data)); Assert.True(mapping.Host.ChangeAccess(mapping.Address, Segment, HostPageProtection.NoAccess)); Assert.True(mapping.Host.ChangeAccess(mapping.Address + Segment, Segment, HostPageProtection.ReadOnly)); Assert.True(mapping.Store.TryCopyBacking(mapping.Address + Segment, mapping.Address, (ulong)data.Length)); Assert.Equal(Marker, *(ulong*)(mapping.Address + Segment)); } [Fact] public void AResolvedAliasReadsTheViewBytesUntilTheMappingChanges() { if (!Supported) return; using var mapping = new TransferMappings(); Assert.True(mapping.Store.TryWriteBacking(mapping.Address + Segment + 8, BitConverter.GetBytes(Marker))); var snapshot = mapping.Store.AliasSnapshot; Assert.True(mapping.Store.TryEnterAliasAccess()); try { Assert.True(mapping.Store.TryResolveAlias(mapping.Address + Segment, 16, out var alias)); Assert.Equal(Marker, *(ulong*)(alias + 8)); Assert.Same(snapshot, mapping.Store.AliasSnapshot); Assert.False(mapping.Store.TryResolveAlias(mapping.Address + Segment - 8, 16, out _)); } finally { mapping.Store.ExitAliasAccess(); } Assert.True(mapping.Store.Unmap(mapping.Address + Segment, Segment, out _)); Assert.NotSame(snapshot, mapping.Store.AliasSnapshot); Assert.False(mapping.Store.TryResolveAlias(mapping.Address + Segment, 16, out _)); } private sealed class TransferMappings : IDisposable { public IHostViewMemory Host { get; } = HostViewMemory.Create(); public SharedBackingViews Store { get; } public ulong Address { get; } private readonly ulong _holeSize; public TransferMappings(bool aliasSecondView = false) { Store = new SharedBackingViews(Host, BackingSize); _holeSize = HoleSize(Host); Address = ReserveFreeHole(Host, _holeSize); Assert.True(Host.SplitHole(Address, Segment)); Assert.True(Host.SplitHole(Address + Segment, Segment)); Assert.True(Store.TryMapReservedRange(Address, Segment, Segment, HostPageProtection.ReadWrite, out _)); Assert.True(Store.TryMapReservedRange(Address + Segment, Segment, aliasSecondView ? Segment : 2 * Segment, HostPageProtection.ReadWrite, out _)); } public void Dispose() { Store.Dispose(); Assert.True(Host.JoinHoles(Address, _holeSize)); Assert.True(Host.FreeHole(Address, _holeSize)); } } }