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> <CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None> </None>
</ItemGroup> </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> </Project>

View File

@ -13,8 +13,8 @@
<dllmap dll="FAudio" os="linux,freebsd,netbsd" target="libFAudio.so.0"/> <dllmap dll="FAudio" os="linux,freebsd,netbsd" target="libFAudio.so.0"/>
<dllmap dll="Wellspring" os="windows" target="Wellspring.dll"/> <dllmap dll="Wellspring" os="windows" target="Wellspring.dll"/>
<dllmap dll="Wellspring" os="osx" target="libWellspring.0.dylib"/> <dllmap dll="Wellspring" os="osx" target="libWellspring.1.dylib"/>
<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.0"/> <dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.1"/>
<dllmap dll="dav1dfile" os="windows" target="dav1dfile.dll"/> <dllmap dll="dav1dfile" os="windows" target="dav1dfile.dll"/>
<dllmap dll="dav1dfile" os="osx" target="libdav1dfile.1.dylib"/> <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 DopplerScale = 1f;
public float SpeedOfSound = 343.5f; 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 readonly HashSet<UpdatingSourceVoice> updatingSourceVoices = new HashSet<UpdatingSourceVoice>();
private AudioTweenManager AudioTweenManager; private AudioTweenManager AudioTweenManager;
@ -123,7 +123,6 @@ namespace MoonWorks.Audio
AudioTweenManager = new AudioTweenManager(); AudioTweenManager = new AudioTweenManager();
VoicePool = new SourceVoicePool(this); VoicePool = new SourceVoicePool(this);
Logger.LogInfo("Setting up audio thread...");
WakeSignal = new AutoResetEvent(true); WakeSignal = new AutoResetEvent(true);
Thread = new Thread(ThreadMain); Thread = new Thread(ThreadMain);
@ -265,7 +264,7 @@ namespace MoonWorks.Audio
{ {
lock (StateLock) lock (StateLock)
{ {
resources.Add(resourceReference); resourceHandles.Add(resourceReference);
if (resourceReference.Target is UpdatingSourceVoice updatableVoice) if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
{ {
@ -278,7 +277,12 @@ namespace MoonWorks.Audio
{ {
lock (StateLock) 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(); Thread.Join();
// dispose all voices first // dispose all source voices first
foreach (var resource in resources) foreach (var handle in resourceHandles)
{ {
if (resource.Target is Voice voice) if (handle.Target is SourceVoice voice)
{ {
voice.Dispose(); voice.Dispose();
} }
} }
// destroy all other audio resources // dispose all submix voices except the faux mastering voice
foreach (var resource in resources) 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); 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); FAudio.FAudio_Release(Handle);
IsDisposed = true; IsDisposed = true;

View File

@ -149,6 +149,8 @@ namespace MoonWorks.Audio
protected override unsafe void Dispose(bool disposing) protected override unsafe void Dispose(bool disposing)
{ {
if (!IsDisposed) if (!IsDisposed)
{
lock (StateLock)
{ {
Stop(); Stop();
@ -160,6 +162,7 @@ namespace MoonWorks.Audio
} }
} }
} }
}
base.Dispose(disposing); base.Dispose(disposing);
} }
} }

View File

