using System.Runtime.InteropServices;
using Nerfed.Runtime.Video;
using RefreshCS;
namespace Nerfed.Runtime.Graphics;
///
/// Manages all graphics-related concerns.
///
public class GraphicsDevice : IDisposable
{
public IntPtr Handle { get; }
public BackendFlags Backend { get; }
public bool DebugMode { get; }
// Built-in video pipeline
internal GraphicsPipeline VideoPipeline { get; }
// Built-in text shader info
public Shader TextVertexShader;
public Shader TextFragmentShader;
public VertexInputState TextVertexInputState { get; }
// Built-in samplers
public Sampler PointSampler { get; }
public Sampler LinearSampler { get; }
public bool IsDisposed { get; private set; }
internal readonly RenderPassPool RenderPassPool = new RenderPassPool();
internal readonly ComputePassPool ComputePassPool = new ComputePassPool();
internal readonly CopyPassPool CopyPassPool = new CopyPassPool();
private readonly HashSet resources = new HashSet();
private readonly CommandBufferPool commandBufferPool;
private readonly FencePool fencePool;
internal GraphicsDevice(BackendFlags preferredBackends)
{
if (preferredBackends == BackendFlags.Invalid)
{
throw new System.Exception("Could not set graphics backend!");
}
bool debugMode = false;
#if DEBUG
debugMode = true;
#endif
Handle = Refresh.Refresh_CreateDevice((Refresh.BackendFlags)preferredBackends, Conversions.BoolToInt(debugMode));
DebugMode = debugMode;
// TODO: check for CreateDevice fail
Backend = (BackendFlags)Refresh.Refresh_GetBackend(Handle);
IEmbeddedShaders embeddedShaders;
switch (Backend)
{
case BackendFlags.Vulkan:
embeddedShaders = new EmbeddedShadersSpirV();
break;
case BackendFlags.D3D11:
throw new NotImplementedException("D3D11 embedded shaders");
break;
case BackendFlags.Metal:
throw new NotImplementedException("Metal embedded shaders");
break;
default: throw new ArgumentOutOfRangeException();
}
Shader fullscreenVertShader;
Shader textVertShader;
Shader textFragShader;
Shader videoFragShader;
using (MemoryStream fullscreenVertStream = new MemoryStream(embeddedShaders.FullscreenVert))
{
fullscreenVertShader = new Shader(
this,
fullscreenVertStream,
"main",
new ShaderCreateInfo
{
ShaderStage = ShaderStage.Vertex,
ShaderFormat = embeddedShaders.ShaderFormat
}
);
}
using (MemoryStream videoYuv2RgbaFragStream = new MemoryStream(embeddedShaders.VideoYuv2RgbaFrag))
{
videoFragShader = new Shader(
this,
videoYuv2RgbaFragStream,
"main",
new ShaderCreateInfo
{
ShaderStage = ShaderStage.Fragment,
ShaderFormat = embeddedShaders.ShaderFormat,
SamplerCount = 3
}
);
}
using (MemoryStream textTransformVertStream = new MemoryStream(embeddedShaders.TextTransformVert))
{
textVertShader = new Shader(
this,
textTransformVertStream,
"main",
new ShaderCreateInfo
{
ShaderStage = ShaderStage.Vertex,
ShaderFormat = embeddedShaders.ShaderFormat,
UniformBufferCount = 1
}
);
}
using (MemoryStream textMsdfFragStream = new MemoryStream(embeddedShaders.TextMsdfFrag))
{
textFragShader = new Shader(
this,
textMsdfFragStream,
"main",
new ShaderCreateInfo
{
ShaderStage = ShaderStage.Fragment,
ShaderFormat = embeddedShaders.ShaderFormat,
SamplerCount = 1,
UniformBufferCount = 1
}
);
}
VideoPipeline = new GraphicsPipeline(
this,
new GraphicsPipelineCreateInfo
{
AttachmentInfo = new GraphicsPipelineAttachmentInfo(
new ColorAttachmentDescription(
TextureFormat.R8G8B8A8,
ColorAttachmentBlendState.None
)
),
DepthStencilState = DepthStencilState.Disable,
VertexShader = fullscreenVertShader,
FragmentShader = videoFragShader,
VertexInputState = VertexInputState.Empty,
RasterizerState = RasterizerState.CCW_CullNone,
PrimitiveType = PrimitiveType.TriangleList,
MultisampleState = MultisampleState.None
}
);
TextVertexShader = textVertShader;
TextFragmentShader = textFragShader;
TextVertexInputState = VertexInputState.CreateSingleBinding();
PointSampler = new Sampler(this, SamplerCreateInfo.PointClamp);
LinearSampler = new Sampler(this, SamplerCreateInfo.LinearClamp);
fencePool = new FencePool(this);
commandBufferPool = new CommandBufferPool(this);
}
///
/// Prepares a window so that frames can be presented to it.
///
/// The desired composition of the swapchain. Ignore this unless you are using HDR or tonemapping.
/// The desired presentation mode for the window. Roughly equivalent to V-Sync.
/// True if successfully claimed.
public bool ClaimWindow(
Window window,
SwapchainComposition swapchainComposition,
PresentMode presentMode
)
{
if (window.Claimed)
{
Log.Error("Window already claimed!");
return false;
}
bool success = Conversions.IntToBool(
Refresh.Refresh_ClaimWindow(
Handle,
window.Handle,
(Refresh.SwapchainComposition)swapchainComposition,
(Refresh.PresentMode)presentMode
)
);
if (success)
{
window.Claimed = true;
window.SwapchainComposition = swapchainComposition;
window.SwapchainFormat = GetSwapchainFormat(window);
if (window.SwapchainTexture == null)
{
window.SwapchainTexture = new Texture(this, window.SwapchainFormat);
}
}
return success;
}
///
/// Unclaims a window, making it unavailable for presenting and freeing associated resources.
///
public void UnclaimWindow(Window window)
{
if (window.Claimed)
{
Refresh.Refresh_UnclaimWindow(
Handle,
window.Handle
);
window.Claimed = false;
// The swapchain texture doesn't actually have a permanent texture reference, so we zero the handle before disposing.
window.SwapchainTexture.Handle = IntPtr.Zero;
window.SwapchainTexture.Dispose();
window.SwapchainTexture = null;
}
}
///
/// Changes the present mode of a claimed window. Does nothing if the window is not claimed.
///
public bool SetSwapchainParameters(
Window window,
SwapchainComposition swapchainComposition,
PresentMode presentMode
)
{
if (!window.Claimed)
{
Log.Error("Cannot set present mode on unclaimed window!");
return false;
}
bool success = Conversions.IntToBool(
Refresh.Refresh_SetSwapchainParameters(
Handle,
window.Handle,
(Refresh.SwapchainComposition)swapchainComposition,
(Refresh.PresentMode)presentMode
)
);
if (success)
{
window.SwapchainComposition = swapchainComposition;
window.SwapchainFormat = GetSwapchainFormat(window);
if (window.SwapchainTexture != null)
{
window.SwapchainTexture.Format = window.SwapchainFormat;
}
}
return success;
}
///
/// Acquires a command buffer.
/// This is the start of your rendering process.
///
///
public CommandBuffer AcquireCommandBuffer()
{
CommandBuffer commandBuffer = commandBufferPool.Obtain();
commandBuffer.SetHandle(Refresh.Refresh_AcquireCommandBuffer(Handle));
#if DEBUG
commandBuffer.ResetStateTracking();
#endif
return commandBuffer;
}
///
/// Submits a command buffer to the GPU for processing.
///
public void Submit(CommandBuffer commandBuffer)
{
#if DEBUG
if (commandBuffer.Submitted)
{
throw new System.InvalidOperationException("Command buffer already submitted!");
}
#endif
Refresh.Refresh_Submit(
commandBuffer.Handle
);
commandBufferPool.Return(commandBuffer);
#if DEBUG
commandBuffer.Submitted = true;
#endif
}
///
/// Submits a command buffer to the GPU for processing and acquires a fence associated with the submission.
///
///
public Fence SubmitAndAcquireFence(CommandBuffer commandBuffer)
{
IntPtr fenceHandle = Refresh.Refresh_SubmitAndAcquireFence(
commandBuffer.Handle
);
Fence fence = fencePool.Obtain();
fence.SetHandle(fenceHandle);
return fence;
}
///
/// Wait for the graphics device to become idle.
///
public void Wait()
{
Refresh.Refresh_Wait(Handle);
}
///
/// Waits for the given fence to become signaled.
///
public unsafe void WaitForFence(Fence fence)
{
IntPtr fenceHandle = fence.Handle;
Refresh.Refresh_WaitForFences(
Handle,
1,
&fenceHandle,
1
);
}
///
/// Wait for one or more fences to become signaled.
///
/// If true, will wait for all given fences to be signaled.
public unsafe void WaitForFences(Span fences, bool waitAll)
{
IntPtr* handlePtr = stackalloc nint[fences.Length];
for (int i = 0; i < fences.Length; i += 1)
{
handlePtr[i] = fences[i].Handle;
}
Refresh.Refresh_WaitForFences(
Handle,
Conversions.BoolToInt(waitAll),
handlePtr,
(uint)fences.Length
);
}
///
/// Returns true if the fence is signaled, indicating that the associated command buffer has finished processing.
///
/// Throws if the fence query indicates that the graphics device has been lost.
public bool QueryFence(Fence fence)
{
int result = Refresh.Refresh_QueryFence(Handle, fence.Handle);
if (result < 0)
{
throw new InvalidOperationException("The graphics device has been lost.");
}
return result != 0;
}
///
/// Release reference to an acquired fence, enabling it to be reused.
///
public void ReleaseFence(Fence fence)
{
Refresh.Refresh_ReleaseFence(Handle, fence.Handle);
fence.Handle = IntPtr.Zero;
fencePool.Return(fence);
}
private TextureFormat GetSwapchainFormat(Window window)
{
if (!window.Claimed)
{
throw new System.ArgumentException("Cannot get swapchain format of unclaimed window!");
}
return (TextureFormat)Refresh.Refresh_GetSwapchainTextureFormat(Handle, window.Handle);
}
internal void AddResourceReference(GCHandle resourceReference)
{
lock (resources)
{
resources.Add(resourceReference);
}
}
internal void RemoveResourceReference(GCHandle resourceReference)
{
lock (resources)
{
resources.Remove(resourceReference);
}
}
private void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
lock (resources)
{
// Dispose video players first to avoid race condition on threaded decoding
foreach (GCHandle resource in resources)
{
if (resource.Target is VideoPlayer player)
{
player.Dispose();
}
}
// Dispose everything else
foreach (GCHandle resource in resources)
{
if (resource.Target is IDisposable disposable)
{
disposable.Dispose();
}
}
resources.Clear();
}
}
Refresh.Refresh_DestroyDevice(Handle);
IsDisposed = true;
}
}
~GraphicsDevice()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: false);
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}