mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
7494792249
VirtualMemory: 4 new tests covering unmapped address returns false, zero-length operations, page-boundary access, and cross-gap failures. PhysicalVirtualMemory: 5 new tests covering lazy commit on demand, reserve-only GetPointer, unmapped GetPointer returns null, free-list first-fit reuse, and coalescing both neighbours on middle-range free. Total: 9 new tests, 18 passed (8 VirtualMemory + 5 PhysicalVirtualMemory + 5 GuestMemoryAllocator). No production code changes.
230 lines
8.6 KiB
C#
230 lines
8.6 KiB
C#
// 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;
|
|
|
|
// PhysicalVirtualMemory is the host-backed (identity-mapped) implementation.
|
|
// Reserve-only regions (> 4 GiB, non-executable) defer commit until first
|
|
// access; TryAllocateGuestMemory serves a first-fit free-list with coalescing.
|
|
// These tests pin that behaviour through fake IHostMemory implementations.
|
|
public sealed class PhysicalVirtualMemoryTests
|
|
{
|
|
// 1. Lazy commit: a reserve-only region has its pages committed on demand
|
|
// when read; freshly committed pages read as zero.
|
|
[Fact]
|
|
public void LazyReadCommitsPageOnDemandAndReadsZero()
|
|
{
|
|
using var host = new LazyZeroedHostMemory();
|
|
using var memory = new PhysicalVirtualMemory(host);
|
|
|
|
// > 4 GiB, non-executable -> reserve-only with lazy commit.
|
|
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
|
|
Assert.NotEqual(0UL, address);
|
|
|
|
// Discard the priming commits AllocateAt issues up front; we want to
|
|
// observe the on-demand commit triggered by the read itself.
|
|
host.CommitCalls.Clear();
|
|
|
|
var buffer = new byte[1];
|
|
Assert.True(memory.TryRead(address, buffer));
|
|
Assert.Equal(0, buffer[0]);
|
|
|
|
// The touched page (page-aligned to `address`) was committed on demand.
|
|
var page = address & ~0xFFFUL;
|
|
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
|
|
}
|
|
|
|
// 2. Reserve-only region: GetPointer commits the page before returning it,
|
|
// so callers receive a valid (non-null) pointer. An unmapped address yields null.
|
|
[Fact]
|
|
public unsafe void GetPointerOnReserveOnlyRegionCommitsAndReturnsValidPointer()
|
|
{
|
|
using var host = new LazyZeroedHostMemory();
|
|
using var memory = new PhysicalVirtualMemory(host);
|
|
|
|
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
|
|
host.CommitCalls.Clear();
|
|
|
|
var pointer = memory.GetPointer(address + 0x123);
|
|
Assert.NotEqual(0UL, (ulong)pointer);
|
|
Assert.Equal(address + 0x123, (ulong)pointer);
|
|
|
|
var page = (address + 0x123) & ~0xFFFUL;
|
|
Assert.Equal([(page, 0x1000UL, HostPageProtection.ReadWrite)], host.CommitCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void GetPointerOnUnmappedAddressReturnsNull()
|
|
{
|
|
using var host = new LazyZeroedHostMemory();
|
|
using var memory = new PhysicalVirtualMemory(host);
|
|
|
|
Assert.Equal(0UL, (ulong)memory.GetPointer(0x0001_0000));
|
|
}
|
|
|
|
// 3. Free-list reuse: a freed range is served back by first-fit allocation,
|
|
// preferring the lowest fitting free range over the larger trailing span.
|
|
[Fact]
|
|
public void FreedRangeIsReusedByFirstFitAllocation()
|
|
{
|
|
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
|
|
|
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
|
|
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var second));
|
|
Assert.NotEqual(first, second);
|
|
Assert.True(memory.TryFreeGuestMemory(first));
|
|
|
|
// A smaller allocation must reuse first's freed slot (lowest fitting range),
|
|
// not the larger trailing free range.
|
|
Assert.True(memory.TryAllocateGuestMemory(0x2000, 0x1000, out var reused));
|
|
Assert.Equal(first, reused);
|
|
}
|
|
|
|
// 4. Coalescing: freeing the middle of three adjacent ranges merges both the
|
|
// left and right free neighbours in a single TryFreeGuestMemory call,
|
|
// restoring the full span for subsequent first-fit reuse.
|
|
[Fact]
|
|
public void FreeingMiddleRangeCoalescesBothNeighbours()
|
|
{
|
|
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
|
|
|
// Three adjacent 0x1000 allocations: offsets 0x1000, 0x2000, 0x3000.
|
|
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var first));
|
|
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var second));
|
|
Assert.True(memory.TryAllocateGuestMemory(0x1000, 0x1000, out var third));
|
|
|
|
// Free the outer ranges first, leaving two separate free ranges.
|
|
Assert.True(memory.TryFreeGuestMemory(first));
|
|
Assert.True(memory.TryFreeGuestMemory(third));
|
|
|
|
// Freeing the middle range must coalesce both neighbours at once.
|
|
Assert.True(memory.TryFreeGuestMemory(second));
|
|
|
|
// The whole arena is now one coalesced free range; a full-arena allocation
|
|
// reuses first's base address.
|
|
Assert.True(memory.TryAllocateGuestMemory(0x000F_F000, 0x1000, out var coalesced));
|
|
Assert.Equal(first, coalesced);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Host memory backed by a single real, zero-initialised page. Reserve/Allocate
|
|
/// report the page-aligned buffer address so lazy-commit read paths can actually
|
|
/// dereference the returned pointer. Query always reports Reserved, so
|
|
/// EnsureRangeCommitted issues a Commit on first access.
|
|
/// </summary>
|
|
private sealed unsafe class LazyZeroedHostMemory : IHostMemory, IDisposable
|
|
{
|
|
private readonly void* _allocation;
|
|
private readonly ulong _address;
|
|
private bool _freed;
|
|
|
|
public LazyZeroedHostMemory()
|
|
{
|
|
_allocation = System.Runtime.InteropServices.NativeMemory.AllocZeroed(0x2000);
|
|
_address = ((ulong)_allocation + 0xFFF) & ~0xFFFUL;
|
|
}
|
|
|
|
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
|
|
|
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
|
|
|
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => _address;
|
|
|
|
public bool Commit(ulong address, ulong size, HostPageProtection protection)
|
|
{
|
|
CommitCalls.Add((address, size, protection));
|
|
return true;
|
|
}
|
|
|
|
public bool Free(ulong address)
|
|
{
|
|
// The real buffer is released in Dispose; keep Free a no-op so
|
|
// PhysicalVirtualMemory.Clear does not double-free it.
|
|
return 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)
|
|
{
|
|
var pageAddress = address & ~0xFFFUL;
|
|
info = new HostRegionInfo(
|
|
pageAddress,
|
|
pageAddress,
|
|
0x1000,
|
|
HostRegionState.Reserved,
|
|
0,
|
|
HostPageProtection.NoAccess,
|
|
0,
|
|
0);
|
|
return true;
|
|
}
|
|
|
|
public void FlushInstructionCache(ulong address, ulong size)
|
|
{
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_freed)
|
|
{
|
|
System.Runtime.InteropServices.NativeMemory.Free(_allocation);
|
|
_freed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Minimal host memory for free-list tests: Allocate honours the desired
|
|
// address (or a fallback), everything else succeeds as a no-op. The guest
|
|
// allocation arena never dereferences, so no real backing is required.
|
|
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)
|
|
{
|
|
}
|
|
}
|
|
}
|