From 4dbd5a2cbeab38f78274644f3b44fe1fa727f304 Mon Sep 17 00:00:00 2001 From: cosmonaut Date: Fri, 15 Dec 2023 18:46:43 +0000 Subject: [PATCH] MSDF font rendering + improved resource tracking (#52) This is a major rewrite of the Font system. MoonWorks now uses MSDF font rendering, which allows high quality rendering of fonts at arbitrary sizes. We now ship default embedded shader binaries for Video and Font. If you replace them with shader binaries of the same name located in your base directory, those will be used instead. Many improvements have been made to resource tracking to prevent memory corruption, particularly on shutdown. You must be careful not to leak AudioResource classes in particular, as there isn't much we can automatically do to recover from this without potentially crashing your game. Reviewed-on: https://gitea.moonside.games/MoonsideGames/MoonWorks/pulls/52 --- MoonWorks.csproj | 15 ++ MoonWorks.dll.config | 4 +- lib/WellspringCS | 2 +- lib/dav1dfile | 2 +- src/Audio/AudioDevice.cs | 44 ++++-- src/Audio/StreamingVoice.cs | 13 +- src/Game.cs | 15 +- src/Graphics/Font/Font.cs | 130 +++++++++++++---- src/Graphics/Font/Packer.cs | 103 -------------- src/Graphics/Font/Structs.cs | 18 ++- src/Graphics/Font/TextBatch.cs | 110 ++++++++++----- src/Graphics/GraphicsDevice.cs | 132 +++++++++++++----- src/Graphics/GraphicsResource.cs | 18 +-- src/Graphics/RefreshResource.cs | 31 ++++ src/Graphics/Resources/Buffer.cs | 2 +- src/Graphics/Resources/ComputePipeline.cs | 2 +- src/Graphics/Resources/Fence.cs | 2 +- src/Graphics/Resources/GraphicsPipeline.cs | 2 +- src/Graphics/Resources/Sampler.cs | 2 +- src/Graphics/Resources/ShaderModule.cs | 2 +- src/Graphics/Resources/Texture.cs | 2 +- .../Binary/text_msdf.frag.refresh | Bin 0 -> 2697 bytes .../Binary/text_transform.vert.refresh | Bin 0 -> 1529 bytes .../Binary/video_fullscreen.vert.refresh | Bin 0 -> 1997 bytes .../Binary/video_yuv2rgba.frag.refresh | Bin 0 -> 3030 bytes .../StockShaders/Source/text_msdf.frag | 34 +++++ .../StockShaders/Source/text_transform.vert | 20 +++ .../Source}/video_fullscreen.vert | 0 .../StockShaders/Source}/video_yuv2rgba.frag | 0 src/Video/VideoAV1.cs | 23 ++- src/Video/VideoAV1Stream.cs | 30 +--- src/Video/VideoPlayer.cs | 83 +++++------ 32 files changed, 515 insertions(+), 326 deletions(-) delete mode 100644 src/Graphics/Font/Packer.cs create mode 100644 src/Graphics/RefreshResource.cs create mode 100644 src/Graphics/StockShaders/Binary/text_msdf.frag.refresh create mode 100644 src/Graphics/StockShaders/Binary/text_transform.vert.refresh create mode 100644 src/Graphics/StockShaders/Binary/video_fullscreen.vert.refresh create mode 100644 src/Graphics/StockShaders/Binary/video_yuv2rgba.frag.refresh create mode 100644 src/Graphics/StockShaders/Source/text_msdf.frag create mode 100644 src/Graphics/StockShaders/Source/text_transform.vert rename src/{Video/Shaders => Graphics/StockShaders/Source}/video_fullscreen.vert (100%) rename src/{Video/Shaders => Graphics/StockShaders/Source}/video_yuv2rgba.frag (100%) diff --git a/MoonWorks.csproj b/MoonWorks.csproj index e09a901..2e0eb18 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 67f28dc..167fa25 100644 --- a/MoonWorks.dll.config +++ b/MoonWorks.dll.config @@ -13,8 +13,8 @@ - - + + diff --git a/lib/WellspringCS b/lib/WellspringCS index f8872ba..074f2af 160000 --- a/lib/WellspringCS +++ b/lib/WellspringCS @@ -1 +1 @@ -Subproject commit f8872bae59e394b0f8a35224bb39ab8fd041af97 +Subproject commit 074f2afc833b221906bb2468735041ce78f2cb89 diff --git a/lib/dav1dfile b/lib/dav1dfile index 3dcd69f..5065e2c 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 17f617e..48ef387 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 528e7ec..22c886c 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 1b7286f..096afc7 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 cf4982e..0dd6d1c 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 9fa053c..0000000 --- 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 8231039..f5f0eec 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 119fbde..7aef845 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 4fe5bca..7fc0737 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 7eeccf9..87d8576 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 0000000..875d645 --- /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 8563ac9..b034940 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 4454501..19312c2 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 fd380ae..223620e 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 a1e7506..421e9c5 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 39463ad..ac52301 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 61f8779..fad27cb 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 8f1c9d0..7a8fadb 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 0000000000000000000000000000000000000000..ba50a5aa2a2d33d398d20d8b5b57b6728ccf2cc0 GIT binary patch literal 2697 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..176e8b4cab11b1f354d8b8560720f4bdc97b806f GIT binary patch literal 3030 zcma)8*=`&~6fN8F!i32}HbMfl6P7WaG`-9k$4fGHOe9MNk1@+jYvgHLBTZ(ko*rX^ zB9VW{XYjxSuYknA@CQ8b5{YxF`*tcGTco1vy346^t8U$z&er4R7h}IoDK#;EcUq}2 zbzbGwno7oHH4cx0Z%WVW8_f-S5V!48*&*VBn$SREE~;sD4m0IxtJ7oPGQ@^l0*-zp zfHnbErT)UAb1J9SH-h!e&Gn77VE5o47{tfjFz9xAVbJbG;cnbH42hdDaGl<6ci1)o zlR%hxCz0bVS zTKkQDw@<=#WM_^$5Y|F$uH~T3d>?2lv6;)n(L7#g5_p;>iVRH>}j4@9cGda0WSyzMp%kA&~5W5ch*Jvi&?|smT--M8xy|9GLw26~Ex1h5RB+qT=W}f$9 z6GOk`;X9(AL-JVAnTI{ZJhX|EJa?e8pCr#+=w_aeU=u^XC_5Jd8qb^}$}Z--=61mi*LP_pK=0?+oHFD#Xs*LOz%NjyL0099n_kfha~8%4I&5@%?c2uY=GLkMf@f!uY!2fQ79%%k znmfUEE7}j^S3HOPPTShUS2o~(Ckk_~Z^F0maYsJx3YvXnEoS#_C)ZpUx!__}h&lVn zSbU7da$(tGaY&WVTKppx%Z+4<#qmFjt@*{Irheg`V>=bEyjQfKWfL!-N6hX?AGF<% z3pjL9*1EP^DLEK^$@e|4TK4NH7f8YA+SR&O$EbNV->+3X-zimU)$-olC_iiQ=KR>J z=W6PDz3lmJ#jBOvYF(2jC-IZ0=U1JIQ$8~-tN7+=J==9%eV|&oT&_Dc&+%PdJRbOz zQ~dyX7a^4M89Uu<@uWK~-DRdlc!uOqZSAJaT1v3XDSh^7Ya!WE42ye{I(H3;+NC literal 0 HcmV?d00001 diff --git a/src/Graphics/StockShaders/Source/text_msdf.frag b/src/Graphics/StockShaders/Source/text_msdf.frag new file mode 100644 index 0000000..046682e --- /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 0000000..f64037c --- /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 9aaf157..b1eb607 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 ea89f8c..cc40aa0 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 826e7b1..f2d079a 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); } } }