using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using MoonWorks.Video; using RefreshCS; namespace MoonWorks.Graphics { /// /// GraphicsDevice 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 GraphicsPipelineResourceInfo TextVertexShaderInfo { get; } public GraphicsPipelineResourceInfo TextFragmentShaderInfo { get; } public VertexInputState TextVertexInputState { get; } // Built-in samplers public Sampler PointSampler { get; } public Sampler LinearSampler { get; } public bool IsDisposed { get; private set; } private readonly HashSet resources = new HashSet(); private CommandBufferPool CommandBufferPool; private FencePool FencePool; internal RenderPassPool RenderPassPool = new RenderPassPool(); internal ComputePassPool ComputePassPool = new ComputePassPool(); internal CopyPassPool CopyPassPool = new CopyPassPool(); internal unsafe GraphicsDevice( BackendFlags preferredBackends, bool debugMode ) { if (preferredBackends == BackendFlags.Invalid) { throw new System.Exception("Could not set graphics backend!"); } Handle = Refresh.Refresh_CreateDevice( (Refresh.BackendFlags) preferredBackends, Conversions.BoolToByte(debugMode) ); DebugMode = debugMode; // TODO: check for CreateDevice fail Backend = (BackendFlags) Refresh.Refresh_GetBackend(Handle); // Check for replacement stock shaders string basePath = System.AppContext.BaseDirectory; string fullscreenVertPath = Path.Combine(basePath, "fullscreen.vert.refresh"); string textVertPath = Path.Combine(basePath, "text_transform.vert.refresh"); string textFragPath = Path.Combine(basePath, "text_msdf.frag.refresh"); string videoFragPath = Path.Combine(basePath, "video_yuv2rgba.frag.refresh"); Shader fullscreenVertShader; Shader textVertShader; Shader textFragShader; Shader videoFragShader; if (File.Exists(fullscreenVertPath)) { fullscreenVertShader = new Shader( this, fullscreenVertPath, "main", ShaderStage.Vertex, ShaderFormat.SECRET ); } else { // use defaults var assembly = typeof(GraphicsDevice).Assembly; using var vertStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.Fullscreen.vert.spv"); fullscreenVertShader = new Shader( this, vertStream, "main", ShaderStage.Vertex, ShaderFormat.SPIRV ); } if (File.Exists(videoFragPath)) { videoFragShader = new Shader( this, videoFragPath, "main", ShaderStage.Fragment, ShaderFormat.SECRET ); } else { // use defaults var assembly = typeof(GraphicsDevice).Assembly; using var fragStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.VideoYUV2RGBA.frag.spv"); videoFragShader = new Shader( this, fragStream, "main", ShaderStage.Fragment, ShaderFormat.SPIRV ); } if (File.Exists(textVertPath) && File.Exists(textFragPath)) { textVertShader = new Shader( this, textVertPath, "main", ShaderStage.Vertex, ShaderFormat.SECRET ); textFragShader = new Shader( this, textFragPath, "main", ShaderStage.Fragment, ShaderFormat.SECRET ); } else { // use defaults var assembly = typeof(GraphicsDevice).Assembly; using var vertStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.TextTransform.vert.spv"); using var fragStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.TextMSDF.frag.spv"); textVertShader = new Shader( this, vertStream, "main", ShaderStage.Fragment, ShaderFormat.SPIRV ); textFragShader = new Shader( this, fragStream, "main", ShaderStage.Fragment, ShaderFormat.SPIRV ); } VideoPipeline = new GraphicsPipeline( this, new GraphicsPipelineCreateInfo { AttachmentInfo = new GraphicsPipelineAttachmentInfo( new ColorAttachmentDescription( TextureFormat.R8G8B8A8, ColorAttachmentBlendState.None ) ), DepthStencilState = DepthStencilState.Disable, VertexShader = fullscreenVertShader, FragmentShader = videoFragShader, FragmentShaderResourceInfo = new GraphicsPipelineResourceInfo { SamplerCount = 3 }, VertexInputState = VertexInputState.Empty, RasterizerState = RasterizerState.CCW_CullNone, PrimitiveType = PrimitiveType.TriangleList, MultisampleState = MultisampleState.None } ); TextVertexShader = textVertShader; TextVertexShaderInfo = new GraphicsPipelineResourceInfo(); TextFragmentShader = textFragShader; TextFragmentShaderInfo = new GraphicsPipelineResourceInfo { SamplerCount = 1 }; 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) { Logger.LogError("Window already claimed!"); return false; } var 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 void SetSwapchainParameters( Window window, SwapchainComposition swapchainComposition, PresentMode presentMode ) { if (!window.Claimed) { Logger.LogError("Cannot set present mode on unclaimed window!"); return; } Refresh.Refresh_SetSwapchainParameters( Handle, window.Handle, (Refresh.SwapchainComposition) swapchainComposition, (Refresh.PresentMode) presentMode ); } /// /// Acquires a command buffer. /// This is the start of your rendering process. /// /// public CommandBuffer AcquireCommandBuffer() { var 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) { var fenceHandle = Refresh.Refresh_SubmitAndAcquireFence( commandBuffer.Handle ); var 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) { var 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) { var handlePtr = stackalloc nint[fences.Length]; for (var 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) { var 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); } } protected virtual void Dispose(bool disposing) { if (!IsDisposed) { if (disposing) { lock (resources) { // Dispose video players first to avoid race condition on threaded decoding foreach (var resource in resources) { if (resource.Target is VideoPlayer player) { player.Dispose(); } } // Dispose everything else foreach (var 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); } } }