164 lines
6.7 KiB
C#
164 lines
6.7 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Collections;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using SharpEmu.Libs.Gpu.Buffers;
|
|
using SharpEmu.Libs.Gpu.GpuCommands;
|
|
using SharpEmu.Libs.Gpu.Images;
|
|
using SharpEmu.Libs.Gpu.Pipelines;
|
|
using SharpEmu.Libs.Gpu.Rendering;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using SharpEmu.Libs.Tests.Gpu.Buffers;
|
|
using SharpEmu.Libs.Tests.Gpu.Vulkan;
|
|
using SharpEmu.Libs.VideoOut;
|
|
using Silk.NET.Vulkan;
|
|
|
|
namespace SharpEmu.Libs.Tests.VideoOut;
|
|
|
|
// The scheduler drives the presenter's submission bookkeeping and rendering state, as in production.
|
|
internal sealed class SchedulerForwarder : IRenderingState
|
|
{
|
|
public PresenterUnderTest? Target { get; set; }
|
|
|
|
public bool IsRendering => Target is { } target && ((IRenderingState)target.Instance).IsRendering;
|
|
|
|
public void EndRendering()
|
|
{
|
|
if (Target is { } target)
|
|
{
|
|
((IRenderingState)target.Instance).EndRendering();
|
|
}
|
|
}
|
|
|
|
public void Prepare(SubmitBundle bundle) => Target?.InvokeMethod("PrepareGuestSubmission", bundle);
|
|
|
|
public void Complete(ulong tick) => Target?.InvokeMethod("CompleteGuestSubmission", tick);
|
|
}
|
|
|
|
// A presenter over the harness caches without a window, a swapchain or a render thread.
|
|
internal sealed class PresenterUnderTest : IDisposable
|
|
{
|
|
public const BindingFlags InstanceMembers = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
|
public static readonly Type PresenterType = typeof(VulkanVideoPresenter).GetNestedType("Presenter", BindingFlags.NonPublic)!;
|
|
|
|
public PresenterUnderTest(HeadlessVulkan vulkan, bool startScheduler = true)
|
|
{
|
|
var forwarder = new SchedulerForwarder();
|
|
Harness = new CacheHarness(vulkan, hooks: new SchedulerHooks(forwarder, forwarder.Prepare, forwarder.Complete), startScheduler: startScheduler);
|
|
Samplers = new SamplerStore(vulkan.DeviceInfo);
|
|
Instance = RuntimeHelpers.GetUninitializedObject(PresenterType);
|
|
SetField("_vk", vulkan.Vk);
|
|
SetField("_instance", vulkan.Instance);
|
|
SetField("_physicalDevice", vulkan.Physical);
|
|
SetField("_device", vulkan.Device);
|
|
SetField("_supportsFragmentShaderBarycentric", vulkan.SupportsFragmentShaderBarycentric);
|
|
SetField("_supportsFillRectangle", vulkan.SupportsFillRectangle);
|
|
SetField("_deviceInfo", vulkan.DeviceInfo);
|
|
SetField("_scheduler", Harness.Scheduler);
|
|
SetField("_relay", Harness.Worker.Relay);
|
|
SetField("_bufferCache", Harness.Cache);
|
|
SetField("_imageCache", Harness.Images);
|
|
SetField("_guestBacking", Harness.Memory);
|
|
SetField("_guestMemory", Harness.Memory);
|
|
SetField("_samplerStore", Samplers);
|
|
SetField("_trackedImageBindings", new List<ResourceSlotIdentifier>());
|
|
SetField("_submissionContext", new SubmissionContext { QueueName = "presenter.test", SubmissionId = 1 });
|
|
SetField("_commandStream", new CommandStreamQueue((ICommandStreamHost)Instance));
|
|
SetField("_descriptorHeap", new DescriptorHeap(vulkan.DeviceInfo, Harness.Scheduler));
|
|
SetField("_maxPushDescriptors", vulkan.MaxPushDescriptors);
|
|
SetField("_noAttachmentSampleCounts", SampleCountFlags.Count1Bit);
|
|
HostBuffers = new VulkanHostBufferPool(128UL * 1024 * 1024, allocation => InvokeMethod("DestroyHostBufferAllocation", allocation));
|
|
SetField("_hostBufferPool", HostBuffers);
|
|
SetField("_maxColorAttachments", 8u);
|
|
SetField("_renderHostLimits", new RenderHostLimits(16384, 16384, 16384, 16384));
|
|
foreach (var name in new[]
|
|
{
|
|
"_batchResources", "_batchRetireBuffers", "_pendingGuestSubmissions",
|
|
"_deferredGuestImageVersionDestroys",
|
|
"_pipelineEntries", "_shaderModules", "_shaderModuleSpirvBytes",
|
|
"_preparedTextures", "_barriersAfterRendering", "_feedbackSnapshotPool",
|
|
"_occlusionWrites", "_pendingOcclusionResults", "_freeOcclusionQueries",
|
|
})
|
|
{
|
|
var field = PresenterType.GetField(name, InstanceMembers)!;
|
|
field.SetValue(Instance, Activator.CreateInstance(field.FieldType, nonPublic: true));
|
|
}
|
|
|
|
SetField("_shaderModuleCacheIdentities", new Dictionary<ulong, string>());
|
|
var cacheShards = PresenterType.GetField("_pipelineCacheShards", InstanceMembers)!;
|
|
cacheShards.SetValue(Instance, Activator.CreateInstance(cacheShards.FieldType, nonPublic: true));
|
|
forwarder.Target = this;
|
|
}
|
|
|
|
public CacheHarness Harness { get; }
|
|
|
|
public SamplerStore Samplers { get; }
|
|
|
|
public VulkanHostBufferPool HostBuffers { get; }
|
|
|
|
public object Instance { get; }
|
|
|
|
public IRenderHost RenderHost => (IRenderHost)Instance;
|
|
|
|
public CommandBuffer Command => new(Harness.Scheduler.Current.Handle);
|
|
|
|
// Loads the dynamic rendering commands the render host records with; the device must carry the extensions.
|
|
public void LoadRenderingCommands() => InvokeMethod("LoadRenderingCommands", false, Harness.Vulkan.DeviceName);
|
|
|
|
public void SetField(string name, object value) => PresenterType.GetField(name, InstanceMembers)!.SetValue(Instance, value);
|
|
|
|
public T GetField<T>(string name) => (T)PresenterType.GetField(name, InstanceMembers)!.GetValue(Instance)!;
|
|
|
|
public object? InvokeMethod(string name, params object?[] arguments)
|
|
{
|
|
try
|
|
{
|
|
return PresenterType.GetMethod(name, InstanceMembers | BindingFlags.Static)!.Invoke(Instance, arguments);
|
|
}
|
|
catch (TargetInvocationException exception) when (exception.InnerException is not null)
|
|
{
|
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public T Run<T>(Func<T> work) => Harness.Worker.Run(work);
|
|
|
|
public void Run(Action work) => Harness.Worker.Run(work);
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
Run(() =>
|
|
{
|
|
// Complete recorded work before destroying resources that its commands use.
|
|
if (Harness.Scheduler.Active)
|
|
{
|
|
Harness.Scheduler.Finish();
|
|
Harness.Scheduler.WaitForAllPriorityOperations();
|
|
}
|
|
|
|
InvokeMethod("WaitForAllGuestSubmissions");
|
|
DestroyPipelineResources();
|
|
Samplers.Dispose();
|
|
HostBuffers.Dispose();
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
Harness.Dispose();
|
|
}
|
|
|
|
Harness.Vulkan.AssertNoValidationMessages();
|
|
}
|
|
|
|
private void DestroyPipelineResources()
|
|
{
|
|
InvokeMethod("DestroyRenderPipelines");
|
|
GetField<DescriptorHeap>("_descriptorHeap").Dispose();
|
|
}
|
|
}
|