diff --git a/src/Graphics/Font/MSDF/Decoder.cs b/src/Graphics/Font/MSDF/Decoder.cs new file mode 100644 index 0000000..effae24 --- /dev/null +++ b/src/Graphics/Font/MSDF/Decoder.cs @@ -0,0 +1,43 @@ +using System.Runtime.CompilerServices; + +namespace MoonWorks.Graphics.Font +{ + /* UTF-8 Decoder */ + + /* Copyright (c) 2008-2009 Bjoern Hoehrmann + * See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. + */ + + public static class Decoder + { + static byte[] utf8d = new byte[] { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df + 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef + 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff + 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0 + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2 + 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4 + 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6 + 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8 + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe uint Decode(uint* state, uint* codep, uint dbyte) { + uint type = utf8d[dbyte]; + + *codep = (uint) ((*state != 0) ? + (dbyte & 0x3fu) | (*codep << 6) : + (0xff >> (int) type) & (dbyte)); + + *state = utf8d[256 + *state*16 + type]; + return *state; + } + + } +} diff --git a/src/Graphics/Font/MSDF/Font.cs b/src/Graphics/Font/MSDF/Font.cs new file mode 100644 index 0000000..c0ea1c8 --- /dev/null +++ b/src/Graphics/Font/MSDF/Font.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace MoonWorks.Graphics.Font.MSDF; + +public class Font : IDisposable +{ + public Texture Texture { get; } + internal AtlasData AtlasData; + + static JsonSerializerOptions SerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + IncludeFields = true + }; + static AtlasDataContext Context = new AtlasDataContext(SerializerOptions); + + private bool IsDisposed; + + public static Font Load( + GraphicsDevice graphicsDevice, + CommandBuffer commandBuffer, + string jsonPath + ) { + var atlasData = (AtlasData) JsonSerializer.Deserialize(File.ReadAllText(jsonPath), typeof(AtlasData), Context); + var imagePath = Path.ChangeExtension(jsonPath, ".png"); + var texture = Texture.FromImageFile(graphicsDevice, commandBuffer, imagePath); + + return new Font(texture, atlasData); + } + + private Font(Texture texture, AtlasData atlasData) + { + Texture = texture; + AtlasData = atlasData; + } + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + Texture.Dispose(); + } + + IsDisposed = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/Graphics/Font/MSDF/Structs.cs b/src/Graphics/Font/MSDF/Structs.cs new file mode 100644 index 0000000..cedfc41 --- /dev/null +++ b/src/Graphics/Font/MSDF/Structs.cs @@ -0,0 +1,85 @@ +using System.Text.Json.Serialization; +using MoonWorks.Math.Float; + +namespace MoonWorks.Graphics.Font.MSDF; + +[JsonSerializable(typeof(AtlasData))] +internal partial class AtlasDataContext : JsonSerializerContext +{ +} + +// Reads from an atlas generated by msdf-atlas-gen +public struct AtlasData +{ + public Atlas Atlas; + public Metrics Metrics; + public Glyph[] Glyphs; +} + +public struct Atlas +{ + public FieldType Type; + public int DistanceRange; + public int Size; + public int Width; + public int Height; + public Origin YOrigin; +} + +public struct Metrics +{ + public int EmSize; + public float LineHeight; + public float Ascender; + public float Descender; + public float UnderlineY; + public float UnderlineThickness; +} + +public struct Glyph +{ + public uint Unicode; + public float Advance; + public Bounds PlaneBounds; + public Bounds AtlasBounds; +} + +public struct Bounds +{ + public float Left; + public float Bottom; + public float Right; + public float Top; +} + +public enum FieldType +{ + Hardmask, + Softmask, + SDF, + PSDF, + MSDF, + MTSDF +} + +public enum Origin +{ + Top, + Bottom +} + +public struct FontVertex : IVertexType +{ + public Vector3 Position; + public Vector2 TexCoord; + public Color Color; + + private static readonly VertexElementFormat[] vertexElementFormats = new VertexElementFormat[] + { + VertexElementFormat.Vector3, + VertexElementFormat.Vector2, + VertexElementFormat.Color + }; + + public static VertexElementFormat[] Formats => vertexElementFormats; +} diff --git a/src/Graphics/Font/MSDF/TextBatch.cs b/src/Graphics/Font/MSDF/TextBatch.cs new file mode 100644 index 0000000..7e4f285 --- /dev/null +++ b/src/Graphics/Font/MSDF/TextBatch.cs @@ -0,0 +1,188 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace MoonWorks.Graphics.Font.MSDF; + +public unsafe class TextBatch : IDisposable +{ + public const int MAX_CHARS = 4096; + public const int MAX_VERTICES = MAX_CHARS * 4; + public const int MAX_INDICES = MAX_CHARS * 6; + + public GraphicsDevice GraphicsDevice; + public Font Font; + + public Buffer VertexBuffer; + public Buffer IndexBuffer; + + public byte* VertexData; + public byte* IndexData; + + private int CurrentVertexDataLengthInBytes; + private int CurrentIndexDataLengthInBytes; + + private byte* StringBytes; + private int StringBytesCount = 16; + + private bool IsDisposed; + + public TextBatch(GraphicsDevice graphicsDevice) + { + GraphicsDevice = graphicsDevice; + } + + public void Start() + { + VertexBuffer = Buffer.Create(GraphicsDevice, BufferUsageFlags.Vertex, MAX_VERTICES); + IndexBuffer = Buffer.Create(GraphicsDevice, BufferUsageFlags.Index, MAX_INDICES); + VertexData = (byte*) NativeMemory.Alloc((nuint) (MAX_VERTICES * Unsafe.SizeOf())); + IndexData = (byte*) NativeMemory.Alloc((nuint) (MAX_INDICES * Unsafe.SizeOf())); + StringBytes = (byte*) NativeMemory.Alloc(128); + } + + public void AddText( + string text, + float x, + float y, + float depth, + Color color, + HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment verticalAlignment = VerticalAlignment.Baseline + ) { + uint decodeState; + uint codepoint; + + var byteCount = System.Text.Encoding.UTF8.GetByteCount(text); + + if (StringBytesCount < byteCount) + { + StringBytes = (byte*) NativeMemory.Realloc(StringBytes, (nuint) byteCount); + StringBytesCount = byteCount; + } + + fixed (char* chars = text) + { + var bytesWritten = System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount); + + y += GetVerticalAlignOffset(verticalAlignment); + + if (horizontalAlignment == HorizontalAlignment.Right) + { + // TODO: check text bounds to adjust alignment + } + + for (var i = 0; i < bytesWritten; i += 1) + { + if (Decoder.Decode(&decodeState, &codepoint, StringBytes[i]) > 0) + { + if (decodeState == 1) + { + // Something went wrong! + return; + } + + continue; + } + + // TODO: we need to convert AtlasData so that codepoints are looked up by key + if (IsWhitespace(codepoint)) + { + x += GetHorizontalAdvance(codepoint); + continue; + } + } + + // TODO: draw the rest of the owl + } + } + + // Call this after you have made all the Draw calls you want. + public void UploadBufferData(CommandBuffer commandBuffer) + { + if (CurrentVertexDataLengthInBytes > 0 && CurrentIndexDataLengthInBytes > 0) + { + commandBuffer.SetBufferData(VertexBuffer, (nint) VertexData, 0, (uint) CurrentVertexDataLengthInBytes); + commandBuffer.SetBufferData(IndexBuffer, (nint) IndexData, 0, (uint) CurrentIndexDataLengthInBytes); + } + } + + private float GetHorizontalAdvance(uint codepoint) + { + Font.AtlasData.Glyphs + } + + private float GetVerticalAlignOffset(VerticalAlignment verticalAlignment) + { + switch (verticalAlignment) + { + case VerticalAlignment.Baseline: + return 0; + + case VerticalAlignment.Top: + return Font.AtlasData.Metrics.Ascender; + + case VerticalAlignment.Middle: + return (Font.AtlasData.Metrics.Ascender + Font.AtlasData.Metrics.Descender) / 2f; + + case VerticalAlignment.Bottom: + return Font.AtlasData.Metrics.Descender; + + default: + return 0; + } + } + + private static bool IsWhitespace(uint codepoint) + { + switch (codepoint) + { + case 0x0020: + case 0x00A0: + case 0x1680: + case 0x202F: + case 0x205F: + case 0x3000: + return true; + + default: + if (codepoint > 0x2000 && codepoint <= 0x200A) + { + return true; + } + + return false; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + VertexBuffer.Dispose(); + IndexBuffer.Dispose(); + } + + NativeMemory.Free(VertexData); + NativeMemory.Free(IndexData); + NativeMemory.Free(StringBytes); + + IsDisposed = true; + } + } + + ~TextBatch() + { + // 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); + } +}