@ -1,5 +1,4 @@
using System.Collections.Generic; using SDL2;
using SDL2;
using MoonWorks.Audio; using MoonWorks.Audio;
using MoonWorks.Graphics; using MoonWorks.Graphics;
using MoonWorks.Input; using MoonWorks.Input;
@ -58,6 +57,7 @@ namespace MoonWorks
bool debugMode = false bool debugMode = false
) )
{ {
Logger.LogInfo("Initializing frame limiter...");
Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep); Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep);
gameTimer = Stopwatch.StartNew(); gameTimer = Stopwatch.StartNew();
@ -68,6 +68,7 @@ namespace MoonWorks
previousSleepTimes[i] = TimeSpan.FromMilliseconds(1); 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) if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0)
{ {
Logger.LogError("Failed to initialize SDL!"); Logger.LogError("Failed to initialize SDL!");
@ -76,13 +77,16 @@ namespace MoonWorks
Logger.Initialize(); Logger.Initialize();
Logger.LogInfo("Initializing input...");
Inputs = new Inputs(); Inputs = new Inputs();
Logger.LogInfo("Initializing graphics device...");
GraphicsDevice = new GraphicsDevice( GraphicsDevice = new GraphicsDevice(
Backend.Vulkan, Backend.Vulkan,
debugMode debugMode
); );
Logger.LogInfo("Initializing main window...");
MainWindow = new Window(windowCreateInfo, GraphicsDevice.WindowFlags | SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN); MainWindow = new Window(windowCreateInfo, GraphicsDevice.WindowFlags | SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN);
if (!GraphicsDevice.ClaimWindow(MainWindow, windowCreateInfo.PresentMode)) if (!GraphicsDevice.ClaimWindow(MainWindow, windowCreateInfo.PresentMode))
@ -90,6 +94,7 @@ namespace MoonWorks
throw new System.SystemException("Could not claim window!"); throw new System.SystemException("Could not claim window!");
} }
Logger.LogInfo("Initializing audio thread...");
AudioDevice = new AudioDevice(); AudioDevice = new AudioDevice();
} }
@ -110,9 +115,6 @@ namespace MoonWorks
Logger.LogInfo("Cleaning up game..."); Logger.LogInfo("Cleaning up game...");
Destroy(); Destroy();
Logger.LogInfo("Closing audio thread...");
AudioDevice.Dispose();
Logger.LogInfo("Unclaiming window..."); Logger.LogInfo("Unclaiming window...");
GraphicsDevice.UnclaimWindow(MainWindow); GraphicsDevice.UnclaimWindow(MainWindow);
@ -122,6 +124,9 @@ namespace MoonWorks
Logger.LogInfo("Disposing graphics device..."); Logger.LogInfo("Disposing graphics device...");
GraphicsDevice.Dispose(); GraphicsDevice.Dispose();
Logger.LogInfo("Closing audio thread...");
AudioDevice.Dispose();
SDL.SDL_Quit(); SDL.SDL_Quit();
} }

View File

