456 lines
16 KiB
C#
456 lines
16 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.Gpu.Scheduling;
|
|
using SharpEmu.Libs.Tests.Memory.GpuMemory;
|
|
using Xunit;
|
|
using static SharpEmu.Libs.Tests.Gpu.Scheduling.SchedulingTestSupport;
|
|
|
|
namespace SharpEmu.Libs.Tests.Gpu.Scheduling;
|
|
|
|
[Collection(SchedulingStateCollection.Name)]
|
|
public sealed class SubmissionSchedulerTests
|
|
{
|
|
private readonly FakeTickDevice _device = new();
|
|
private readonly RecordingRenderingState _rendering = new();
|
|
|
|
[Fact]
|
|
public void FirstSubmitSignalsTickOneAndFlushBeginsTheNextBuffer()
|
|
{
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
Assert.True(scheduler.Active);
|
|
Assert.False(scheduler.Current.IsInvalid);
|
|
Assert.Equal(new[] { "read", "allocate 4", "begin b0" }, _device.Log);
|
|
|
|
Assert.Equal(1UL, scheduler.Submit());
|
|
|
|
Assert.Equal(2UL, scheduler.CurrentTick);
|
|
Assert.True(scheduler.Current.IsInvalid);
|
|
Assert.Equal(new[] { "end b0", "submit b0 tick=1" }, _device.Log.Skip(3));
|
|
Assert.Equal((1UL, 0, 1), (_device.Submits[0].Tick, _device.Submits[0].Waits, _device.Submits[0].Signals));
|
|
|
|
var bundle = new SubmitBundle();
|
|
bundle.AddWait(0x77, 3);
|
|
scheduler.BeginCommand();
|
|
scheduler.Flush(bundle);
|
|
Assert.Equal((2UL, 1, 1), (_device.Submits[1].Tick, _device.Submits[1].Waits, _device.Submits[1].Signals));
|
|
Assert.Equal((1, 0), (bundle.WaitCount, bundle.SignalCount));
|
|
|
|
scheduler.Flush(bundle);
|
|
Assert.Equal((3UL, 1, 1), (_device.Submits[2].Tick, _device.Submits[2].Waits, _device.Submits[2].Signals));
|
|
Assert.Equal((1, 0), (bundle.WaitCount, bundle.SignalCount));
|
|
Assert.Equal("begin b3", _device.Log[^1]);
|
|
_device.CompleteOnSubmit = true;
|
|
}
|
|
|
|
[Fact]
|
|
public void SubmitRejectsAFullSignalListAndAMissingBuffer()
|
|
{
|
|
using var fatal = new FatalScope();
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
var bundle = new SubmitBundle();
|
|
bundle.AddSignal(1);
|
|
bundle.AddSignal(2);
|
|
bundle.AddSignal(3);
|
|
Assert.Throws<SchedulerFatalException>(() => bundle.AddSignal(4));
|
|
Assert.Throws<SchedulerFatalException>(() => scheduler.Submit(bundle));
|
|
|
|
_device.CompleteOnSubmit = true;
|
|
scheduler.Submit();
|
|
Assert.Throws<SchedulerFatalException>(() => scheduler.Submit());
|
|
Assert.Equal(3, fatal.Messages.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void WaitOnTheCurrentTickSubmitsFirst()
|
|
{
|
|
using var fatal = new FatalScope();
|
|
_device.CompleteOnSubmit = true;
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
|
|
scheduler.Wait(1);
|
|
|
|
Assert.Equal(new[] { "end b0", "submit b0 tick=1", "read", "begin b1" }, _device.Log.Skip(3));
|
|
Assert.Equal(2UL, scheduler.CurrentTick);
|
|
|
|
scheduler.Wait(1);
|
|
Assert.Equal(7, _device.Log.Length);
|
|
Assert.Throws<SchedulerFatalException>(() => scheduler.Wait(5));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UrgentWorkRunsAtRetirementAndDeferredWorkAfterSameTickUrgentWork()
|
|
{
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
var order = new List<string>();
|
|
void Add(string entry)
|
|
{
|
|
lock (order)
|
|
{
|
|
order.Add(entry);
|
|
}
|
|
}
|
|
|
|
scheduler.QueuePriorityCompletionAction(() => Add("urgent1"));
|
|
scheduler.QueueCompletionAction(() => Add("deferred1"));
|
|
scheduler.Submit();
|
|
Assert.Empty(order);
|
|
|
|
_device.Complete(1);
|
|
Assert.True(WaitUntil(() => order.Count == 1));
|
|
Assert.Equal(new[] { "urgent1" }, order);
|
|
scheduler.RunCompletedOperations();
|
|
Assert.Equal(new[] { "urgent1", "deferred1" }, order);
|
|
|
|
using var gate = new ManualResetEventSlim();
|
|
scheduler.BeginCommand();
|
|
scheduler.QueuePriorityCompletionAction(() =>
|
|
{
|
|
Add("urgent2");
|
|
gate.Wait();
|
|
Add("urgent2_end");
|
|
});
|
|
scheduler.QueueCompletionAction(() => Add("deferred2"));
|
|
scheduler.Submit();
|
|
_device.Complete(2);
|
|
Assert.True(WaitUntil(() => order.Contains("urgent2")));
|
|
|
|
var pop = Task.Run(scheduler.RunCompletedOperations);
|
|
Assert.False(await CompletesWithin(pop, 100));
|
|
gate.Set();
|
|
Assert.True(await CompletesWithin(pop, 5000));
|
|
Assert.Equal(new[] { "urgent1", "deferred1", "urgent2", "urgent2_end", "deferred2" }, order);
|
|
}
|
|
|
|
[Fact]
|
|
public void FinishSubmitsWaitsBeginsAgainAndPopsDeferredWork()
|
|
{
|
|
_device.CompleteOnSubmit = true;
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
var ran = false;
|
|
scheduler.QueueCompletionAction(() => ran = true);
|
|
|
|
scheduler.Finish();
|
|
|
|
Assert.True(ran);
|
|
Assert.False(scheduler.Current.IsInvalid);
|
|
Assert.Equal(new[] { "end b0", "submit b0 tick=1", "read", "begin b1", "read" }, _device.Log.Skip(3));
|
|
Assert.True(scheduler.IsTickComplete(1));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeferredWorkWhileDrainingRunsInlineFromTheOwnerAndBlocksOtherThreadsUntilClosed()
|
|
{
|
|
_device.CompleteOnSubmit = true;
|
|
var scheduler = NewActiveScheduler(_device, _rendering);
|
|
var order = new List<string>();
|
|
using var gate = new ManualResetEventSlim();
|
|
scheduler.QueueCompletionAction(() =>
|
|
{
|
|
order.Add("op1");
|
|
scheduler.QueueCompletionAction(() => order.Add("op2"));
|
|
scheduler.QueuePriorityCompletionAction(() => order.Add("op2_urgent"));
|
|
gate.Wait();
|
|
order.Add("op1_end");
|
|
});
|
|
|
|
var shutdown = Task.Run(scheduler.Shutdown);
|
|
Assert.True(WaitUntil(() => order.Count == 3));
|
|
|
|
var other = Task.Run(() => scheduler.QueueCompletionAction(() => order.Add("op3")));
|
|
Assert.False(await CompletesWithin(other, 100));
|
|
gate.Set();
|
|
Assert.True(await CompletesWithin(shutdown, 5000));
|
|
Assert.True(await CompletesWithin(other, 5000));
|
|
Assert.Equal(new[] { "op1", "op2", "op2_urgent", "op1_end", "op3" }, order);
|
|
scheduler.Dispose();
|
|
}
|
|
|
|
[Fact]
|
|
public void PriorityWaitsFromTheOwnCallbackAreFatalButAnotherSchedulerMayWait()
|
|
{
|
|
using var fatal = new FatalScope();
|
|
_device.CompleteOnSubmit = true;
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
var inside = false;
|
|
scheduler.QueueCompletionAction(() =>
|
|
{
|
|
inside = SubmissionScheduler.InDeferredOperation;
|
|
scheduler.WaitForAllPriorityOperations();
|
|
});
|
|
scheduler.Submit();
|
|
Assert.False(SubmissionScheduler.InDeferredOperation);
|
|
|
|
Assert.Throws<SchedulerFatalException>(scheduler.RunCompletedOperations);
|
|
Assert.True(inside);
|
|
Assert.False(SubmissionScheduler.InDeferredOperation);
|
|
Assert.Contains("own deferred callback", fatal.Messages.Single());
|
|
|
|
var otherDevice = new FakeTickDevice { CompleteOnSubmit = true };
|
|
using var other = NewActiveScheduler(otherDevice);
|
|
var waited = false;
|
|
other.QueueCompletionAction(() =>
|
|
{
|
|
scheduler.WaitForAllPriorityOperations();
|
|
scheduler.WaitForPriorityOperations(1);
|
|
waited = true;
|
|
});
|
|
other.Submit();
|
|
other.RunCompletedOperations();
|
|
Assert.True(waited);
|
|
|
|
scheduler.BeginCommand();
|
|
scheduler.QueueCompletionAction(() => scheduler.Shutdown());
|
|
scheduler.Submit();
|
|
Assert.Throws<SchedulerFatalException>(scheduler.RunCompletedOperations);
|
|
Assert.Equal("Cannot stop the scheduler from its own deferred callback.", fatal.Messages[1]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ShutdownSubmitsTheOpenBufferJoinsTheWorkerAndIsIdempotent()
|
|
{
|
|
using var fatal = new FatalScope();
|
|
_device.CompleteOnSubmit = true;
|
|
var scheduler = NewActiveScheduler(_device, _rendering);
|
|
var runs = 0;
|
|
scheduler.QueueCompletionAction(() => Interlocked.Increment(ref runs));
|
|
scheduler.QueuePriorityCompletionAction(() => Interlocked.Increment(ref runs));
|
|
|
|
var concurrent = Task.WhenAll(Task.Run(scheduler.Shutdown), Task.Run(scheduler.Shutdown));
|
|
scheduler.Shutdown();
|
|
Assert.True(await CompletesWithin(concurrent, 5000));
|
|
|
|
Assert.Equal(2, runs);
|
|
Assert.Contains("submit b0 tick=1", _device.Log);
|
|
Assert.False(scheduler.PriorityWorkerAlive);
|
|
Assert.True(scheduler.Current.IsInvalid);
|
|
scheduler.Shutdown();
|
|
|
|
var late = false;
|
|
scheduler.QueueCompletionAction(() => late = true);
|
|
Assert.True(late);
|
|
Assert.Throws<SchedulerFatalException>(() => scheduler.Begin(new SubmissionContext()));
|
|
|
|
scheduler.Dispose();
|
|
Assert.Equal("dispose", _device.Log[^1]);
|
|
}
|
|
|
|
[Fact]
|
|
public void DisposeFromTheOwnCallbackDefersToTheOwnerAndReleasesTheDeviceOnce()
|
|
{
|
|
_device.CompleteOnSubmit = true;
|
|
var scheduler = NewActiveScheduler(_device, _rendering);
|
|
var disposedInsideCallback = false;
|
|
scheduler.QueueCompletionAction(() =>
|
|
{
|
|
scheduler.Dispose();
|
|
disposedInsideCallback = _device.Log.Contains("dispose");
|
|
});
|
|
|
|
scheduler.Shutdown();
|
|
|
|
Assert.False(disposedInsideCallback);
|
|
Assert.Equal(1, _device.Log.Count(entry => entry == "dispose"));
|
|
Assert.Equal("dispose", _device.Log[^1]);
|
|
scheduler.Dispose();
|
|
Assert.Equal(1, _device.Log.Count(entry => entry == "dispose"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RepeatedAndConcurrentDisposeReleaseTheDeviceOnceAfterClosure()
|
|
{
|
|
_device.CompleteOnSubmit = true;
|
|
var scheduler = NewActiveScheduler(_device, _rendering);
|
|
using var gate = new ManualResetEventSlim();
|
|
scheduler.QueueCompletionAction(() => gate.Wait());
|
|
|
|
var disposals = Task.WhenAll(Task.Run(scheduler.Dispose), Task.Run(scheduler.Dispose), Task.Run(scheduler.Dispose));
|
|
Assert.False(await CompletesWithin(disposals, 100));
|
|
Assert.DoesNotContain("dispose", _device.Log);
|
|
|
|
gate.Set();
|
|
Assert.True(await CompletesWithin(disposals, 5000));
|
|
scheduler.Dispose();
|
|
Assert.Equal(1, _device.Log.Count(entry => entry == "dispose"));
|
|
Assert.Equal("dispose", _device.Log[^1]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ShutdownBeforeActivationIsClean()
|
|
{
|
|
var scheduler = new SubmissionScheduler(_device, _rendering);
|
|
Assert.False(scheduler.Active);
|
|
|
|
scheduler.Dispose();
|
|
|
|
Assert.Equal(new[] { "read", "dispose" }, _device.Log);
|
|
Assert.Empty(_device.Submits);
|
|
Assert.False(scheduler.PriorityWorkerAlive);
|
|
}
|
|
|
|
[Fact]
|
|
public void SubmitFailureReportsTheDebugFields()
|
|
{
|
|
using var fatal = new FatalScope();
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
_device.FailSubmit = true;
|
|
scheduler.Current.SetDebugInfo((uint)RecordedOperation.EopInterrupt, 99, 1, 2, 3, 4, 0xABCD);
|
|
|
|
Assert.Throws<SchedulerFatalException>(() => scheduler.Submit());
|
|
|
|
Assert.Equal(
|
|
"vkQueueSubmit failed: VK_ERROR_DEVICE_LOST, tick=1 debug_op=4 debug_submit=99 args=1,2,3,4,0x000000000000ABCD",
|
|
fatal.Messages.Single());
|
|
_device.FailSubmit = false;
|
|
_device.CompleteOnSubmit = true;
|
|
}
|
|
|
|
[Fact]
|
|
public void SubmissionHistoryIsBoundedOrderedAndResetsRecordingFields()
|
|
{
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
_device.CompleteOnSubmit = true;
|
|
for (uint index = 1; index <= 40; index++)
|
|
{
|
|
Assert.Equal((uint)RecordedOperation.Unknown, scheduler.Current.DebugOp);
|
|
Assert.Equal(0UL, scheduler.Current.DebugArg4);
|
|
scheduler.Current.SetDebugInfo((uint)RecordedOperation.DispatchDirect, index,
|
|
index, 2, 3, 0, 0xABCD);
|
|
scheduler.Submit();
|
|
scheduler.BeginCommand();
|
|
}
|
|
|
|
var history = scheduler.FormatRecentSubmissions().ToArray();
|
|
Assert.Equal(32, history.Length);
|
|
Assert.StartsWith("tick=9 op=DispatchDirect submit=9 ", history[0]);
|
|
Assert.StartsWith("tick=40 op=DispatchDirect submit=40 ", history[^1]);
|
|
Assert.EndsWith("args=40,2,3,0,0x000000000000ABCD", history[^1]);
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoSchedulersKeepIndependentTicksAndOneMayRunWithoutActivation()
|
|
{
|
|
var otherDevice = new FakeTickDevice();
|
|
using var renderer = NewActiveScheduler(_device, _rendering);
|
|
using var present = new SubmissionScheduler(otherDevice, new RecordingRenderingState());
|
|
|
|
renderer.Flush();
|
|
Assert.Equal(2UL, renderer.Submit());
|
|
present.BeginCommand();
|
|
Assert.Equal(1UL, present.Submit());
|
|
Assert.False(present.Active);
|
|
|
|
_device.Complete(2);
|
|
Assert.True(renderer.IsTickComplete(2));
|
|
Assert.False(present.IsTickComplete(1));
|
|
otherDevice.Complete(1);
|
|
present.Wait(1);
|
|
Assert.True(present.IsTickComplete(1));
|
|
Assert.NotEqual(_device.TimelineHandle, otherDevice.TimelineHandle);
|
|
}
|
|
|
|
[Fact]
|
|
public void EndClosesRenderingBeforeTheBufferEndsAndBeginRefusesAnOpenPass()
|
|
{
|
|
using var fatal = new FatalScope();
|
|
_device.CompleteOnSubmit = true;
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
_rendering.IsRendering = true;
|
|
|
|
scheduler.EndRendering();
|
|
Assert.Equal(new[] { "end_rendering" }, _rendering.Log);
|
|
|
|
_rendering.IsRendering = true;
|
|
scheduler.Submit();
|
|
Assert.Equal(new[] { "end_rendering", "end_rendering" }, _rendering.Log);
|
|
Assert.False(_rendering.IsRendering);
|
|
|
|
_rendering.IsRendering = true;
|
|
Assert.Throws<SchedulerFatalException>(() => scheduler.BeginCommand());
|
|
_rendering.IsRendering = false;
|
|
}
|
|
|
|
private sealed class OrderedStores : IGuestBufferStore, IGuestImageStore
|
|
{
|
|
public List<string> Order { get; } = new();
|
|
|
|
public bool MarkCpuWrite(ulong address, ulong size)
|
|
{
|
|
lock (Order)
|
|
{
|
|
Order.Add("stores");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool DownloadToCpu(ulong address, ulong size) => false;
|
|
|
|
public void Unregister(ulong address, ulong size)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class InlineRelay : IGpuQueueRelay
|
|
{
|
|
public bool IsGpuQueueThread => true;
|
|
|
|
public void Post(Action work) => work();
|
|
|
|
public void RunOnGpuQueue(Action work) => work();
|
|
|
|
public bool TryRunOnGpuQueue(Action work)
|
|
{
|
|
work();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void UnregisterDrainsTheRealSchedulerBeforeTouchingTheStores()
|
|
{
|
|
var previous = PageGuard.OnFatal;
|
|
var fatals = new List<string>();
|
|
PageGuard.OnFatal = fatals.Add;
|
|
try
|
|
{
|
|
_device.CompleteOnSubmit = true;
|
|
using var scheduler = NewActiveScheduler(_device, _rendering);
|
|
var stores = new OrderedStores();
|
|
using var memory = new GuestGpuMemory(new RecordingAddressSpace());
|
|
memory.AttachStores(stores, stores);
|
|
memory.Register(0x10000, 0x1000, GuestPageProtection.Read | GuestPageProtection.Write);
|
|
memory.AttachGpuQueue(new InlineRelay(), scheduler);
|
|
scheduler.QueuePriorityCompletionAction(() =>
|
|
{
|
|
lock (stores.Order)
|
|
{
|
|
stores.Order.Add("urgent");
|
|
}
|
|
});
|
|
|
|
memory.Unregister(0x10000, 0x1000);
|
|
|
|
Assert.Equal(new[] { "urgent", "stores" }, stores.Order);
|
|
Assert.Contains("submit b0 tick=1", _device.Log);
|
|
Assert.False(scheduler.Current.IsInvalid);
|
|
Assert.False(memory.Covers(0x10000, 0x1000));
|
|
|
|
memory.Register(0x20000, 0x1000, GuestPageProtection.Read | GuestPageProtection.Write);
|
|
scheduler.QueueCompletionAction(() => memory.Unregister(0x20000, 0x1000));
|
|
scheduler.Submit();
|
|
scheduler.RunCompletedOperations();
|
|
Assert.Single(fatals);
|
|
Assert.True(memory.Covers(0x20000, 0x1000));
|
|
memory.AttachGpuQueue(null, null);
|
|
memory.Unregister(0x20000, 0x1000);
|
|
}
|
|
finally
|
|
{
|
|
PageGuard.OnFatal = previous;
|
|
}
|
|
}
|
|
}
|