MSDF font rendering + improved resource tracking #52
			
				
			
		
		
		
	|  | @ -24,4 +24,19 @@ | |||
| 			<CopyToPublishDirectory>Never</CopyToPublishDirectory> | ||||
| 		</None> | ||||
| 	</ItemGroup> | ||||
| 
 | ||||
| 	<ItemGroup> | ||||
| 		<EmbeddedResource Include="src\Graphics\StockShaders\Binary\video_fullscreen.vert.refresh"> | ||||
| 			<LogicalName>MoonWorks.Graphics.StockShaders.VideoFullscreen.vert.refresh</LogicalName> | ||||
| 		</EmbeddedResource> | ||||
| 		<EmbeddedResource Include="src\Graphics\StockShaders\Binary\video_yuv2rgba.frag.refresh"> | ||||
| 			<LogicalName>MoonWorks.Graphics.StockShaders.VideoYUV2RGBA.frag.refresh</LogicalName> | ||||
| 		</EmbeddedResource> | ||||
| 		<EmbeddedResource Include="src\Graphics\StockShaders\Binary\text_transform.vert.refresh"> | ||||
| 			<LogicalName>MoonWorks.Graphics.StockShaders.TextTransform.vert.refresh</LogicalName> | ||||
| 		</EmbeddedResource> | ||||
| 		<EmbeddedResource Include="src\Graphics\StockShaders\Binary\text_msdf.frag.refresh"> | ||||
| 			<LogicalName>MoonWorks.Graphics.StockShaders.TextMSDF.frag.refresh</LogicalName> | ||||
| 		</EmbeddedResource> | ||||
| 	</ItemGroup> | ||||
| </Project> | ||||
|  |  | |||
|  | @ -13,8 +13,8 @@ | |||
| 	<dllmap dll="FAudio" os="linux,freebsd,netbsd" target="libFAudio.so.0"/> | ||||
| 
 | ||||
| 	<dllmap dll="Wellspring" os="windows" target="Wellspring.dll"/> | ||||
| 	<dllmap dll="Wellspring" os="osx" target="libWellspring.0.dylib"/> | ||||
| 	<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.0"/> | ||||
| 	<dllmap dll="Wellspring" os="osx" target="libWellspring.1.dylib"/> | ||||
| 	<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.1"/> | ||||
| 
 | ||||
| 	<dllmap dll="dav1dfile" os="windows" target="dav1dfile.dll"/> | ||||
| 	<dllmap dll="dav1dfile" os="osx" target="libdav1dfile.1.dylib"/> | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| Subproject commit f8872bae59e394b0f8a35224bb39ab8fd041af97 | ||||
| Subproject commit 074f2afc833b221906bb2468735041ce78f2cb89 | ||||
|  | @ -1 +1 @@ | |||
| Subproject commit 3dcd69ff85db80eea51481edd323b42c05993e1a | ||||
| Subproject commit 5065e2cd4662dbe023b77a45ef967f975170dfff | ||||
|  | @ -25,7 +25,7 @@ namespace MoonWorks.Audio | |||
| 		public float DopplerScale = 1f; | ||||
| 		public float SpeedOfSound = 343.5f; | ||||
| 
 | ||||
| 		private readonly HashSet<GCHandle> resources = new HashSet<GCHandle>(); | ||||
| 		private readonly HashSet<GCHandle> resourceHandles = new HashSet<GCHandle>(); | ||||
| 		private readonly HashSet<UpdatingSourceVoice> updatingSourceVoices = new HashSet<UpdatingSourceVoice>(); | ||||
| 
 | ||||
| 		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.FAudio_Release(Handle); | ||||
| 
 | ||||
| 				IsDisposed = true; | ||||
|  |  | |||
|  | @ -149,6 +149,8 @@ namespace MoonWorks.Audio | |||
| 		protected override unsafe void Dispose(bool disposing) | ||||
| 		{ | ||||
| 			if (!IsDisposed) | ||||
| 			{ | ||||
| 				lock (StateLock) | ||||
| 				{ | ||||
| 					Stop(); | ||||
| 
 | ||||
|  | @ -160,6 +162,7 @@ namespace MoonWorks.Audio | |||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			base.Dispose(disposing); | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										15
									
								
								src/Game.cs
								
								
								
								
							
							
						
						
									
										15
									
								
								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(); | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,45 +5,117 @@ using WellspringCS; | |||
| 
 | ||||
| namespace MoonWorks.Graphics.Font | ||||
| { | ||||
|     public class Font : IDisposable | ||||
| 	public unsafe class Font : GraphicsResource | ||||
| 	{ | ||||
| 		public IntPtr Handle { get; } | ||||
| 		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<byte>(fileByteBuffer, (int) fileStream.Length); | ||||
| 	        fileStream.ReadExactly(fileByteSpan); | ||||
| 	        fileStream.Close(); | ||||
| 		private byte* StringBytes; | ||||
| 		private int StringBytesLength; | ||||
| 
 | ||||
| 	        Handle = Wellspring.Wellspring_CreateFont((IntPtr) fileByteBuffer, (uint) fileByteSpan.Length); | ||||
| 		/// <summary> | ||||
| 		/// 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. | ||||
| 		/// </summary> | ||||
| 		/// <returns></returns> | ||||
| 		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<byte>(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<byte>(atlasFileByteBuffer, (int) atlasFileStream.Length); | ||||
| 			atlasFileStream.ReadExactly(atlasFileByteSpan); | ||||
| 			atlasFileStream.Close(); | ||||
| 
 | ||||
| 			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); | ||||
| 		} | ||||
| 
 | ||||
| 		protected virtual void Dispose(bool disposing) | ||||
| 		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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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<Wellspring.FontRange>(); | ||||
| 				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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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 | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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<Vertex>(GraphicsDevice, BufferUsageFlags.Vertex, INITIAL_VERTEX_COUNT); | ||||
| 			IndexBuffer = Buffer.Create<uint>(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; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 		// Call this after you have made all the Draw calls you want. | ||||
| 			return true; | ||||
| 		} | ||||
| 
 | ||||
| 		// 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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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<GCHandle> resources = new HashSet<GCHandle>(); | ||||
|  | @ -41,14 +51,56 @@ 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); | ||||
| 				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, | ||||
|  | @ -77,7 +129,13 @@ namespace MoonWorks.Graphics | |||
| 					MultisampleState = MultisampleState.None | ||||
| 				} | ||||
| 			); | ||||
| 			} | ||||
| 
 | ||||
| 			TextVertexShaderInfo = GraphicsShaderInfo.Create<Math.Float.Matrix4x4>(textVertShader, "main", 0); | ||||
| 			TextFragmentShaderInfo = GraphicsShaderInfo.Create<float>(textFragShader, "main", 1); | ||||
| 			TextVertexInputState = VertexInputState.CreateSingleBinding<Font.Vertex>(); | ||||
| 
 | ||||