@ -5,45 +5,117 @@ using WellspringCS;
namespace MoonWorks.Graphics.Font 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) private byte* StringBytes;
{ private int StringBytesLength;
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();
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 (!IsDisposed)
{ {
if (disposing)
{
Texture.Dispose();
}
Wellspring.Wellspring_DestroyFont(Handle); Wellspring.Wellspring_DestroyFont(Handle);
IsDisposed = true;
} }
} base.Dispose(disposing);
~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);
} }
} }
} }

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 namespace MoonWorks.Graphics.Font
{ {
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public struct FontRange public struct Vertex : IVertexType
{
public uint FirstCodepoint;
public uint NumChars;
public byte OversampleH;
public byte OversampleV;
}
[StructLayout(LayoutKind.Sequential)]
public struct Vertex
{ {
public Vector3 Position; public Vector3 Position;
public Vector2 TexCoord; public Vector2 TexCoord;
public Color Color; 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;
using System.Runtime.InteropServices;
using WellspringCS; using WellspringCS;
namespace MoonWorks.Graphics.Font 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; } private GraphicsDevice GraphicsDevice { get; }
public IntPtr Handle { get; } public IntPtr Handle { get; }
public Buffer VertexBuffer { get; protected set; } = null; public Buffer VertexBuffer { get; protected set; } = null;
public Buffer IndexBuffer { get; protected set; } = null; public Buffer IndexBuffer { get; protected set; } = null;
public Texture Texture { get; protected set; }
public uint PrimitiveCount { 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(); 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); Wellspring.Wellspring_StartTextBatch(Handle, font.Handle);
Texture = packer.Texture; CurrentFont = font;
PrimitiveCount = 0; PrimitiveCount = 0;
} }
public unsafe void Draw( // Add text with size and color to the batch
public unsafe bool Add(
string text, string text,
float x, int pixelSize,
float y,
float depth,
Color color, Color color,
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left, HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment verticalAlignment = VerticalAlignment.Baseline VerticalAlignment verticalAlignment = VerticalAlignment.Baseline
) { ) {
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text); 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 (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, Handle,
x, pixelSize,
y,
depth,
new Wellspring.Color { R = color.R, G = color.G, B = color.B, A = color.A }, new Wellspring.Color { R = color.R, G = color.G, B = color.B, A = color.A },
(Wellspring.HorizontalAlignment) horizontalAlignment, (Wellspring.HorizontalAlignment) horizontalAlignment,
(Wellspring.VerticalAlignment) verticalAlignment, (Wellspring.VerticalAlignment) verticalAlignment,
(IntPtr) bytes, (IntPtr) StringBytes,
(uint) byteCount (uint) byteCount
); );
if (result == 0) 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) public unsafe void UploadBufferData(CommandBuffer commandBuffer)
{ {
Wellspring.Wellspring_GetBufferData( Wellspring.Wellspring_GetBufferData(
@ -81,24 +93,16 @@ namespace MoonWorks.Graphics.Font
out uint indexDataLengthInBytes out uint indexDataLengthInBytes
); );
if (VertexBuffer == null) if (VertexBuffer.Size < vertexDataLengthInBytes)
{
VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
}
else if (VertexBuffer.Size < vertexDataLengthInBytes)
{ {
VertexBuffer.Dispose(); VertexBuffer.Dispose();
VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes); VertexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
} }
if (IndexBuffer == null) if (IndexBuffer.Size < indexDataLengthInBytes)
{
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes);
}
else if (IndexBuffer.Size < indexDataLengthInBytes)
{ {
IndexBuffer.Dispose(); IndexBuffer.Dispose();
IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Index, indexDataLengthInBytes); IndexBuffer = new Buffer(GraphicsDevice, BufferUsageFlags.Vertex, vertexDataLengthInBytes);
} }
if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0) if (vertexDataLengthInBytes > 0 && indexDataLengthInBytes > 0)
@ -107,7 +111,41 @@ namespace MoonWorks.Graphics.Font
commandBuffer.SetBufferData(IndexBuffer, indexDataPointer, 0, indexDataLengthInBytes); 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;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using MoonWorks.Video;
using RefreshCS; using RefreshCS;
using WellspringCS;
namespace MoonWorks.Graphics namespace MoonWorks.Graphics
{ {
@ -21,6 +22,15 @@ namespace MoonWorks.Graphics
// Built-in video pipeline // Built-in video pipeline
internal GraphicsPipeline VideoPipeline { get; } 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; } public bool IsDisposed { get; private set; }
private readonly HashSet<GCHandle> resources = new HashSet<GCHandle>(); private readonly HashSet<GCHandle> resources = new HashSet<GCHandle>();
@ -41,14 +51,56 @@ namespace MoonWorks.Graphics
Conversions.BoolToByte(debugMode) Conversions.BoolToByte(debugMode)
); );
// Check for optional video shaders // TODO: check for CreateDevice fail
// Check for replacement stock shaders
string basePath = System.AppContext.BaseDirectory; string basePath = System.AppContext.BaseDirectory;
string videoVertPath = Path.Combine(basePath, "video_fullscreen.vert.refresh"); string videoVertPath = Path.Combine(basePath, "video_fullscreen.vert.refresh");
string videoFragPath = Path.Combine(basePath, "video_yuv2rgba.frag.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)) if (File.Exists(videoVertPath) && File.Exists(videoFragPath))
{ {
ShaderModule videoVertShader = new ShaderModule(this, videoVertPath); videoVertShader = new ShaderModule(this, videoVertPath);
ShaderModule videoFragShader = new ShaderModule(this, videoFragPath); 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( VideoPipeline = new GraphicsPipeline(
this, this,
@ -77,7 +129,13 @@ namespace MoonWorks.Graphics
MultisampleState = MultisampleState.None 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); FencePool = new FencePool(this);
} }
@ -363,6 +421,16 @@ namespace MoonWorks.Graphics
{ {
lock (resources) 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) foreach (var resource in resources)
{ {
if (resource.Target is IDisposable disposable) if (resource.Target is IDisposable disposable)

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
namespace MoonWorks.Graphics namespace MoonWorks.Graphics
{ {
@ -8,13 +7,11 @@ namespace MoonWorks.Graphics
public abstract class GraphicsResource : IDisposable public abstract class GraphicsResource : IDisposable
{ {
public GraphicsDevice Device { get; } 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; private GCHandle SelfReference;
public bool IsDisposed { get; private set; }
protected GraphicsResource(GraphicsDevice device) protected GraphicsResource(GraphicsDevice device)
{ {
Device = device; Device = device;
@ -23,7 +20,7 @@ namespace MoonWorks.Graphics
Device.AddResourceReference(SelfReference); Device.AddResourceReference(SelfReference);
} }
protected void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (!IsDisposed) if (!IsDisposed)
{ {
@ -33,13 +30,6 @@ namespace MoonWorks.Graphics
SelfReference.Free(); 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; 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> /// <summary>
/// Buffers are generic data containers that can be used by the GPU. /// Buffers are generic data containers that can be used by the GPU.
/// </summary> /// </summary>
public class Buffer : GraphicsResource public class Buffer : RefreshResource
{ {
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyBuffer; protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyBuffer;

View File

@ -6,7 +6,7 @@ namespace MoonWorks.Graphics
/// <summary> /// <summary>
/// Compute pipelines perform arbitrary parallel processing on input data. /// Compute pipelines perform arbitrary parallel processing on input data.
/// </summary> /// </summary>
public class ComputePipeline : GraphicsResource public class ComputePipeline : RefreshResource
{ {
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyComputePipeline; 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 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. /// The internal handle is replaced so that we can pool Fence objects to manage garbage.
/// </summary> /// </summary>
public class Fence : GraphicsResource public class Fence : RefreshResource
{ {
protected override Action<nint, nint> QueueDestroyFunction => Refresh.Refresh_ReleaseFence; 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/> /// Graphics pipelines encapsulate all of the render state in a single object. <br/>
/// These pipelines are bound before draw calls are issued. /// These pipelines are bound before draw calls are issued.
/// </summary> /// </summary>
public class GraphicsPipeline : GraphicsResource public class GraphicsPipeline : RefreshResource
{ {
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyGraphicsPipeline; protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyGraphicsPipeline;

View File

@ -6,7 +6,7 @@ namespace MoonWorks.Graphics
/// <summary> /// <summary>
/// A sampler specifies how a texture will be sampled in a shader. /// A sampler specifies how a texture will be sampled in a shader.
/// </summary> /// </summary>
public class Sampler : GraphicsResource public class Sampler : RefreshResource
{ {
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroySampler; protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroySampler;

View File

@ -8,7 +8,7 @@ namespace MoonWorks.Graphics
/// <summary> /// <summary>
/// Shader modules expect input in Refresh bytecode format. /// Shader modules expect input in Refresh bytecode format.
/// </summary> /// </summary>
public class ShaderModule : GraphicsResource public class ShaderModule : RefreshResource
{ {
protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyShaderModule; protected override Action<IntPtr, IntPtr> QueueDestroyFunction => Refresh.Refresh_QueueDestroyShaderModule;

View File

@ -8,7 +8,7 @@ namespace MoonWorks.Graphics
/// <summary> /// <summary>
/// A container for pixel data. /// A container for pixel data.
/// </summary> /// </summary>
public class Texture : GraphicsResource public class Texture : RefreshResource
{ {
public uint Width { get; internal set; } public uint Width { get; internal set; }
public uint Height { 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;
using System.IO; using System.IO;
using MoonWorks.Graphics;
namespace MoonWorks.Video namespace MoonWorks.Video
{ {
/// <summary> /// <summary>
/// This class takes in a filename for AV1 data in .obu (open bitstream unit) format /// This class takes in a filename for AV1 data in .obu (open bitstream unit) format
/// </summary> /// </summary>
public unsafe class VideoAV1 public unsafe class VideoAV1 : GraphicsResource
{ {
public string Filename { get; } public string Filename { get; }
@ -28,7 +29,7 @@ namespace MoonWorks.Video
/// <summary> /// <summary>
/// Opens an AV1 file so it can be loaded by VideoPlayer. You must also provide a playback framerate. /// Opens an AV1 file so it can be loaded by VideoPlayer. You must also provide a playback framerate.
/// </summary> /// </summary>
public VideoAV1(string filename, double framesPerSecond) public VideoAV1(GraphicsDevice device, string filename, double framesPerSecond) : base(device)
{ {
if (!File.Exists(filename)) if (!File.Exists(filename))
{ {
@ -67,8 +68,22 @@ namespace MoonWorks.Video
Filename = filename; Filename = filename;
StreamA = new VideoAV1Stream(this); StreamA = new VideoAV1Stream(device, this);
StreamB = new VideoAV1Stream(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 System;
using MoonWorks.Graphics;
namespace MoonWorks.Video namespace MoonWorks.Video
{ {
internal class VideoAV1Stream internal class VideoAV1Stream : GraphicsResource
{ {
public IntPtr Handle => handle; public IntPtr Handle => handle;
IntPtr handle; IntPtr handle;
@ -19,9 +20,7 @@ namespace MoonWorks.Video
public bool FrameDataUpdated { get; set; } public bool FrameDataUpdated { get; set; }
bool IsDisposed; public VideoAV1Stream(GraphicsDevice device, VideoAV1 video) : base(device)
public VideoAV1Stream(VideoAV1 video)
{ {
if (Dav1dfile.df_fopen(video.Filename, out handle) == 0) 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 (!IsDisposed)
{ {
if (disposing)
{
// dispose managed state (managed objects)
}
// free unmanaged resources (unmanaged objects)
Dav1dfile.df_close(Handle); Dav1dfile.df_close(Handle);
IsDisposed = true;
} }
} base.Dispose(disposing);
~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);
} }
} }
} }

View File

@ -8,7 +8,7 @@ namespace MoonWorks.Video
/// <summary> /// <summary>
/// A structure for continuous decoding of AV1 videos and rendering them into a texture. /// A structure for continuous decoding of AV1 videos and rendering them into a texture.
/// </summary> /// </summary>
public unsafe class VideoPlayer : IDisposable public unsafe class VideoPlayer : GraphicsResource
{ {
public Texture RenderTexture { get; private set; } = null; public Texture RenderTexture { get; private set; } = null;
public VideoState State { get; private set; } = VideoState.Stopped; public VideoState State { get; private set; } = VideoState.Stopped;
@ -18,6 +18,10 @@ namespace MoonWorks.Video
private VideoAV1 Video = null; private VideoAV1 Video = null;
private VideoAV1Stream CurrentStream = null; private VideoAV1Stream CurrentStream = null;
private Task ReadNextFrameTask;
private Task ResetStreamATask;
private Task ResetStreamBTask;
private GraphicsDevice GraphicsDevice; private GraphicsDevice GraphicsDevice;
private Texture yTexture = null; private Texture yTexture = null;
private Texture uTexture = null; private Texture uTexture = null;
@ -30,17 +34,11 @@ namespace MoonWorks.Video
private double lastTimestamp; private double lastTimestamp;
private double timeElapsed; private double timeElapsed;
private bool disposed; public VideoPlayer(GraphicsDevice device) : base(device)
public VideoPlayer(GraphicsDevice graphicsDevice)
{ {
GraphicsDevice = graphicsDevice; GraphicsDevice = device;
if (GraphicsDevice.VideoPipeline == null)
{
throw new InvalidOperationException("Missing video shaders!");
}
LinearSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp); LinearSampler = new Sampler(device, SamplerCreateInfo.LinearClamp);
timer = new Stopwatch(); timer = new Stopwatch();
} }
@ -168,6 +166,8 @@ namespace MoonWorks.Video
public void Unload() public void Unload()
{ {
Stop(); Stop();
ResetStreamATask?.Wait();
ResetStreamBTask?.Wait();
Video = null; Video = null;
} }
@ -194,7 +194,8 @@ namespace MoonWorks.Video
} }
currentFrame = thisFrame; currentFrame = thisFrame;
Task.Run(CurrentStream.ReadNextFrame).ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); ReadNextFrameTask = Task.Run(CurrentStream.ReadNextFrame);
ReadNextFrameTask.ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
} }
if (CurrentStream.Ended) if (CurrentStream.Ended)
@ -202,7 +203,17 @@ namespace MoonWorks.Video
timer.Stop(); timer.Stop();
timer.Reset(); 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) if (Loop)
{ {
@ -280,46 +291,40 @@ namespace MoonWorks.Video
private void InitializeDav1dStream() private void InitializeDav1dStream()
{ {
Task.Run(Video.StreamA.Reset).ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted); ReadNextFrameTask?.Wait();
Task.Run(Video.StreamB.Reset).ContinueWith(HandleTaskException, TaskContinuationOptions.OnlyOnFaulted);
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; CurrentStream = Video.StreamA;
currentFrame = -1; currentFrame = -1;
} }
private static void HandleTaskException(Task task) private static void HandleTaskException(Task task)
{
if (task.Exception.InnerException is not TaskCanceledException)
{ {
throw task.Exception; throw task.Exception;
} }
}
protected virtual void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (!disposed) if (!IsDisposed)
{ {
if (disposing) if (disposing)
{ {
// dispose managed state (managed objects) Unload();
RenderTexture.Dispose();
yTexture.Dispose();
uTexture.Dispose();
vTexture.Dispose();
}
disposed = true; RenderTexture?.Dispose();
yTexture?.Dispose();
uTexture?.Dispose();
vTexture?.Dispose();
} }
} }
base.Dispose(disposing);
~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);
} }
} }
} }