525 lines
16 KiB
C#
525 lines
16 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Runtime.InteropServices;
|
|
using SharpEmu.HLE.Host;
|
|
|
|
namespace SharpEmu.HLE.GuestMemory;
|
|
|
|
public readonly record struct ViewRecord(ulong Address, ulong Size, ulong Offset, HostPageProtection Protection)
|
|
{
|
|
public bool WasRestored { get; init; }
|
|
}
|
|
|
|
public sealed unsafe class SharedBackingViews : IDisposable
|
|
{
|
|
internal static Action<string> OnFatal = message => Environment.FailFast(message);
|
|
|
|
private readonly IHostViewMemory _host;
|
|
private readonly HostBackingObject? _backing;
|
|
private readonly object _lock = new();
|
|
private readonly SortedList<ulong, ViewRecord> _views = new();
|
|
private bool _disposed;
|
|
|
|
public SharedBackingViews(IHostViewMemory host, ulong size)
|
|
{
|
|
_host = host;
|
|
_ = host.TryCreateBacking(size, out _backing, out _);
|
|
}
|
|
|
|
public bool IsAvailable => _backing != null && !_disposed;
|
|
|
|
public ulong AliasBase => _backing?.AliasBase ?? 0;
|
|
|
|
public ulong Size => _backing?.Size ?? 0;
|
|
|
|
public bool Clear(ulong offset, ulong size)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (!IsAvailable || !IsWithinBacking(offset, size))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
NativeMemory.Clear((void*)(AliasBase + offset), (nuint)size);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public bool TryWriteBacking(ulong address, ReadOnlySpan<byte> data)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (IsAvailable && TryFindRecord(address, (ulong)data.Length, out var record))
|
|
{
|
|
var offset = record.Offset + address - record.Address;
|
|
if (!IsWithinBacking(offset, (ulong)data.Length))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
data.CopyTo(new Span<byte>((void*)(AliasBase + offset), data.Length));
|
|
return true;
|
|
}
|
|
|
|
if (!TryCollectBackingSegments(address, (ulong)data.Length, out var pieces))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var (backing, dataOffset, bytes) in pieces)
|
|
{
|
|
data.Slice(dataOffset, bytes).CopyTo(new Span<byte>((void*)backing, bytes));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public bool TryReadBacking(ulong address, Span<byte> data)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
// A read inside one mapping needs no temporary segment list.
|
|
if (IsAvailable && TryFindRecord(address, (ulong)data.Length, out var record))
|
|
{
|
|
var offset = record.Offset + address - record.Address;
|
|
if (!IsWithinBacking(offset, (ulong)data.Length))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
new ReadOnlySpan<byte>((void*)(AliasBase + offset), data.Length).CopyTo(data);
|
|
return true;
|
|
}
|
|
|
|
if (!TryCollectBackingSegments(address, (ulong)data.Length, out var pieces))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var (backing, dataOffset, bytes) in pieces)
|
|
{
|
|
new ReadOnlySpan<byte>((void*)backing, bytes).CopyTo(data.Slice(dataOffset, bytes));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Use temporary storage if a copy segment can overwrite another segment's source.
|
|
public bool TryCopyBacking(ulong destination, ulong source, ulong size)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (IsAvailable && size <= int.MaxValue &&
|
|
TryFindRecord(source, size, out var sourceRecord) &&
|
|
TryFindRecord(destination, size, out var destinationRecord))
|
|
{
|
|
var sourceOffset = sourceRecord.Offset + source - sourceRecord.Address;
|
|
var destinationOffset = destinationRecord.Offset + destination - destinationRecord.Address;
|
|
if (!IsWithinBacking(sourceOffset, size) || !IsWithinBacking(destinationOffset, size))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Use the backing alias so CopyTo can detect overlap between different guest views.
|
|
new ReadOnlySpan<byte>((void*)(AliasBase + sourceOffset), (int)size)
|
|
.CopyTo(new Span<byte>((void*)(AliasBase + destinationOffset), (int)size));
|
|
return true;
|
|
}
|
|
|
|
if (!TryCollectBackingSegments(source, size, out var from) || !TryCollectBackingSegments(destination, size, out var to))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var chunks = CreateCopySegments(from, to);
|
|
if (!HasCrossSegmentOverlap(chunks))
|
|
{
|
|
foreach (var (fromPtr, toPtr, bytes) in chunks)
|
|
{
|
|
Buffer.MemoryCopy((void*)fromPtr, (void*)toPtr, bytes, bytes);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
var staging = new byte[size];
|
|
foreach (var (backing, dataOffset, bytes) in from)
|
|
{
|
|
new ReadOnlySpan<byte>((void*)backing, bytes).CopyTo(staging.AsSpan(dataOffset, bytes));
|
|
}
|
|
|
|
foreach (var (backing, dataOffset, bytes) in to)
|
|
{
|
|
staging.AsSpan(dataOffset, bytes).CopyTo(new Span<byte>((void*)backing, bytes));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public bool TryMapReservedRange(ulong address, ulong size, ulong offset, HostPageProtection protection, out HostViewFailure failure)
|
|
=> TryMapReservedRange(address, size, offset, protection, false, out failure);
|
|
|
|
private bool TryMapReservedRange(ulong address, ulong size, ulong offset, HostPageProtection protection,
|
|
bool wasRestored, out HostViewFailure failure)
|
|
{
|
|
if (!IsAvailable || !IsWithinBacking(offset, size))
|
|
{
|
|
failure = IsAvailable ? HostViewFailure.OffsetOutOfBounds : HostViewFailure.BackingUnavailable;
|
|
return false;
|
|
}
|
|
|
|
// Keep the mapping and its record under one lock to prevent disposal between them.
|
|
lock (_lock)
|
|
{
|
|
if (!_host.TryMapView(_backing!, address, offset, size, protection, out failure))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_disposed)
|
|
{
|
|
_ = _host.UnmapView(address, size);
|
|
failure = HostViewFailure.BackingUnavailable;
|
|
return false;
|
|
}
|
|
|
|
if (_views.ContainsKey(address))
|
|
{
|
|
OnFatal($"A backing view record already exists at 0x{address:X16}.");
|
|
}
|
|
|
|
_views[address] = new ViewRecord(address, size, offset, protection) { WasRestored = wasRestored };
|
|
}
|
|
|
|
failure = HostViewFailure.None;
|
|
return true;
|
|
}
|
|
|
|
public bool Unmap(ulong address, ulong size, out bool holePreserved)
|
|
{
|
|
holePreserved = false;
|
|
if (!IsAvailable || size == 0 || ulong.MaxValue - address < size)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var end = address + size;
|
|
var targets = new List<ViewRecord>();
|
|
lock (_lock)
|
|
{
|
|
var current = address;
|
|
while (current < end)
|
|
{
|
|
if (!TryFindRecord(current, 1, out var record))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var partSize = Math.Min(end, record.Address + record.Size) - current;
|
|
targets.Add(new ViewRecord(current, partSize, record.Offset + current - record.Address, record.Protection));
|
|
current += partSize;
|
|
}
|
|
}
|
|
|
|
var removed = new List<ViewRecord>();
|
|
foreach (var target in targets)
|
|
{
|
|
if (!TryUnmapSingleView(target.Address, target.Size, out var partPreserved))
|
|
{
|
|
RestoreMappingsInReverseOrder(removed);
|
|
return false;
|
|
}
|
|
|
|
if (!partPreserved)
|
|
{
|
|
OnFatal($"The address range was not kept reserved after unmapping the view at 0x{target.Address:X16}.");
|
|
}
|
|
|
|
removed.Add(target);
|
|
}
|
|
|
|
// Each removed view leaves a reserved range. Join these ranges into one free range.
|
|
if (removed.Count > 1 && !_host.JoinHoles(address, size))
|
|
{
|
|
RestoreMappingsInReverseOrder(removed);
|
|
return false;
|
|
}
|
|
|
|
holePreserved = true;
|
|
return true;
|
|
}
|
|
|
|
private bool TryUnmapSingleView(ulong address, ulong size, out bool holePreserved)
|
|
{
|
|
holePreserved = false;
|
|
var old = default(ViewRecord);
|
|
lock (_lock)
|
|
{
|
|
var index = RangeSearch.FindLastIndexAtOrBelow(_views, address);
|
|
if (index >= 0)
|
|
{
|
|
var record = _views.Values[index];
|
|
if (address + size <= record.Address + record.Size)
|
|
{
|
|
old = record;
|
|
_views.RemoveAt(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (old.Size == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_host.UnmapView(old.Address, old.Size))
|
|
{
|
|
RestoreViewRecord(old);
|
|
return false;
|
|
}
|
|
|
|
if (address == old.Address && size == old.Size)
|
|
{
|
|
holePreserved = true;
|
|
return true;
|
|
}
|
|
|
|
var leftSize = address - old.Address;
|
|
var rightAddress = address + size;
|
|
var rightSize = old.Address + old.Size - rightAddress;
|
|
var ok = true;
|
|
if (leftSize != 0 && leftSize != old.Size)
|
|
{
|
|
ok = _host.SplitHole(old.Address, leftSize) && ok;
|
|
}
|
|
|
|
if (rightSize != 0)
|
|
{
|
|
ok = _host.SplitHole(address, size) && ok;
|
|
}
|
|
|
|
if (leftSize != 0)
|
|
{
|
|
ok = TryMapReservedRange(old.Address, leftSize, old.Offset, old.Protection, true, out _) && ok;
|
|
}
|
|
|
|
if (rightSize != 0)
|
|
{
|
|
ok = TryMapReservedRange(rightAddress, rightSize, old.Offset + (rightAddress - old.Address), old.Protection, true, out _) && ok;
|
|
}
|
|
|
|
if (ok)
|
|
{
|
|
holePreserved = true;
|
|
return true;
|
|
}
|
|
|
|
if (leftSize != 0 && Contains(old.Address, leftSize) && !TryUnmapSingleView(old.Address, leftSize, out _))
|
|
{
|
|
OnFatal($"Could not remove the partial backing view at 0x{old.Address:X16} during recovery.");
|
|
}
|
|
|
|
if (rightSize != 0 && Contains(rightAddress, rightSize) && !TryUnmapSingleView(rightAddress, rightSize, out _))
|
|
{
|
|
OnFatal($"Could not remove the partial backing view at 0x{rightAddress:X16} during recovery.");
|
|
}
|
|
|
|
if (!_host.JoinHoles(old.Address, old.Size))
|
|
{
|
|
OnFatal($"Could not join the reserved ranges at 0x{old.Address:X16} during recovery.");
|
|
}
|
|
|
|
RestoreViewMapping(old);
|
|
return false;
|
|
}
|
|
|
|
internal bool IsRestoredView(ulong address)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return IsAvailable && TryFindRecord(address, 1, out var record) && record.WasRestored;
|
|
}
|
|
}
|
|
|
|
public bool Contains(ulong address, ulong size)
|
|
{
|
|
if (size == 0 || ulong.MaxValue - address < size)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
lock (_lock)
|
|
{
|
|
var end = address + size;
|
|
var current = address;
|
|
while (current < end)
|
|
{
|
|
if (!TryFindRecord(current, 1, out var record))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
current = Math.Min(end, record.Address + record.Size);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Remove all recorded views before releasing the backing alias and object.
|
|
public void Dispose()
|
|
{
|
|
List<ViewRecord> views;
|
|
lock (_lock)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
views = new List<ViewRecord>(_views.Values);
|
|
_views.Clear();
|
|
}
|
|
|
|
foreach (var view in views)
|
|
{
|
|
if (!_host.UnmapView(view.Address, view.Size))
|
|
{
|
|
OnFatal($"Could not remove the backing view at 0x{view.Address:X16} during disposal.");
|
|
}
|
|
}
|
|
|
|
_backing?.Dispose();
|
|
}
|
|
|
|
private bool TryCollectBackingSegments(ulong address, ulong size, out List<(ulong Backing, int DataOffset, int Bytes)> pieces)
|
|
{
|
|
pieces = new List<(ulong, int, int)>();
|
|
if (!IsAvailable || size == 0 || ulong.MaxValue - address < size)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var end = address + size;
|
|
var current = address;
|
|
while (current < end)
|
|
{
|
|
if (!TryFindRecord(current, 1, out var record))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var bytes = Math.Min(end, record.Address + record.Size) - current;
|
|
var offset = record.Offset + current - record.Address;
|
|
if (!IsWithinBacking(offset, bytes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
pieces.Add((AliasBase + offset, (int)(current - address), (int)bytes));
|
|
current += bytes;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static List<(ulong From, ulong To, int Bytes)> CreateCopySegments(
|
|
List<(ulong Backing, int DataOffset, int Bytes)> from,
|
|
List<(ulong Backing, int DataOffset, int Bytes)> to)
|
|
{
|
|
var chunks = new List<(ulong, ulong, int)>();
|
|
var i = 0;
|
|
var j = 0;
|
|
var offset = 0;
|
|
while (i < from.Count && j < to.Count)
|
|
{
|
|
var fromEnd = from[i].DataOffset + from[i].Bytes;
|
|
var toEnd = to[j].DataOffset + to[j].Bytes;
|
|
var end = Math.Min(fromEnd, toEnd);
|
|
chunks.Add((
|
|
from[i].Backing + (ulong)(offset - from[i].DataOffset),
|
|
to[j].Backing + (ulong)(offset - to[j].DataOffset),
|
|
end - offset));
|
|
offset = end;
|
|
i += fromEnd == end ? 1 : 0;
|
|
j += toEnd == end ? 1 : 0;
|
|
}
|
|
|
|
return chunks;
|
|
}
|
|
|
|
private static bool HasCrossSegmentOverlap(List<(ulong From, ulong To, int Bytes)> chunks)
|
|
{
|
|
for (var a = 0; a < chunks.Count; a++)
|
|
{
|
|
for (var b = 0; b < chunks.Count; b++)
|
|
{
|
|
if (a != b && chunks[a].From < chunks[b].To + (ulong)chunks[b].Bytes &&
|
|
chunks[b].To < chunks[a].From + (ulong)chunks[a].Bytes)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryFindRecord(ulong address, ulong size, out ViewRecord record)
|
|
{
|
|
record = default;
|
|
if (size == 0 || ulong.MaxValue - address < size)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var index = RangeSearch.FindLastIndexAtOrBelow(_views, address);
|
|
if (index < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var candidate = _views.Values[index];
|
|
if (address + size > candidate.Address + candidate.Size)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
record = candidate;
|
|
return true;
|
|
}
|
|
|
|
private bool IsWithinBacking(ulong offset, ulong size) =>
|
|
size != 0 && offset < Size && size <= Size - offset;
|
|
|
|
private void RestoreViewRecord(ViewRecord record)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_views[record.Address] = record;
|
|
}
|
|
}
|
|
|
|
private void RestoreViewMapping(ViewRecord record)
|
|
{
|
|
if (!TryMapReservedRange(record.Address, record.Size, record.Offset, record.Protection, true, out _))
|
|
{
|
|
OnFatal($"Could not restore the backing view at 0x{record.Address:X16}.");
|
|
}
|
|
}
|
|
|
|
private void RestoreMappingsInReverseOrder(List<ViewRecord> records)
|
|
{
|
|
for (var index = records.Count - 1; index >= 0; index--)
|
|
{
|
|
RestoreViewMapping(records[index]);
|
|
}
|
|
}
|
|
}
|