| 			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) | ||||
|  |  | |||
|  | @ -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<IntPtr, IntPtr> 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; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -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<IntPtr, IntPtr> 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); | ||||
| 	} | ||||
| } | ||||
|  | @ -7,7 +7,7 @@ namespace MoonWorks.Graphics | |||
| 	/// <summary> | ||||
| 	/// Buffers are generic data containers that can be used by the GPU. | ||||
| 	/// </summary> | ||||
| 	public class Buffer : GraphicsResource | ||||
| 	public class Buffer : RefreshResource | ||||
| 	{ | ||||
| 		protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyBuffer; | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ namespace MoonWorks.Graphics | |||
| 	/// <summary> | ||||
| 	/// Compute pipelines perform arbitrary parallel processing on input data. | ||||
| 	/// </summary> | ||||
| 	public class ComputePipeline : GraphicsResource | ||||
| 	public class ComputePipeline : RefreshResource | ||||
| 	{ | ||||
| 		protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyComputePipeline; | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ namespace MoonWorks.Graphics | |||
| 	/// The Fence object itself is basically just a wrapper for the Refresh_Fence. <br/> | ||||
| 	/// The internal handle is replaced so that we can pool Fence objects to manage garbage. | ||||
| 	/// </summary> | ||||
| 	public class Fence : GraphicsResource | ||||
| 	public class Fence : RefreshResource | ||||
| 	{ | ||||
| 		protected override Action<nint, nint> QueueDestroyFunction => Refresh.Refresh_ReleaseFence; | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ namespace MoonWorks.Graphics | |||
| 	/// Graphics pipelines encapsulate all of the render state in a single object. <br/> | ||||
| 	/// These pipelines are bound before draw calls are issued. | ||||
| 	/// </summary> | ||||
| 	public class GraphicsPipeline : GraphicsResource | ||||
| 	public class GraphicsPipeline : RefreshResource | ||||
| 	{ | ||||
| 		protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyGraphicsPipeline; | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ namespace MoonWorks.Graphics | |||
| 	/// <summary> | ||||
| 	/// A sampler specifies how a texture will be sampled in a shader. | ||||
| 	/// </summary> | ||||
| 	public class Sampler : GraphicsResource | ||||
| 	public class Sampler : RefreshResource | ||||
| 	{ | ||||
| 		protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroySampler; | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ namespace MoonWorks.Graphics | |||
| 	/// <summary> | ||||
| 	/// Shader modules expect input in Refresh bytecode format. | ||||
| 	/// </summary> | ||||
| 	public class ShaderModule : GraphicsResource | ||||
| 	public class ShaderModule : RefreshResource | ||||
| 	{ | ||||
| 		protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyShaderModule; | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ namespace MoonWorks.Graphics | |||
| 	/// <summary> | ||||
| 	/// A container for pixel data. | ||||
| 	/// </summary> | ||||
| 	public class Texture : GraphicsResource | ||||
| 	public class Texture : RefreshResource | ||||
| 	{ | ||||
| 		public uint Width { get; internal set; } | ||||
| 		public uint Height { get; internal set; } | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -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); | ||||
| } | ||||
|  | @ -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; | ||||
| } | ||||
|  | @ -1,12 +1,13 @@ | |||
| using System; | ||||
| using System.IO; | ||||
| using MoonWorks.Graphics; | ||||
| 
 | ||||
| namespace MoonWorks.Video | ||||
| { | ||||
| 	/// <summary> | ||||
| 	/// This class takes in a filename for AV1 data in .obu (open bitstream unit) format | ||||
| 	/// </summary> | ||||
| 	public unsafe class VideoAV1 | ||||
| 	public unsafe class VideoAV1 : GraphicsResource | ||||
| 	{ | ||||
| 		public string Filename { get; } | ||||
| 
 | ||||
|  | @ -28,7 +29,7 @@ namespace MoonWorks.Video | |||
| 		/// <summary> | ||||
| 		/// Opens an AV1 file so it can be loaded by VideoPlayer. You must also provide a playback framerate. | ||||
| 		/// </summary> | ||||
| 		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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ namespace MoonWorks.Video | |||
| 	/// <summary> | ||||
| 	/// A structure for continuous decoding of AV1 videos and rendering them into a texture. | ||||
| 	/// </summary> | ||||
| 	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,46 +291,40 @@ 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; | ||||
| 		} | ||||
| 
 | ||||
| 		private static void HandleTaskException(Task task) | ||||
| 		{ | ||||
| 			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) | ||||
| 					RenderTexture.Dispose(); | ||||
| 					yTexture.Dispose(); | ||||
| 					uTexture.Dispose(); | ||||
| 					vTexture.Dispose(); | ||||
| 				} | ||||
| 					Unload(); | ||||
| 
 | ||||
| 				disposed = true; | ||||
| 					RenderTexture?.Dispose(); | ||||
| 					yTexture?.Dispose(); | ||||
| 					uTexture?.Dispose(); | ||||
| 					vTexture?.Dispose(); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 		~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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue