diff --git a/src/Graphics/Font/Font.cs b/src/Graphics/Font/Font.cs index a38f899..b1af731 100644 --- a/src/Graphics/Font/Font.cs +++ b/src/Graphics/Font/Font.cs @@ -5,7 +5,7 @@ using WellspringCS; namespace MoonWorks.Graphics.Font { - public unsafe class Font : IDisposable + public unsafe class Font : GraphicsResource { public Texture Texture { get; } public float PixelsPerEm { get; } @@ -16,8 +16,6 @@ namespace MoonWorks.Graphics.Font private byte* StringBytes; private int StringBytesLength; - private bool IsDisposed; - public unsafe static Font Load( GraphicsDevice graphicsDevice, CommandBuffer commandBuffer, @@ -49,10 +47,10 @@ namespace MoonWorks.Graphics.Font NativeMemory.Free(fontFileByteBuffer); NativeMemory.Free(atlasFileByteBuffer); - return new Font(handle, texture, pixelsPerEm, distanceRange); + return new Font(graphicsDevice, handle, texture, pixelsPerEm, distanceRange); } - private Font(IntPtr handle, Texture texture, float pixelsPerEm, float distanceRange) + private Font(GraphicsDevice device, IntPtr handle, Texture texture, float pixelsPerEm, float distanceRange) : base(device) { Handle = handle; Texture = texture; @@ -101,7 +99,7 @@ namespace MoonWorks.Graphics.Font return true; } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { if (!IsDisposed) { @@ -111,21 +109,8 @@ namespace MoonWorks.Graphics.Font } 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/TextBatch.cs b/src/Graphics/Font/TextBatch.cs index 390727c..a3b5e28 100644 --- a/src/Graphics/Font/TextBatch.cs +++ b/src/Graphics/Font/TextBatch.cs @@ -4,7 +4,7 @@ using WellspringCS; namespace MoonWorks.Graphics.Font { - public unsafe class TextBatch : IDisposable + public unsafe class TextBatch : GraphicsResource { public const int MAX_CHARS = 4096; public const int MAX_VERTICES = MAX_CHARS * 4; @@ -22,11 +22,9 @@ namespace MoonWorks.Graphics.Font private byte* StringBytes; private int StringBytesLength; - private bool IsDisposed; - - public TextBatch(GraphicsDevice graphicsDevice) + public TextBatch(GraphicsDevice device) : base(device) { - GraphicsDevice = graphicsDevice; + GraphicsDevice = device; Handle = Wellspring.Wellspring_CreateTextBatch(); StringBytesLength = 128; @@ -102,7 +100,7 @@ namespace MoonWorks.Graphics.Font PrimitiveCount = vertexCount / 2; // FIXME: is this jank? } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { if (!IsDisposed) { @@ -114,22 +112,8 @@ namespace MoonWorks.Graphics.Font NativeMemory.Free(StringBytes); Wellspring.Wellspring_DestroyTextBatch(Handle); - - IsDisposed = true; } - } - - ~TextBatch() - { - // 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/GraphicsDevice.cs b/src/Graphics/GraphicsDevice.cs index 4fe5bca..9920b09 100644 --- a/src/Graphics/GraphicsDevice.cs +++ b/src/Graphics/GraphicsDevice.cs @@ -1,8 +1,8 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; +using MoonWorks.Video; using RefreshCS; namespace MoonWorks.Graphics @@ -363,6 +363,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/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..cf14eeb 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,15 @@ namespace MoonWorks.Video private double lastTimestamp; private double timeElapsed; - private bool disposed; - - public VideoPlayer(GraphicsDevice graphicsDevice) + public VideoPlayer(GraphicsDevice device) : base(device) { - GraphicsDevice = graphicsDevice; + GraphicsDevice = device; if (GraphicsDevice.VideoPipeline == null) { throw new InvalidOperationException("Missing video shaders!"); } - LinearSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp); + LinearSampler = new Sampler(device, SamplerCreateInfo.LinearClamp); timer = new Stopwatch(); } @@ -168,6 +170,8 @@ namespace MoonWorks.Video public void Unload() { Stop(); + ResetStreamATask?.Wait(); + ResetStreamBTask?.Wait(); Video = null; } @@ -194,7 +198,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 +207,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 +295,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 +308,27 @@ namespace MoonWorks.Video private static void HandleTaskException(Task task) { - throw task.Exception; + if (task.Exception.InnerException is not TaskCanceledException) + { + throw task.Exception; + } } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { - if (!disposed) + if (!IsDisposed) { if (disposing) { - // dispose managed state (managed objects) + Unload(); + RenderTexture.Dispose(); yTexture.Dispose(); uTexture.Dispose(); vTexture.Dispose(); } - - disposed = true; } - } - - ~VideoPlayer() - { - // 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); } } }