forked from MoonsideGames/MoonWorks
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
parent
2e890fd696
commit
4dbd5a2cbe
|
@ -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>
|
||||||
|
|
|
@ -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
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
src/Game.cs
15
src/Game.cs
|
@ -1,5 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using SDL2;
|
||||||
using SDL2;
|
|
||||||
using MoonWorks.Audio;
|
using MoonWorks.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,103 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using WellspringCS;
|
|
||||||
|
|
||||||
namespace MoonWorks.Graphics.Font
|
|
||||||
{
|
|
||||||
public class Packer : IDisposable
|
|
||||||
{
|
|
||||||
public IntPtr Handle { get; }
|
|
||||||
public Texture Texture { get; }
|
|
||||||
|
|
||||||
public Font Font { get; }
|
|
||||||
|
|
||||||
private byte[] StringBytes;
|
|
||||||
|
|
||||||
private bool IsDisposed;
|
|
||||||
|
|
||||||
public unsafe Packer(GraphicsDevice graphicsDevice, Font font, float fontSize, uint textureWidth, uint textureHeight, uint padding = 1)
|
|
||||||
{
|
|
||||||
Font = font;
|
|
||||||
Handle = Wellspring.Wellspring_CreatePacker(Font.Handle, fontSize, textureWidth, textureHeight, 0, padding);
|
|
||||||
Texture = Texture.CreateTexture2D(graphicsDevice, textureWidth, textureHeight, TextureFormat.R8, TextureUsageFlags.Sampler);
|
|
||||||
StringBytes = new byte[128];
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe bool PackFontRanges(params FontRange[] fontRanges)
|
|
||||||
{
|
|
||||||
fixed (FontRange *pFontRanges = &fontRanges[0])
|
|
||||||
{
|
|
||||||
var nativeSize = fontRanges.Length * Marshal.SizeOf<Wellspring.FontRange>();
|
|
||||||
var result = Wellspring.Wellspring_PackFontRanges(Handle, (IntPtr) pFontRanges, (uint) fontRanges.Length);
|
|
||||||
return result > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void SetTextureData(CommandBuffer commandBuffer)
|
|
||||||
{
|
|
||||||
var pixelDataPointer = Wellspring.Wellspring_GetPixelDataPointer(Handle);
|
|
||||||
commandBuffer.SetTextureData(Texture, pixelDataPointer, Texture.Width * Texture.Height);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void TextBounds(
|
|
||||||
string text,
|
|
||||||
float x,
|
|
||||||
float y,
|
|
||||||
HorizontalAlignment horizontalAlignment,
|
|
||||||
VerticalAlignment verticalAlignment,
|
|
||||||
out Wellspring.Rectangle rectangle
|
|
||||||
) {
|
|
||||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
|
|
||||||
|
|
||||||
if (StringBytes.Length < byteCount)
|
|
||||||
{
|
|
||||||
System.Array.Resize(ref StringBytes, byteCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
fixed (char* chars = text)
|
|
||||||
fixed (byte* bytes = StringBytes)
|
|
||||||
{
|
|
||||||
System.Text.Encoding.UTF8.GetBytes(chars, text.Length, bytes, byteCount);
|
|
||||||
Wellspring.Wellspring_TextBounds(
|
|
||||||
Handle,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
(Wellspring.HorizontalAlignment) horizontalAlignment,
|
|
||||||
(Wellspring.VerticalAlignment) verticalAlignment,
|
|
||||||
(IntPtr) bytes,
|
|
||||||
(uint) byteCount,
|
|
||||||
out rectangle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!IsDisposed)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
Texture.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Wellspring.Wellspring_DestroyPacker(Handle);
|
|
||||||
|
|
||||||
IsDisposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~Packer()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,19 +4,17 @@ using MoonWorks.Math.Float;
|
||||||
namespace MoonWorks.Graphics.Font
|
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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace MoonWorks.Graphics;
|
||||||
|
|
||||||
|
public abstract class RefreshResource : GraphicsResource
|
||||||
|
{
|
||||||
|
public IntPtr Handle { get => handle; internal set => handle = value; }
|
||||||
|
private IntPtr handle;
|
||||||
|
|
||||||
|
protected abstract Action<IntPtr, IntPtr> QueueDestroyFunction { get; }
|
||||||
|
|
||||||
|
protected RefreshResource(GraphicsDevice device) : base(device)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!IsDisposed)
|
||||||
|
{
|
||||||
|
// Atomically call destroy function in case this is called from the finalizer thread
|
||||||
|
var toDispose = Interlocked.Exchange(ref handle, IntPtr.Zero);
|
||||||
|
if (toDispose != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
QueueDestroyFunction(Device.Handle, toDispose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ namespace MoonWorks.Graphics
|
||||||
/// <summary>
|
/// <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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,34 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(set = 1, binding = 0) uniform sampler2D msdf;
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 inTexCoord;
|
||||||
|
layout(location = 1) in vec4 inColor;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
layout(binding = 0, set = 3) uniform UBO
|
||||||
|
{
|
||||||
|
float pxRange;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
float median(float r, float g, float b)
|
||||||
|
{
|
||||||
|
return max(min(r, g), min(max(r, g), b));
|
||||||
|
}
|
||||||
|
|
||||||
|
float screenPxRange()
|
||||||
|
{
|
||||||
|
vec2 unitRange = vec2(ubo.pxRange)/vec2(textureSize(msdf, 0));
|
||||||
|
vec2 screenTexSize = vec2(1.0)/fwidth(inTexCoord);
|
||||||
|
return max(0.5*dot(unitRange, screenTexSize), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec3 msd = texture(msdf, inTexCoord).rgb;
|
||||||
|
float sd = median(msd.r, msd.g, msd.b);
|
||||||
|
float screenPxDistance = screenPxRange() * (sd - 0.5);
|
||||||
|
float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
|
||||||
|
outColor = mix(vec4(0.0, 0.0, 0.0, 0.0), inColor, opacity);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 inPos;
|
||||||
|
layout(location = 1) in vec2 inTexCoord;
|
||||||
|
layout(location = 2) in vec4 inColor;
|
||||||
|
|
||||||
|
layout(location = 0) out vec2 outTexCoord;
|
||||||
|
layout(location = 1) out vec4 outColor;
|
||||||
|
|
||||||
|
layout(binding = 0, set = 2) uniform UBO
|
||||||
|
{
|
||||||
|
mat4 ViewProjection;
|
||||||
|
} ubo;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = ubo.ViewProjection * vec4(inPos, 1.0);
|
||||||
|
outTexCoord = inTexCoord;
|
||||||
|
outColor = inColor;
|
||||||
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue