diff --git a/MoonWorks.csproj b/MoonWorks.csproj index e09a9010..2e0eb181 100644 --- a/MoonWorks.csproj +++ b/MoonWorks.csproj @@ -24,4 +24,19 @@ Never + + + + MoonWorks.Graphics.StockShaders.VideoFullscreen.vert.refresh + + + MoonWorks.Graphics.StockShaders.VideoYUV2RGBA.frag.refresh + + + MoonWorks.Graphics.StockShaders.TextTransform.vert.refresh + + + MoonWorks.Graphics.StockShaders.TextMSDF.frag.refresh + + diff --git a/MoonWorks.dll.config b/MoonWorks.dll.config index 67f28dcf..167fa25b 100644 --- a/MoonWorks.dll.config +++ b/MoonWorks.dll.config @@ -13,8 +13,8 @@ - - + + diff --git a/lib/WellspringCS b/lib/WellspringCS index f8872bae..074f2afc 160000 --- a/lib/WellspringCS +++ b/lib/WellspringCS @@ -1 +1 @@ -Subproject commit f8872bae59e394b0f8a35224bb39ab8fd041af97 +Subproject commit 074f2afc833b221906bb2468735041ce78f2cb89 diff --git a/lib/dav1dfile b/lib/dav1dfile index 3dcd69ff..5065e2cd 160000 --- a/lib/dav1dfile +++ b/lib/dav1dfile @@ -1 +1 @@ -Subproject commit 3dcd69ff85db80eea51481edd323b42c05993e1a +Subproject commit 5065e2cd4662dbe023b77a45ef967f975170dfff diff --git a/src/Audio/AudioDevice.cs b/src/Audio/AudioDevice.cs index 17f617e8..48ef387c 100644 --- a/src/Audio/AudioDevice.cs +++ b/src/Audio/AudioDevice.cs @@ -25,7 +25,7 @@ namespace MoonWorks.Audio public float DopplerScale = 1f; public float SpeedOfSound = 343.5f; - private readonly HashSet resources = new HashSet(); + private readonly HashSet resourceHandles = new HashSet(); private readonly HashSet updatingSourceVoices = new HashSet(); private AudioTweenManager AudioTweenManager; @@ -123,7 +123,6 @@ namespace MoonWorks.Audio AudioTweenManager = new AudioTweenManager(); VoicePool = new SourceVoicePool(this); - Logger.LogInfo("Setting up audio thread..."); WakeSignal = new AutoResetEvent(true); Thread = new Thread(ThreadMain); @@ -265,7 +264,7 @@ namespace MoonWorks.Audio { lock (StateLock) { - resources.Add(resourceReference); + resourceHandles.Add(resourceReference); if (resourceReference.Target is UpdatingSourceVoice updatableVoice) { @@ -278,7 +277,12 @@ namespace MoonWorks.Audio { lock (StateLock) { - resources.Remove(resourceReference); + resourceHandles.Remove(resourceReference); + + if (resourceReference.Target is UpdatingSourceVoice updatableVoice) + { + updatingSourceVoices.Remove(updatableVoice); + } } } @@ -292,28 +296,42 @@ namespace MoonWorks.Audio { Thread.Join(); - // dispose all voices first - foreach (var resource in resources) + // dispose all source voices first + foreach (var handle in resourceHandles) { - if (resource.Target is Voice voice) + if (handle.Target is SourceVoice voice) { voice.Dispose(); } } - // destroy all other audio resources - foreach (var resource in resources) + // dispose all submix voices except the faux mastering voice + foreach (var handle in resourceHandles) { - if (resource.Target is IDisposable disposable) + if (handle.Target is SubmixVoice voice && voice != fauxMasteringVoice) { - disposable.Dispose(); + voice.Dispose(); } } - resources.Clear(); + // dispose the faux mastering voice + fauxMasteringVoice.Dispose(); + + // dispose the true mastering voice + FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice); + + // destroy all other audio resources + foreach (var handle in resourceHandles) + { + if (handle.Target is AudioResource resource) + { + resource.Dispose(); + } + } + + resourceHandles.Clear(); } - FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice); FAudio.FAudio_Release(Handle); IsDisposed = true; diff --git a/src/Audio/StreamingVoice.cs b/src/Audio/StreamingVoice.cs index 528e7eca..22c886cb 100644 --- a/src/Audio/StreamingVoice.cs +++ b/src/Audio/StreamingVoice.cs @@ -150,13 +150,16 @@ namespace MoonWorks.Audio { if (!IsDisposed) { - Stop(); - - for (int i = 0; i < BUFFER_COUNT; i += 1) + lock (StateLock) { - if (buffers[i] != IntPtr.Zero) + Stop(); + + for (int i = 0; i < BUFFER_COUNT; i += 1) { - NativeMemory.Free((void*) buffers[i]); + if (buffers[i] != IntPtr.Zero) + { + NativeMemory.Free((void*) buffers[i]); + } } } } diff --git a/src/Game.cs b/src/Game.cs index 1b7286ff..096afc76 100644 --- a/src/Game.cs +++ b/src/Game.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using SDL2; +using SDL2; using MoonWorks.Audio; using MoonWorks.Graphics; using MoonWorks.Input; @@ -58,6 +57,7 @@ namespace MoonWorks bool debugMode = false ) { + Logger.LogInfo("Initializing frame limiter..."); Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep); gameTimer = Stopwatch.StartNew(); @@ -68,6 +68,7 @@ namespace MoonWorks previousSleepTimes[i] = TimeSpan.FromMilliseconds(1); } + Logger.LogInfo("Initializing SDL..."); if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0) { Logger.LogError("Failed to initialize SDL!"); @@ -76,13 +77,16 @@ namespace MoonWorks Logger.Initialize(); + Logger.LogInfo("Initializing input..."); Inputs = new Inputs(); + Logger.LogInfo("Initializing graphics device..."); GraphicsDevice = new GraphicsDevice( Backend.Vulkan, debugMode ); + Logger.LogInfo("Initializing main window..."); MainWindow = new Window(windowCreateInfo, GraphicsDevice.WindowFlags | SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN); if (!GraphicsDevice.ClaimWindow(MainWindow, windowCreateInfo.PresentMode)) @@ -90,6 +94,7 @@ namespace MoonWorks throw new System.SystemException("Could not claim window!"); } + Logger.LogInfo("Initializing audio thread..."); AudioDevice = new AudioDevice(); } @@ -110,9 +115,6 @@ namespace MoonWorks Logger.LogInfo("Cleaning up game..."); Destroy(); - Logger.LogInfo("Closing audio thread..."); - AudioDevice.Dispose(); - Logger.LogInfo("Unclaiming window..."); GraphicsDevice.UnclaimWindow(MainWindow); @@ -122,6 +124,9 @@ namespace MoonWorks Logger.LogInfo("Disposing graphics device..."); GraphicsDevice.Dispose(); + Logger.LogInfo("Closing audio thread..."); + AudioDevice.Dispose(); + SDL.SDL_Quit(); } diff --git a/src/Graphics/Font/Font.cs b/src/Graphics/Font/Font.cs index cf4982e1..0dd6d1cd 100644 --- a/src/Graphics/Font/Font.cs +++ b/src/Graphics/Font/Font.cs @@ -5,45 +5,117 @@ using WellspringCS; namespace MoonWorks.Graphics.Font { - public class Font : IDisposable - { - public IntPtr Handle { get; } + public unsafe class Font : GraphicsResource + { + public Texture Texture { get; } + public float PixelsPerEm { get; } + public float DistanceRange { get; } - private bool IsDisposed; + internal IntPtr Handle { get; } - public unsafe Font(string path) - { - var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); - var fileByteBuffer = NativeMemory.Alloc((nuint) fileStream.Length); - var fileByteSpan = new Span(fileByteBuffer, (int) fileStream.Length); - fileStream.ReadExactly(fileByteSpan); - fileStream.Close(); + private byte* StringBytes; + private int StringBytesLength; - Handle = Wellspring.Wellspring_CreateFont((IntPtr) fileByteBuffer, (uint) fileByteSpan.Length); + /// + /// Loads a TTF or OTF font from a path for use in MSDF rendering. + /// Note that there must be an msdf-atlas-gen JSON and image file alongside. + /// + /// + public unsafe static Font Load( + GraphicsDevice graphicsDevice, + CommandBuffer commandBuffer, + string fontPath + ) { + var fontFileStream = new FileStream(fontPath, FileMode.Open, FileAccess.Read); + var fontFileByteBuffer = NativeMemory.Alloc((nuint) fontFileStream.Length); + var fontFileByteSpan = new Span(fontFileByteBuffer, (int) fontFileStream.Length); + fontFileStream.ReadExactly(fontFileByteSpan); + fontFileStream.Close(); - NativeMemory.Free(fileByteBuffer); - } + var atlasFileStream = new FileStream(Path.ChangeExtension(fontPath, ".json"), FileMode.Open, FileAccess.Read); + var atlasFileByteBuffer = NativeMemory.Alloc((nuint) atlasFileStream.Length); + var atlasFileByteSpan = new Span(atlasFileByteBuffer, (int) atlasFileStream.Length); + atlasFileStream.ReadExactly(atlasFileByteSpan); + atlasFileStream.Close(); - protected virtual void Dispose(bool disposing) + var handle = Wellspring.Wellspring_CreateFont( + (IntPtr) fontFileByteBuffer, + (uint) fontFileByteSpan.Length, + (IntPtr) atlasFileByteBuffer, + (uint) atlasFileByteSpan.Length, + out float pixelsPerEm, + out float distanceRange + ); + + var texture = Texture.FromImageFile(graphicsDevice, commandBuffer, Path.ChangeExtension(fontPath, ".png")); + + NativeMemory.Free(fontFileByteBuffer); + NativeMemory.Free(atlasFileByteBuffer); + + return new Font(graphicsDevice, handle, texture, pixelsPerEm, distanceRange); + } + + private Font(GraphicsDevice device, IntPtr handle, Texture texture, float pixelsPerEm, float distanceRange) : base(device) + { + Handle = handle; + Texture = texture; + PixelsPerEm = pixelsPerEm; + DistanceRange = distanceRange; + + StringBytesLength = 32; + StringBytes = (byte*) NativeMemory.Alloc((nuint) StringBytesLength); + } + + public unsafe bool TextBounds( + string text, + int pixelSize, + HorizontalAlignment horizontalAlignment, + VerticalAlignment verticalAlignment, + out Wellspring.Rectangle rectangle + ) { + var byteCount = System.Text.Encoding.UTF8.GetByteCount(text); + + if (StringBytesLength < byteCount) + { + StringBytes = (byte*) NativeMemory.Realloc(StringBytes, (nuint) byteCount); + } + + fixed (char* chars = text) + { + System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount); + + var result = Wellspring.Wellspring_TextBounds( + Handle, + pixelSize, + (Wellspring.HorizontalAlignment) horizontalAlignment, + (Wellspring.VerticalAlignment) verticalAlignment, + (IntPtr) StringBytes, + (uint) byteCount, + out rectangle + ); + + if (result == 0) + { + Logger.LogWarn("Could not decode string: " + text); + return false; + } + } + + return true; + } + + protected override void Dispose(bool disposing) { if (!IsDisposed) { + if (disposing) + { + Texture.Dispose(); + } + Wellspring.Wellspring_DestroyFont(Handle); - IsDisposed = true; } - } - - ~Font() - { - // 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); + base.Dispose(disposing); } } } diff --git a/src/Graphics/Font/Packer.cs b/src/Graphics/Font/Packer.cs deleted file mode 100644 index 9fa053ca..00000000 --- a/src/Graphics/Font/Packer.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using WellspringCS; - -namespace MoonWorks.Graphics.Font -{ - public class Packer : IDisposable - { - public IntPtr Handle { get; } - public Texture Texture { get; } - - public Font Font { get; } - - private byte[] StringBytes; - - private bool IsDisposed; - - public unsafe Packer(GraphicsDevice graphicsDevice, Font font, float fontSize, uint textureWidth, uint textureHeight, uint padding = 1) - { - Font = font; - Handle = Wellspring.Wellspring_CreatePacker(Font.Handle, fontSize, textureWidth, textureHeight, 0, padding); - Texture = Texture.CreateTexture2D(graphicsDevice, textureWidth, textureHeight, TextureFormat.R8, TextureUsageFlags.Sampler); - StringBytes = new byte[128]; - } - - public unsafe bool PackFontRanges(params FontRange[] fontRanges) - { - fixed (FontRange *pFontRanges = &fontRanges[0]) - { - var nativeSize = fontRanges.Length * Marshal.SizeOf(); - var result = Wellspring.Wellspring_PackFontRanges(Handle, (IntPtr) pFontRanges, (uint) fontRanges.Length); - return result > 0; - } - } - - public unsafe void SetTextureData(CommandBuffer commandBuffer) - { - var pixelDataPointer = Wellspring.Wellspring_GetPixelDataPointer(Handle); - commandBuffer.SetTextureData(Texture, pixelDataPointer, Texture.Width * Texture.Height); - } - - public unsafe void TextBounds( - string text, - float x, - float y, - HorizontalAlignment horizontalAlignment, - VerticalAlignment verticalAlignment, - out Wellspring.Rectangle rectangle - ) { - var byteCount = System.Text.Encoding.UTF8.GetByteCount(text); - - if (StringBytes.Length < byteCount) - { - System.Array.Resize(ref StringBytes, byteCount); - } - - fixed (char* chars = text) - fixed (byte* bytes = StringBytes) - { - System.Text.Encoding.UTF8.GetBytes(chars, text.Length, bytes, byteCount); - Wellspring.Wellspring_TextBounds( - Handle, - x, - y, - (Wellspring.HorizontalAlignment) horizontalAlignment, - (Wellspring.VerticalAlignment) verticalAlignment, - (IntPtr) bytes, - (uint) byteCount, - out rectangle - ); - } - } - - protected virtual void Dispose(bool disposing) - { - if (!IsDisposed) - { - if (disposing) - { - Texture.Dispose(); - } - - Wellspring.Wellspring_DestroyPacker(Handle); - - IsDisposed = true; - } - } - - ~Packer() - { - // 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); - } - } -} diff --git a/src/Graphics/Font/Structs.cs b/src/Graphics/Font/Structs.cs index 82310390..f5f0eec3 100644 --- a/src/Graphics/Font/Structs.cs +++ b/src/Graphics/Font/Structs.cs @@ -4,19 +4,17 @@ using MoonWorks.Math.Float; namespace MoonWorks.Graphics.Font { [StructLayout(LayoutKind.Sequential)] - public struct FontRange - { - public uint FirstCodepoint; - public uint NumChars; - public byte OversampleH; - public byte OversampleV; - } - - [StructLayout(LayoutKind.Sequential)] - public struct Vertex + public struct Vertex : IVertexType { public Vector3 Position; public Vector2 TexCoord; public Color Color; + + public static VertexElementFormat[] Formats { get; } = new VertexElementFormat[] + { + VertexElementFormat.Vector3, + VertexElementFormat.Vector2, + VertexElementFormat.Color + }; } } diff --git a/src/Graphics/Font/TextBatch.cs b/src/Graphics/Font/TextBatch.cs index 119fbde8..7aef845b 100644 --- a/src/Graphics/Font/TextBatch.cs +++ b/src/Graphics/Font/TextBatch.cs @@ -1,75 +1,87 @@ using System; +using System.Runtime.InteropServices; using WellspringCS; namespace MoonWorks.Graphics.Font { - public class TextBatch + public unsafe class TextBatch : GraphicsResource { + public const int INITIAL_CHAR_COUNT = 64; + public const int INITIAL_VERTEX_COUNT = INITIAL_CHAR_COUNT * 4; + public const int INITIAL_INDEX_COUNT = INITIAL_CHAR_COUNT * 6; + private GraphicsDevice GraphicsDevice { get; } public IntPtr Handle { get; } public Buffer VertexBuffer { get; protected set; } = null; public Buffer IndexBuffer { get; protected set; } = null; - public Texture Texture { get; protected set; } public uint PrimitiveCount { get; protected set; } - private byte[] StringBytes; + public Font CurrentFont { get; private set; } - public TextBatch(GraphicsDevice graphicsDevice) + private byte* StringBytes; + private int StringBytesLength; + + public TextBatch(GraphicsDevice device) : base(device) { - GraphicsDevice = graphicsDevice; + GraphicsDevice = device; Handle = Wellspring.Wellspring_CreateTextBatch(); - StringBytes = new byte[128]; + + StringBytesLength = 128; + StringBytes = (byte*) NativeMemory.Alloc((nuint) StringBytesLength); + + VertexBuffer = Buffer.Create(GraphicsDevice, BufferUsageFlags.Vertex, INITIAL_VERTEX_COUNT); + IndexBuffer = Buffer.Create(GraphicsDevice, BufferUsageFlags.Index, INITIAL_INDEX_COUNT); } - public void Start(Packer packer) + // Call this to initialize or reset the batch. + public void Start(Font font) { - Wellspring.Wellspring_StartTextBatch(Handle, packer.Handle); - Texture = packer.Texture; + Wellspring.Wellspring_StartTextBatch(Handle, font.Handle); + CurrentFont = font; PrimitiveCount = 0; } - public unsafe void Draw( + // Add text with size and color to the batch + public unsafe bool Add( string text, - float x, - float y, - float depth, + int pixelSize, Color color, HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left, VerticalAlignment verticalAlignment = VerticalAlignment.Baseline ) { var byteCount = System.Text.Encoding.UTF8.GetByteCount(text); - if (StringBytes.Length < byteCount) + if (StringBytesLength < byteCount) { - System.Array.Resize(ref StringBytes, byteCount); + StringBytes = (byte*) NativeMemory.Realloc(StringBytes, (nuint) byteCount); } fixed (char* chars = text) - fixed (byte* bytes = StringBytes) { - System.Text.Encoding.UTF8.GetBytes(chars, text.Length, bytes, byteCount); + System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount); - var result = Wellspring.Wellspring_Draw( + var result = Wellspring.Wellspring_AddToTextBatch( Handle, - x, - y, - depth, + pixelSize, new Wellspring.Color { R = color.R, G = color.G, B = color.B, A = color.A }, (Wellspring.HorizontalAlignment) horizontalAlignment, (Wellspring.VerticalAlignment) verticalAlignment, - (IntPtr) bytes, + (IntPtr) StringBytes, (uint) byteCount ); if (result == 0) { - throw new System.ArgumentException("Could not decode string!"); + Logger.LogWarn("Could not decode string: " + text); + return false; } } + + return true; } - // Call this after you have made all the Draw calls you want. + // Call this after you have made all the Add calls you want, but before beginning a render pass. public unsafe void UploadBufferData(CommandBuffer commandBuffer) { Wellspring.Wellspring_GetBufferData( @@ -81,24 +93,16 @@ namespace MoonWorks.Graphics.Font out uint indexDataLengthInBytes ); - if (VertexBuffer == null) - { - VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes); - } - else if (VertexBuffer.Size < vertexDataLengthInBytes) + if (VertexBuffer.Size < vertexDataLengthInBytes) { VertexBuffer.Dispose(); VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes); } - if (IndexBuffer == null) - { - IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes); - } - else if (IndexBuffer.Size < indexDataLengthInBytes) + if (IndexBuffer.Size < indexDataLengthInBytes) { IndexBuffer.Dispose(); - IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes); + IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes); } if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0) @@ -107,7 +111,41 @@ namespace MoonWorks.Graphics.Font commandBuffer.SetBufferData(IndexBuffer, indexDataPointer, 0, indexDataLengthInBytes); } - PrimitiveCount = vertexCount / 2; // FIXME: is this jank? + PrimitiveCount = vertexCount / 2; + } + + // Call this AFTER binding your text pipeline! + public void Render(CommandBuffer commandBuffer, Math.Float.Matrix4x4 transformMatrix) + { + commandBuffer.BindFragmentSamplers(new TextureSamplerBinding( + CurrentFont.Texture, + GraphicsDevice.LinearSampler + )); + commandBuffer.BindVertexBuffers(VertexBuffer); + commandBuffer.BindIndexBuffer(IndexBuffer, IndexElementSize.ThirtyTwo); + commandBuffer.DrawIndexedPrimitives( + 0, + 0, + PrimitiveCount, + commandBuffer.PushVertexShaderUniforms(transformMatrix), + commandBuffer.PushFragmentShaderUniforms(CurrentFont.DistanceRange) + ); + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + VertexBuffer.Dispose(); + IndexBuffer.Dispose(); + } + + NativeMemory.Free(StringBytes); + Wellspring.Wellspring_DestroyTextBatch(Handle); + } + base.Dispose(disposing); } } } diff --git a/src/Graphics/GraphicsDevice.cs b/src/Graphics/GraphicsDevice.cs index 4fe5bca9..7fc07375 100644 --- a/src/Graphics/GraphicsDevice.cs +++ b/src/Graphics/GraphicsDevice.cs @@ -1,9 +1,10 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; +using MoonWorks.Video; using RefreshCS; +using WellspringCS; namespace MoonWorks.Graphics { @@ -21,6 +22,15 @@ namespace MoonWorks.Graphics // Built-in video pipeline internal GraphicsPipeline VideoPipeline { get; } + // Built-in text shader info + public GraphicsShaderInfo TextVertexShaderInfo { get; } + public GraphicsShaderInfo 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(); @@ -41,43 +51,91 @@ namespace MoonWorks.Graphics Conversions.BoolToByte(debugMode) ); - // Check for optional video shaders + // TODO: check for CreateDevice fail + + // Check for replacement stock shaders string basePath = System.AppContext.BaseDirectory; + string videoVertPath = Path.Combine(basePath, "video_fullscreen.vert.refresh"); string videoFragPath = Path.Combine(basePath, "video_yuv2rgba.frag.refresh"); + + string textVertPath = Path.Combine(basePath, "text_transform.vert.refresh"); + string textFragPath = Path.Combine(basePath, "text_msdf.frag.refresh"); + + ShaderModule videoVertShader; + ShaderModule videoFragShader; + + ShaderModule textVertShader; + ShaderModule textFragShader; + if (File.Exists(videoVertPath) && File.Exists(videoFragPath)) { - ShaderModule videoVertShader = new ShaderModule(this, videoVertPath); - ShaderModule videoFragShader = new ShaderModule(this, videoFragPath); - - VideoPipeline = new GraphicsPipeline( - this, - new GraphicsPipelineCreateInfo - { - AttachmentInfo = new GraphicsPipelineAttachmentInfo( - new ColorAttachmentDescription( - TextureFormat.R8G8B8A8, - ColorAttachmentBlendState.None - ) - ), - DepthStencilState = DepthStencilState.Disable, - VertexShaderInfo = GraphicsShaderInfo.Create( - videoVertShader, - "main", - 0 - ), - FragmentShaderInfo = GraphicsShaderInfo.Create( - videoFragShader, - "main", - 3 - ), - VertexInputState = VertexInputState.Empty, - RasterizerState = RasterizerState.CCW_CullNone, - PrimitiveType = PrimitiveType.TriangleList, - MultisampleState = MultisampleState.None - } - ); + videoVertShader = new ShaderModule(this, videoVertPath); + videoFragShader = new ShaderModule(this, videoFragPath); } + else + { + // use defaults + var assembly = typeof(GraphicsDevice).Assembly; + + using var vertStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.VideoFullscreen.vert.refresh"); + using var fragStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.VideoYUV2RGBA.frag.refresh"); + + videoVertShader = new ShaderModule(this, vertStream); + videoFragShader = new ShaderModule(this, fragStream); + } + + if (File.Exists(textVertPath) && File.Exists(textFragPath)) + { + textVertShader = new ShaderModule(this, textVertPath); + textFragShader = new ShaderModule(this, textFragPath); + } + else + { + // use defaults + var assembly = typeof(GraphicsDevice).Assembly; + + using var vertStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.TextTransform.vert.refresh"); + using var fragStream = assembly.GetManifestResourceStream("MoonWorks.Graphics.StockShaders.TextMSDF.frag.refresh"); + + textVertShader = new ShaderModule(this, vertStream); + textFragShader = new ShaderModule(this, fragStream); + } + + VideoPipeline = new GraphicsPipeline( + this, + new GraphicsPipelineCreateInfo + { + AttachmentInfo = new GraphicsPipelineAttachmentInfo( + new ColorAttachmentDescription( + TextureFormat.R8G8B8A8, + ColorAttachmentBlendState.None + ) + ), + DepthStencilState = DepthStencilState.Disable, + VertexShaderInfo = GraphicsShaderInfo.Create( + videoVertShader, + "main", + 0 + ), + FragmentShaderInfo = GraphicsShaderInfo.Create( + videoFragShader, + "main", + 3 + ), + VertexInputState = VertexInputState.Empty, + RasterizerState = RasterizerState.CCW_CullNone, + PrimitiveType = PrimitiveType.TriangleList, + MultisampleState = MultisampleState.None + } + ); + + TextVertexShaderInfo = GraphicsShaderInfo.Create(textVertShader, "main", 0); + TextFragmentShaderInfo = GraphicsShaderInfo.Create(textFragShader, "main", 1); + TextVertexInputState = VertexInputState.CreateSingleBinding(); + + PointSampler = new Sampler(this, SamplerCreateInfo.PointClamp); + LinearSampler = new Sampler(this, SamplerCreateInfo.LinearClamp); FencePool = new FencePool(this); } @@ -363,6 +421,16 @@ namespace MoonWorks.Graphics { 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) diff --git a/src/Graphics/GraphicsResource.cs b/src/Graphics/GraphicsResource.cs index 7eeccf9c..87d8576f 100644 --- a/src/Graphics/GraphicsResource.cs +++ b/src/Graphics/GraphicsResource.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.InteropServices; -using System.Threading; namespace MoonWorks.Graphics { @@ -8,13 +7,11 @@ namespace MoonWorks.Graphics public abstract class GraphicsResource : IDisposable { public GraphicsDevice Device { get; } - public IntPtr Handle { get => handle; internal set => handle = value; } - private nint handle; - - public bool IsDisposed { get; private set; } - protected abstract Action QueueDestroyFunction { get; } private GCHandle SelfReference; + + public bool IsDisposed { get; private set; } + protected GraphicsResource(GraphicsDevice device) { Device = device; @@ -23,7 +20,7 @@ namespace MoonWorks.Graphics Device.AddResourceReference(SelfReference); } - protected void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) { if (!IsDisposed) { @@ -33,13 +30,6 @@ namespace MoonWorks.Graphics SelfReference.Free(); } - // Atomically call destroy function in case this is called from the finalizer thread - var toDispose = Interlocked.Exchange(ref handle, IntPtr.Zero); - if (toDispose != IntPtr.Zero) - { - QueueDestroyFunction(Device.Handle, toDispose); - } - IsDisposed = true; } } diff --git a/src/Graphics/RefreshResource.cs b/src/Graphics/RefreshResource.cs new file mode 100644 index 00000000..875d6459 --- /dev/null +++ b/src/Graphics/RefreshResource.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace MoonWorks.Graphics; + +public abstract class RefreshResource : GraphicsResource +{ + public IntPtr Handle { get => handle; internal set => handle = value; } + private IntPtr handle; + + protected abstract Action QueueDestroyFunction { get; } + + protected RefreshResource(GraphicsDevice device) : base(device) + { + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + // Atomically call destroy function in case this is called from the finalizer thread + var toDispose = Interlocked.Exchange(ref handle, IntPtr.Zero); + if (toDispose != IntPtr.Zero) + { + QueueDestroyFunction(Device.Handle, toDispose); + } + } + base.Dispose(disposing); + } +} diff --git a/src/Graphics/Resources/Buffer.cs b/src/Graphics/Resources/Buffer.cs index 8563ac9c..b0349404 100644 --- a/src/Graphics/Resources/Buffer.cs +++ b/src/Graphics/Resources/Buffer.cs @@ -7,7 +7,7 @@ namespace MoonWorks.Graphics /// /// Buffers are generic data containers that can be used by the GPU. /// - public class Buffer : GraphicsResource + public class Buffer : RefreshResource { protected override Action QueueDestroyFunction => Refresh.Refresh_QueueDestroyBuffer; diff --git a/src/Graphics/Resources/ComputePipeline.cs b/src/Graphics/Resources/ComputePipeline.cs index 44545012..19312c21 100644 --- a/src/Graphics/Resources/ComputePipeline.cs +++ b/src/Graphics/Resources/ComputePipeline.cs @@ -6,7 +6,7 @@ namespace MoonWorks.Graphics /// /// Compute pipelines perform arbitrary parallel processing on input data. /// - public class ComputePipeline : GraphicsResource + public class ComputePipeline : RefreshResource { protected override Action QueueDestroyFunction => Refresh.Refresh_QueueDestroyComputePipeline; diff --git a/src/Graphics/Resources/Fence.cs b/src/Graphics/Resources/Fence.cs index fd380ae5..223620e3 100644 --- a/src/Graphics/Resources/Fence.cs +++ b/src/Graphics/Resources/Fence.cs @@ -10,7 +10,7 @@ namespace MoonWorks.Graphics /// The Fence object itself is basically just a wrapper for the Refresh_Fence.
/// The internal handle is replaced so that we can pool Fence objects to manage garbage. /// - public class Fence : GraphicsResource + public class Fence : RefreshResource { protected override Action QueueDestroyFunction => Refresh.Refresh_ReleaseFence; diff --git a/src/Graphics/Resources/GraphicsPipeline.cs b/src/Graphics/Resources/GraphicsPipeline.cs index a1e75067..421e9c56 100644 --- a/src/Graphics/Resources/GraphicsPipeline.cs +++ b/src/Graphics/Resources/GraphicsPipeline.cs @@ -8,7 +8,7 @@ namespace MoonWorks.Graphics /// Graphics pipelines encapsulate all of the render state in a single object.
/// These pipelines are bound before draw calls are issued. /// - public class GraphicsPipeline : GraphicsResource + public class GraphicsPipeline : RefreshResource { protected override Action QueueDestroyFunction => Refresh.Refresh_QueueDestroyGraphicsPipeline; diff --git a/src/Graphics/Resources/Sampler.cs b/src/Graphics/Resources/Sampler.cs index 39463ad3..ac523018 100644 --- a/src/Graphics/Resources/Sampler.cs +++ b/src/Graphics/Resources/Sampler.cs @@ -6,7 +6,7 @@ namespace MoonWorks.Graphics /// /// A sampler specifies how a texture will be sampled in a shader. /// - public class Sampler : GraphicsResource + public class Sampler : RefreshResource { protected override Action QueueDestroyFunction => Refresh.Refresh_QueueDestroySampler; diff --git a/src/Graphics/Resources/ShaderModule.cs b/src/Graphics/Resources/ShaderModule.cs index 61f87793..fad27cb9 100644 --- a/src/Graphics/Resources/ShaderModule.cs +++ b/src/Graphics/Resources/ShaderModule.cs @@ -8,7 +8,7 @@ namespace MoonWorks.Graphics /// /// Shader modules expect input in Refresh bytecode format. /// - public class ShaderModule : GraphicsResource + public class ShaderModule : RefreshResource { protected override Action QueueDestroyFunction => Refresh.Refresh_QueueDestroyShaderModule; diff --git a/src/Graphics/Resources/Texture.cs b/src/Graphics/Resources/Texture.cs index 8f1c9d0c..7a8fadb6 100644 --- a/src/Graphics/Resources/Texture.cs +++ b/src/Graphics/Resources/Texture.cs @@ -8,7 +8,7 @@ namespace MoonWorks.Graphics /// /// A container for pixel data. /// - public class Texture : GraphicsResource + public class Texture : RefreshResource { public uint Width { get; internal set; } public uint Height { get; internal set; } diff --git a/src/Graphics/StockShaders/Binary/text_msdf.frag.refresh b/src/Graphics/StockShaders/Binary/text_msdf.frag.refresh new file mode 100644 index 00000000..ba50a5aa Binary files /dev/null and b/src/Graphics/StockShaders/Binary/text_msdf.frag.refresh differ diff --git a/src/Graphics/StockShaders/Binary/text_transform.vert.refresh b/src/Graphics/StockShaders/Binary/text_transform.vert.refresh new file mode 100644 index 00000000..21bee62b Binary files /dev/null and b/src/Graphics/StockShaders/Binary/text_transform.vert.refresh differ diff --git a/src/Graphics/StockShaders/Binary/video_fullscreen.vert.refresh b/src/Graphics/StockShaders/Binary/video_fullscreen.vert.refresh new file mode 100644 index 00000000..131f3a67 Binary files /dev/null and b/src/Graphics/StockShaders/Binary/video_fullscreen.vert.refresh differ diff --git a/src/Graphics/StockShaders/Binary/video_yuv2rgba.frag.refresh b/src/Graphics/StockShaders/Binary/video_yuv2rgba.frag.refresh new file mode 100644 index 00000000..176e8b4c Binary files /dev/null and b/src/Graphics/StockShaders/Binary/video_yuv2rgba.frag.refresh differ diff --git a/src/Graphics/StockShaders/Source/text_msdf.frag b/src/Graphics/StockShaders/Source/text_msdf.frag new file mode 100644 index 00000000..046682e7 --- /dev/null +++ b/src/Graphics/StockShaders/Source/text_msdf.frag @@ -0,0 +1,34 @@ +#version 450 + +layout(set = 1, binding = 0) uniform sampler2D msdf; + +layout(location = 0) in vec2 inTexCoord; +layout(location = 1) in vec4 inColor; + +layout(location = 0) out vec4 outColor; + +layout(binding = 0, set = 3) uniform UBO +{ + float pxRange; +} ubo; + +float median(float r, float g, float b) +{ + return max(min(r, g), min(max(r, g), b)); +} + +float screenPxRange() +{ + vec2 unitRange = vec2(ubo.pxRange)/vec2(textureSize(msdf, 0)); + vec2 screenTexSize = vec2(1.0)/fwidth(inTexCoord); + return max(0.5*dot(unitRange, screenTexSize), 1.0); +} + +void main() +{ + vec3 msd = texture(msdf, inTexCoord).rgb; + float sd = median(msd.r, msd.g, msd.b); + float screenPxDistance = screenPxRange() * (sd - 0.5); + float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); + outColor = mix(vec4(0.0, 0.0, 0.0, 0.0), inColor, opacity); +} diff --git a/src/Graphics/StockShaders/Source/text_transform.vert b/src/Graphics/StockShaders/Source/text_transform.vert new file mode 100644 index 00000000..f64037c4 --- /dev/null +++ b/src/Graphics/StockShaders/Source/text_transform.vert @@ -0,0 +1,20 @@ +#version 450 + +layout(location = 0) in vec3 inPos; +layout(location = 1) in vec2 inTexCoord; +layout(location = 2) in vec4 inColor; + +layout(location = 0) out vec2 outTexCoord; +layout(location = 1) out vec4 outColor; + +layout(binding = 0, set = 2) uniform UBO +{ + mat4 ViewProjection; +} ubo; + +void main() +{ + gl_Position = ubo.ViewProjection * vec4(inPos, 1.0); + outTexCoord = inTexCoord; + outColor = inColor; +} diff --git a/src/Video/Shaders/video_fullscreen.vert b/src/Graphics/StockShaders/Source/video_fullscreen.vert similarity index 100% rename from src/Video/Shaders/video_fullscreen.vert rename to src/Graphics/StockShaders/Source/video_fullscreen.vert diff --git a/src/Video/Shaders/video_yuv2rgba.frag b/src/Graphics/StockShaders/Source/video_yuv2rgba.frag similarity index 100% rename from src/Video/Shaders/video_yuv2rgba.frag rename to src/Graphics/StockShaders/Source/video_yuv2rgba.frag diff --git a/src/Video/VideoAV1.cs b/src/Video/VideoAV1.cs index 9aaf157e..b1eb6074 100644 --- a/src/Video/VideoAV1.cs +++ b/src/Video/VideoAV1.cs @@ -1,12 +1,13 @@ using System; using System.IO; +using MoonWorks.Graphics; namespace MoonWorks.Video { /// /// This class takes in a filename for AV1 data in .obu (open bitstream unit) format /// - public unsafe class VideoAV1 + public unsafe class VideoAV1 : GraphicsResource { public string Filename { get; } @@ -28,7 +29,7 @@ namespace MoonWorks.Video /// /// Opens an AV1 file so it can be loaded by VideoPlayer. You must also provide a playback framerate. /// - public VideoAV1(string filename, double framesPerSecond) + public VideoAV1(GraphicsDevice device, string filename, double framesPerSecond) : base(device) { if (!File.Exists(filename)) { @@ -67,8 +68,22 @@ namespace MoonWorks.Video Filename = filename; - StreamA = new VideoAV1Stream(this); - StreamB = new VideoAV1Stream(this); + StreamA = new VideoAV1Stream(device, this); + StreamB = new VideoAV1Stream(device, this); + } + + // NOTE: if you call this while a VideoPlayer is playing the stream, your program will explode + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + StreamA.Dispose(); + StreamB.Dispose(); + } + } + base.Dispose(disposing); } } } diff --git a/src/Video/VideoAV1Stream.cs b/src/Video/VideoAV1Stream.cs index ea89f8ce..cc40aa0c 100644 --- a/src/Video/VideoAV1Stream.cs +++ b/src/Video/VideoAV1Stream.cs @@ -1,8 +1,9 @@ using System; +using MoonWorks.Graphics; namespace MoonWorks.Video { - internal class VideoAV1Stream + internal class VideoAV1Stream : GraphicsResource { public IntPtr Handle => handle; IntPtr handle; @@ -19,9 +20,7 @@ namespace MoonWorks.Video public bool FrameDataUpdated { get; set; } - bool IsDisposed; - - public VideoAV1Stream(VideoAV1 video) + public VideoAV1Stream(GraphicsDevice device, VideoAV1 video) : base(device) { if (Dav1dfile.df_fopen(video.Filename, out handle) == 0) { @@ -71,32 +70,13 @@ namespace MoonWorks.Video } } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { if (!IsDisposed) { - if (disposing) - { - // dispose managed state (managed objects) - } - - // free unmanaged resources (unmanaged objects) Dav1dfile.df_close(Handle); - - IsDisposed = true; } - } - - ~VideoAV1Stream() - { - 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); + base.Dispose(disposing); } } } diff --git a/src/Video/VideoPlayer.cs b/src/Video/VideoPlayer.cs index 826e7b1b..f2d079a5 100644 --- a/src/Video/VideoPlayer.cs +++ b/src/Video/VideoPlayer.cs @@ -8,7 +8,7 @@ namespace MoonWorks.Video /// /// A structure for continuous decoding of AV1 videos and rendering them into a texture. /// - public unsafe class VideoPlayer : IDisposable + public unsafe class VideoPlayer : GraphicsResource { public Texture RenderTexture { get; private set; } = null; public VideoState State { get; private set; } = VideoState.Stopped; @@ -18,6 +18,10 @@ namespace MoonWorks.Video private VideoAV1 Video = null; private VideoAV1Stream CurrentStream = null; + private Task ReadNextFrameTask; + private Task ResetStreamATask; + private Task ResetStreamBTask; + private GraphicsDevice GraphicsDevice; private Texture yTexture = null; private Texture uTexture = null; @@ -30,17 +34,11 @@ namespace MoonWorks.Video private double lastTimestamp; private double timeElapsed; - private bool disposed; - - public VideoPlayer(GraphicsDevice graphicsDevice) + public VideoPlayer(GraphicsDevice device) : base(device) { - GraphicsDevice = graphicsDevice; - if (GraphicsDevice.VideoPipeline == null) - { - throw new InvalidOperationException("Missing video shaders!"); - } + GraphicsDevice = device; - LinearSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp); + LinearSampler = new Sampler(device, SamplerCreateInfo.LinearClamp); timer = new Stopwatch(); } @@ -168,6 +166,8 @@ namespace MoonWorks.Video public void Unload() { Stop(); + ResetStreamATask?.Wait(); + ResetStreamBTask?.Wait(); Video = null; } @@ -194,7 +194,8 @@ namespace MoonWorks.Video } currentFrame = thisFrame; - Task.Run(CurrentStream.ReadNextFrame).ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); + ReadNextFrameTask = Task.Run(CurrentStream.ReadNextFrame); + ReadNextFrameTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); } if (CurrentStream.Ended) @@ -202,7 +203,17 @@ namespace MoonWorks.Video timer.Stop(); timer.Reset(); - Task.Run(CurrentStream.Reset).ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); + var task = Task.Run(CurrentStream.Reset); + task.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); + + if (CurrentStream == Video.StreamA) + { + ResetStreamATask = task; + } + else + { + ResetStreamBTask = task; + } if (Loop) { @@ -280,8 +291,12 @@ namespace MoonWorks.Video private void InitializeDav1dStream() { - Task.Run(Video.StreamA.Reset).ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); - Task.Run(Video.StreamB.Reset).ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); + ReadNextFrameTask?.Wait(); + + ResetStreamATask = Task.Run(Video.StreamA.Reset); + ResetStreamATask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); + ResetStreamBTask = Task.Run(Video.StreamB.Reset); + ResetStreamBTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); CurrentStream = Video.StreamA; currentFrame = -1; @@ -289,37 +304,27 @@ namespace MoonWorks.Video private static void HandleTaskException(Task task) { - throw task.Exception; - } - - protected virtual void Dispose(bool disposing) - { - if (!disposed) + if (task.Exception.InnerException is not TaskCanceledException) { - if (disposing) - { - // dispose managed state (managed objects) - RenderTexture.Dispose(); - yTexture.Dispose(); - uTexture.Dispose(); - vTexture.Dispose(); - } - - disposed = true; + throw task.Exception; } } - ~VideoPlayer() + protected override void Dispose(bool disposing) { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } + if (!IsDisposed) + { + if (disposing) + { + Unload(); - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + RenderTexture?.Dispose(); + yTexture?.Dispose(); + uTexture?.Dispose(); + vTexture?.Dispose(); + } + } + base.Dispose(disposing); } } }