sharpemu/tests/SharpEmu.Libs.Tests/Memory/GpuMemory/GuestGpuMemoryTests.cs

418 lines
14 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.HLE.GpuMemory;
using SharpEmu.Libs.Tests.Gpu.Scheduling;
using Xunit;
namespace SharpEmu.Libs.Tests.Memory.GpuMemory;
[Collection(GpuMemoryStateCollection.Name)]
public sealed class GuestGpuMemoryTests
{
private const GuestPageProtection ReadWrite = GuestPageProtection.Read | GuestPageProtection.Write;
private sealed class RecordingStores : IGuestBufferStore, IGuestImageStore
{
public List<string> Calls { get; } = new();
public bool BufferHandles { get; set; }
public bool ImageHandles { get; set; }
public Action? Recover { get; set; }
bool IGuestBufferStore.MarkCpuWrite(ulong address, ulong size)
{
Calls.Add($"buffer.write {address:X}+{size:X}");
Recover?.Invoke();
return BufferHandles;
}
public bool DownloadToCpu(ulong address, ulong size)
{
Calls.Add($"buffer.pull {address:X}+{size:X}");
Recover?.Invoke();
return BufferHandles;
}
bool IGuestImageStore.MarkCpuWrite(ulong address, ulong size)
{
Calls.Add($"image.write {address:X}+{size:X}");
return ImageHandles;
}
public void Unregister(ulong address, ulong size) => Calls.Add($"image.unregister {address:X}+{size:X}");
}
private sealed class InlineQueue : IGpuQueueRelay
{
public int Runs { get; private set; }
public bool IsGpuQueueThread { get; set; } = true;
public bool Accepting { get; set; } = true;
public void Post(Action work) => work();
public void RunOnGpuQueue(Action work)
{
Runs++;
work();
}
public bool TryRunOnGpuQueue(Action work)
{
if (!Accepting)
{
return false;
}
RunOnGpuQueue(work);
return true;
}
}
private sealed class RecordingScheduler : IGpuTickScheduler
{
public List<string> Calls { get; } = new();
public bool Active { get; set; }
public ulong CurrentTick { get; set; } = 7;
public bool InsideTickCallback { get; set; }
public void Finish()
{
Calls.Add("finish");
CurrentTick++;
}
public void WaitForPriorityOperations(ulong tick) => Calls.Add($"wait_priority {tick}");
public void FinishMemoryAccess()
{
var tick = CurrentTick;
Finish();
WaitForPriorityOperations(tick);
}
}
private readonly RecordingStores _stores = new();
private readonly GuestGpuMemory _memory;
public GuestGpuMemoryTests()
{
_memory = new GuestGpuMemory(new RecordingAddressSpace());
_memory.AttachStores(_stores, _stores);
}
[Fact]
public void Register_ThenCoversSubSpansOnly()
{
_memory.Register(0x10000, 0x4000, ReadWrite);
Assert.True(_memory.Covers(0x10000, 0x4000));
Assert.True(_memory.Covers(0x11000, 0x8));
Assert.False(_memory.Covers(0x13FF8, 0x10));
Assert.False(_memory.Covers(1UL << 40, 0x8));
Assert.Empty(_stores.Calls);
_memory.Unregister(0x10000, 0x4000);
Assert.False(_memory.Covers(0x11000, 0x8));
Assert.Equal(new[] { "buffer.write 10000+4000", "image.unregister 10000+4000" }, _stores.Calls);
_memory.Dispose();
}
[Theory]
[InlineData(FaultKind.Write, "buffer.write 10010+8", "image.write 10010+8")]
[InlineData(FaultKind.Read, "buffer.pull 10010+8")]
[InlineData(FaultKind.Execute)]
[InlineData(FaultKind.Unknown, "buffer.pull 10010+8")]
public void TryResolveFault_NotifiesStoresButDeclinesWhenNoneRecovers(FaultKind kind, params string[] expected)
{
_memory.Register(0x10000, 0x1000, ReadWrite);
Assert.False(_memory.TryResolveFault(kind, 0x10010));
Assert.Equal(expected, _stores.Calls);
_memory.Unregister(0x10000, 0x1000);
_memory.Dispose();
}
[Theory]
[InlineData(FaultKind.Write, true, false)]
[InlineData(FaultKind.Write, false, true)]
[InlineData(FaultKind.Read, true, false)]
public void TryResolveFault_ClaimsWhenAStoreRecovers(FaultKind kind, bool buffer, bool image)
{
_stores.BufferHandles = buffer;
_stores.ImageHandles = image;
_memory.Register(0x10000, 0x1000, ReadWrite);
Assert.True(_memory.TryResolveFault(kind, 0x10010));
_memory.Unregister(0x10000, 0x1000);
_memory.Dispose();
}
[Fact]
public void TryResolveFault_WriteNotifiesBothStoresEvenWhenTheFirstRecovers()
{
_stores.BufferHandles = true;
_memory.Register(0x10000, 0x1000, ReadWrite);
Assert.True(_memory.TryResolveFault(FaultKind.Write, 0x10010));
Assert.Equal(new[] { "buffer.write 10010+8", "image.write 10010+8" }, _stores.Calls);
_memory.Unregister(0x10000, 0x1000);
_memory.Dispose();
}
[Theory]
[InlineData(FaultKind.Read, true)]
[InlineData(FaultKind.Write, false)]
public void TryResolveFault_RetriesAfterRecoveryWhenAWatchIsRearmed(FaultKind kind, bool blockReads)
{
_stores.BufferHandles = true;
_memory.Register(0x10000, 0x1000, ReadWrite);
_memory.Pages.AddWatch(0x10000, 0x1000, blockReads);
var rearm = true;
_stores.Recover = () =>
{
_memory.Pages.RemoveWatch(0x10000, 0x1000, blockReads);
if (rearm)
_memory.Pages.AddWatch(0x10000, 0x1000, blockReads);
};
Assert.True(_memory.TryResolveFault(kind, 0x10010));
Assert.False(_memory.Pages.Allows(0x10010, kind));
rearm = false;
Assert.True(_memory.TryResolveFault(kind, 0x10010));
Assert.True(_memory.Pages.Allows(0x10010, kind));
Assert.Equal(2, _stores.Calls.Count(call => call.StartsWith("buffer.", StringComparison.Ordinal)));
_stores.Recover = null;
_memory.Unregister(0x10000, 0x1000);
_memory.Dispose();
}
[Fact]
public void TryResolveFault_OutsideMappedSpansIsDeclined()
{
_stores.BufferHandles = true;
_memory.Register(0x10000, 0x1000, ReadWrite);
Assert.False(_memory.TryResolveFault(FaultKind.Write, 0x20000));
Assert.False(_memory.TryResolveFault(FaultKind.Write, 0x10FFC));
Assert.False(_memory.MarkCpuWrite(0x20000, 0x10));
Assert.Empty(_stores.Calls);
Assert.True(_memory.MarkCpuWrite(0x10100, 0x10));
Assert.Equal(new[] { "buffer.write 10100+10", "image.write 10100+10" }, _stores.Calls);
_memory.Unregister(0x10000, 0x1000);
_memory.Dispose();
}
[Fact]
public void TryResolveFault_DeclinesWhatTheGuestForbidsBeforeAnyStore()
{
_stores.BufferHandles = true;
_stores.ImageHandles = true;
_memory.Register(0x10000, 0x1000, GuestPageProtection.Read);
_memory.Register(0x11000, 0x1000, GuestPageProtection.None);
Assert.False(_memory.TryResolveFault(FaultKind.Write, 0x10010));
Assert.False(_memory.TryResolveFault(FaultKind.Execute, 0x10010));
Assert.False(_memory.TryResolveFault(FaultKind.Read, 0x11010));
Assert.Empty(_stores.Calls);
Assert.True(_memory.TryResolveFault(FaultKind.Read, 0x10010));
Assert.Equal(new[] { "buffer.pull 10010+8" }, _stores.Calls);
_memory.Unregister(0x10000, 0x2000);
_memory.Dispose();
}
[Fact]
public void NoteProtected_AppliesTheDerivedProtectionOnCoveredRangesOnly()
{
var space = new RecordingAddressSpace();
using var memory = new GuestGpuMemory(space);
memory.AttachStores(_stores, _stores);
memory.Register(0x10000, 0x2000, ReadWrite);
Assert.Equal(new[] { (0x10000UL, 0x2000UL, ReadWrite) }, space.Protects);
Assert.False(memory.NoteProtected(0x20000, 0x1000, GuestPageProtection.Read));
Assert.Single(space.Protects);
Assert.True(memory.NoteProtected(0x10000, 0x1000, GuestPageProtection.Read));
Assert.Equal((0x10000UL, 0x1000UL, GuestPageProtection.Read), space.Protects[1]);
Assert.Equal(GuestPageProtection.Read, memory.Pages.Permissions.Lookup(0x10000));
Assert.Equal(ReadWrite, memory.Pages.Permissions.Lookup(0x11000));
memory.Unregister(0x10000, 0x2000);
Assert.Equal(ReadWrite, memory.Pages.Permissions.Lookup(0x10000));
}
[Fact]
public void AttachStores_NullStoresDeclineEveryFault()
{
_memory.AttachStores(null, null);
_memory.Register(0x10000, 0x1000, ReadWrite);
Assert.False(_memory.TryResolveFault(FaultKind.Write, 0x10010));
Assert.False(_memory.TryResolveFault(FaultKind.Read, 0x10010));
Assert.True(_memory.MarkCpuWrite(0x10010, 8));
Assert.Empty(_stores.Calls);
_memory.Unregister(0x10000, 0x1000);
_memory.Dispose();
}
[Fact]
public void Unregister_RunsOnTheGpuQueueWhenAttached()
{
var queue = new InlineQueue { IsGpuQueueThread = false };
_memory.Register(0x10000, 0x1000, ReadWrite);
_memory.AttachGpuQueue(queue, null);
_memory.Unregister(0x10000, 0x1000);
Assert.Equal(1, queue.Runs);
Assert.False(_memory.Covers(0x10000, 0x1000));
_memory.Dispose();
}
[Fact]
public void MappingChangeFinishesGpuWorkBeforeRunningTheTransaction()
{
var scheduler = new RecordingScheduler { Active = true };
var queue = new InlineQueue { IsGpuQueueThread = false };
_memory.AttachGpuQueue(queue, scheduler);
_memory.RunMappingChange(() => scheduler.Calls.Add("change"));
Assert.Equal(new[] { "finish", "wait_priority 7", "change" }, scheduler.Calls);
Assert.Equal(1, queue.Runs);
_memory.AttachGpuQueue(null, null);
_memory.Dispose();
}
[Fact]
public async Task MappingChangeWaitsForDetachWhenTheRelayCloses()
{
var queue = new InlineQueue { IsGpuQueueThread = false, Accepting = false };
var scheduler = new RecordingScheduler { Active = true };
_memory.AttachGpuQueue(queue, scheduler);
var changes = 0;
var completion = Task.Run(() => _memory.RunMappingChange(() => Interlocked.Increment(ref changes)));
try
{
Assert.False(await SchedulingTestSupport.CompletesWithin(completion, 100));
Assert.Equal(0, Volatile.Read(ref changes));
}
finally
{
_memory.AttachGpuQueue(null, null);
}
Assert.True(await SchedulingTestSupport.CompletesWithin(completion, 5000));
Assert.Equal(1, changes);
Assert.Empty(scheduler.Calls);
_memory.Dispose();
}
[Fact]
public void MappingChangeRejectsCompletionCallbacks()
{
var previous = PageGuard.OnFatal;
var fatals = new List<string>();
PageGuard.OnFatal = fatals.Add;
try
{
_memory.AttachGpuQueue(new InlineQueue(), new RecordingScheduler { InsideTickCallback = true });
var changed = false;
_memory.RunMappingChange(() => changed = true);
Assert.False(changed);
Assert.Single(fatals);
}
finally
{
PageGuard.OnFatal = previous;
_memory.AttachGpuQueue(null, null);
_memory.Dispose();
}
}
[Fact]
public void Unregister_DrainsTheSchedulerThroughTheTickItCaptured()
{
var scheduler = new RecordingScheduler { Active = true };
_memory.Register(0x10000, 0x1000, ReadWrite);
_memory.AttachGpuQueue(new InlineQueue(), scheduler);
_memory.Unregister(0x10000, 0x1000);
Assert.Equal(new[] { "finish", "wait_priority 7" }, scheduler.Calls);
Assert.Equal(new[] { "buffer.write 10000+1000", "image.unregister 10000+1000" }, _stores.Calls);
Assert.False(_memory.Covers(0x10000, 0x1000));
scheduler.Active = false;
_memory.Register(0x20000, 0x1000, ReadWrite);
_memory.Unregister(0x20000, 0x1000);
Assert.Equal(2, scheduler.Calls.Count);
_memory.Dispose();
}
[Fact]
public async Task Unregister_AfterClosureWaitsForDetachAndRunsWithoutTheScheduler()
{
var scheduler = new RecordingScheduler { Active = true };
var queue = new InlineQueue { IsGpuQueueThread = false, Accepting = false };
_memory.Register(0x10000, 0x1000, ReadWrite);
_memory.AttachGpuQueue(queue, scheduler);
var unregister = Task.Run(() => _memory.Unregister(0x10000, 0x1000));
Assert.False(await SchedulingTestSupport.CompletesWithin(unregister, 100));
Assert.Empty(_stores.Calls);
_memory.AttachGpuQueue(null, null);
Assert.True(await SchedulingTestSupport.CompletesWithin(unregister, 5000));
Assert.Equal(0, queue.Runs);
Assert.Empty(scheduler.Calls);
Assert.Equal(new[] { "buffer.write 10000+1000", "image.unregister 10000+1000" }, _stores.Calls);
Assert.False(_memory.Covers(0x10000, 0x1000));
_memory.Dispose();
}
[Fact]
public void Unregister_RefusesToRunFromATickCallbackBeforeDispatch()
{
var previous = PageGuard.OnFatal;
var fatals = new List<string>();
PageGuard.OnFatal = fatals.Add;
try
{
var queue = new InlineQueue();
_memory.Register(0x10000, 0x1000, ReadWrite);
_memory.AttachGpuQueue(queue, new RecordingScheduler { Active = true, InsideTickCallback = true });
_memory.Unregister(0x10000, 0x1000);
Assert.Single(fatals);
Assert.Contains("addr=0x0000000000010000", fatals[0]);
Assert.Equal(0, queue.Runs);
Assert.True(_memory.Covers(0x10000, 0x1000));
_memory.AttachGpuQueue(null, null);
_memory.Unregister(0x10000, 0x1000);
}
finally
{
PageGuard.OnFatal = previous;
}
_memory.Dispose();
}
}