using Nerfed.Runtime.Video; using RefreshCS; using System.Runtime.InteropServices; 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 shaders public Shader FullscreenVertexShader { get; private set; } public Shader VideoFragmentShader { get; private set; } public Shader TextVertexShader { get; private set; } public Shader TextFragmentShader { get; private set; } // Built-in video pipeline internal GraphicsPipeline VideoPipeline { get; private set; } // Built-in text shader info 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); TextVertexInputState = VertexInputState.CreateSingleBinding(); PointSampler = new Sampler(this, SamplerCreateInfo.PointClamp); LinearSampler = new Sampler(this, SamplerCreateInfo.LinearClamp); fencePool = new FencePool(this); commandBufferPool = new CommandBufferPool(this); } internal void LoadDefaultPipelines() { FullscreenVertexShader = ResourceManager.Load("Shaders/Fullscreen.vert"); VideoFragmentShader = ResourceManager.Load("Shaders/Video.frag"); TextVertexShader = ResourceManager.Load("Shaders/Text.vert"); TextFragmentShader = ResourceManager.Load("Shaders/Text.frag"); VideoPipeline = new GraphicsPipeline( this, new GraphicsPipelineCreateInfo { AttachmentInfo = new GraphicsPipelineAttachmentInfo( new ColorAttachmentDescription( TextureFormat.R8G8B8A8, ColorAttachmentBlendState.None ) ), DepthStencilState = DepthStencilState.Disable, VertexShader = FullscreenVertexShader, FragmentShader = VideoFragmentShader, VertexInputState = VertexInputState.Empty, RasterizerState = RasterizerState.CCW_CullNone, PrimitiveType = PrimitiveType.TriangleList, MultisampleState = MultisampleState.None } ); } /// /// 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(); } ResourceManager.Unload(FullscreenVertexShader); ResourceManager.Unload(TextFragmentShader); ResourceManager.Unload(TextVertexShader); ResourceManager.Unload(VideoFragmentShader); } 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); } }