MSDF font rendering + improved resource tracking #52

Merged
cosmonaut merged 17 commits from msdf into main 2023-12-15 18:46:44 +00:00
32 changed files with 515 additions and 326 deletions

View File

@ -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>

View File

@ -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

View File

@ -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.FAudioVoice_DestroyVoice(trueMasteringVoice);
FAudio.FAudio_Release(Handle);
IsDisposed = true;

View File

@ -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]);
}
}
}
}

View File

@ -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();
}

View File

@ -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<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();
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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
};
}
}

View File

@ -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;
}
}
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);
}
}
}

View File

@ -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,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<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)

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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; }

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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,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);
}
}
}