MSDF font rendering + improved resource tracking (#52)

This is a major rewrite of the Font system. MoonWorks now uses MSDF font rendering, which allows high quality rendering of fonts at arbitrary sizes.

We now ship default embedded shader binaries for Video and Font. If you replace them with shader binaries of the same name located in your base directory, those will be used instead.

Many improvements have been made to resource tracking to prevent memory corruption, particularly on shutdown.

You must be careful not to leak AudioResource classes in particular, as there isn't much we can automatically do to recover from this without potentially crashing your game.

Reviewed-on: MoonsideGames/MoonWorks#52
remotes/1734711610904720328/main
cosmonaut 2023-12-15 18:46:43 +00:00
parent 2e890fd696
commit 4dbd5a2cbe
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);
}
}
}