From bd69c0d93f75ea6fbcdf48bbb80810daee1377fa Mon Sep 17 00:00:00 2001 From: "H. Utku Maden" Date: Wed, 15 May 2024 23:17:01 +0300 Subject: [PATCH] Preliminary typesetting work. --- Quik.Media.Defaults/QFontFreeType.cs | 79 ++++------ Quik.Media.Defaults/StbMediaLoader.cs | 10 +- Quik.Media.Defaults/Win32/EnumerateFonts.cs | 151 +++++++++++++++++++ Quik.OpenTK/OpenTKPlatform.cs | 13 ++ Quik.OpenTK/OpenTKPort.cs | 1 + Quik/Media/Color/ImageBuffer.cs | 46 ++---- Quik/Media/Font/FontAtlas.cs | 45 +++++- Quik/Media/QFont.cs | 133 ++++++++++------- Quik/Media/QImage.cs | 2 +- Quik/OpenGL/GL21Driver.cs | 3 +- Quik/OpenGL/GLEnum.cs | 3 + Quik/PAL/IQuikPlatform.cs | 2 + Quik/Typography/FontProvider.cs | 71 +++++++++ Quik/Typography/TextLayout.cs | 6 +- Quik/Typography/Typesetter.cs | 155 ++++++++++++++++++++ Quik/VertexGenerator/VertexCommandEngine.cs | 2 +- Quik/res/gl21.frag | 18 +-- tests/QuikDemo/Program.cs | 24 ++- 18 files changed, 597 insertions(+), 167 deletions(-) create mode 100644 Quik.Media.Defaults/Win32/EnumerateFonts.cs create mode 100644 Quik/Typography/FontProvider.cs create mode 100644 Quik/Typography/Typesetter.cs diff --git a/Quik.Media.Defaults/QFontFreeType.cs b/Quik.Media.Defaults/QFontFreeType.cs index a92dbab..7b72701 100644 --- a/Quik.Media.Defaults/QFontFreeType.cs +++ b/Quik.Media.Defaults/QFontFreeType.cs @@ -1,8 +1,9 @@ using System; +using System.Buffers; using System.IO; using Quik.FreeType; -using Quik.Media; using Quik.Media.Color; +using Quik.Media.Font; namespace Quik.Media.Defaults { @@ -11,7 +12,7 @@ namespace Quik.Media.Defaults private MemoryStream ms; private FTFace face; - public override FontInfo Info => throw new NotImplementedException(); + public override FontFace Face => throw new NotImplementedException(); public QFontFreeType(Stream stream) { @@ -30,65 +31,39 @@ namespace Quik.Media.Defaults return FT.GetCharIndex(face, (ulong)rune) != 0; } - public override QFontPage RasterizePage(int codepage, float size, in FontRasterizerOptions options) + protected override QImage Render(out QGlyphMetrics metrics, int codepoint, float size, in FontRasterizerOptions options) { FT.SetCharSize(face, 0, (long)Math.Round(64*size), 0, (uint)Math.Round(options.Resolution)); - QGlyphMetrics[] allMetrics = new QGlyphMetrics[256]; - // Figure out the map size needed. - int pixels = 0; - for (int i = 0; i < 256; i++) + uint index = FT.GetCharIndex(face, (ulong)codepoint); + FT.LoadGlyph(face, index, FTLoadFlags.Default); + + ref readonly FTGlyphMetrics ftmetrics = ref face.Glyph.Metrics; + metrics = new QGlyphMetrics(codepoint, + new QVec2(ftmetrics.Width/64f, ftmetrics.Height/64f), + new QVec2(ftmetrics.HorizontalBearingX/64f, ftmetrics.HorizontalBearingY/64f), + new QVec2(ftmetrics.VerticalBearingX/64f, ftmetrics.VerticalBearingY/64f), + new QVec2(ftmetrics.HorizontalAdvance/64f, ftmetrics.VerticalAdvance/64f) + ); + + FT.RenderGlyph(face.Glyph, options.Sdf ? FTRenderMode.Sdf : FTRenderMode.Normal); + ref readonly FTBitmap bitmap = ref face.Glyph.Bitmap; + + if (bitmap.Width == 0 || bitmap.Pitch == 0 || bitmap.Buffer == IntPtr.Zero) { - uint index = FT.GetCharIndex(face, (ulong)(codepage + i)); - FT.LoadGlyph(face, index, FTLoadFlags.Default); - - ref readonly FTGlyphMetrics metrics = ref face.Glyph.Metrics; - allMetrics[i] = new QGlyphMetrics( - codepage + i, - new QVec2(metrics.Width, metrics.Height), - new QVec2(metrics.HorizontalBearingX/64f, metrics.HorizontalBearingY/64f), - new QVec2(metrics.VerticalBearingX/64f, metrics.VerticalBearingY/64f), - new QVec2(metrics.HorizontalAdvance/64f, metrics.VerticalAdvance/64f) - ); - pixels = (int)Math.Max(pixels, Math.Max(face.Glyph.Metrics.Width/64f, face.Glyph.Metrics.Height/64f)); + return null; } - int bits = Math.ILogB(pixels); - if (1 << bits != pixels) + QImageBuffer image = new QImageBuffer(QImageFormat.RedU8, (int)bitmap.Width, (int)bitmap.Rows); + image.LockBits2d(out QImageLock lk, QImageLockOptions.Default); + + unsafe { - pixels = 1 << bits + 1; + Buffer.MemoryCopy((void*)bitmap.Buffer, (void*)lk.ImagePtr, lk.Width * lk.Height, bitmap.Width * bitmap.Rows); } - // Now we can create a bitmap and render our glyphs. - - QImageBuffer buffer = new QImageBuffer(QImageFormat.RedU8, (int)pixels, (int)pixels, 256); - - for (int i = 0; i < 256; i++) - { - uint index = FT.GetCharIndex(face, (ulong)(codepage + i)); - FT.LoadGlyph(face, index, FTLoadFlags.Default); - FT.RenderGlyph(face.Glyph, options.Sdf ? FTRenderMode.Sdf : FTRenderMode.Mono); - - ref readonly FTBitmap bitmap = ref face.Glyph.Bitmap; - - buffer.LockBits3d(out QImageLock dst, QImageLockOptions.Default, i); - - if (bitmap.Buffer != IntPtr.Zero) unsafe - { - for (int j = 0; j < bitmap.Rows; j++) - { - Buffer.MemoryCopy( - (byte*)bitmap.Buffer + (j * bitmap.Pitch), - (byte*)dst.ImagePtr + (j * dst.Width), - dst.Width, - bitmap.Width); - } - } - - buffer.UnlockBits(); - } - - return new QFontPage(this, codepage, size, options, buffer, allMetrics); + image.UnlockBits(); + return image; } protected override void Dispose(bool disposing) diff --git a/Quik.Media.Defaults/StbMediaLoader.cs b/Quik.Media.Defaults/StbMediaLoader.cs index 9fb4ea1..4c6d471 100644 --- a/Quik.Media.Defaults/StbMediaLoader.cs +++ b/Quik.Media.Defaults/StbMediaLoader.cs @@ -4,14 +4,14 @@ using System.Collections; using System.IO; using System.Linq; using System.Net; -using Quik.Media; +using Quik.Media.Font; // WebRequest is obsolete but runs on .NET framework. #pragma warning disable SYSLIB0014 namespace Quik.Media.Defaults { - public class StbMediaLoader : MediaLoader, MediaLoader, MediaLoader, MediaLoader + public class StbMediaLoader : MediaLoader, MediaLoader, MediaLoader, MediaLoader { public bool AllowRemoteTransfers { get; set; } = false; private readonly ArrayPool ByteArrays = ArrayPool.Create(); @@ -31,9 +31,9 @@ namespace Quik.Media.Defaults { return GetMedia((FileInfo)key, hint); } - else if (t == typeof(FontInfo)) + else if (t == typeof(FontFace)) { - return GetMedia((FontInfo)key, hint); + return GetMedia((FontFace)key, hint); } else { @@ -56,7 +56,7 @@ namespace Quik.Media.Defaults throw new NotImplementedException(); } - public IDisposable GetMedia(FontInfo key, MediaHint hint) + public IDisposable GetMedia(FontFace key, MediaHint hint) { throw new NotImplementedException(); } diff --git a/Quik.Media.Defaults/Win32/EnumerateFonts.cs b/Quik.Media.Defaults/Win32/EnumerateFonts.cs new file mode 100644 index 0000000..488e71f --- /dev/null +++ b/Quik.Media.Defaults/Win32/EnumerateFonts.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +#if false +namespace Quik.Media.Defaults.Win32 +{ + + public class EnumerateFonts + { + private const byte DEFAULT_CHARSET = 1; + + public static void Enumerate(FontFace font) + { + /* It's windows, just borrow the desktop window. */ + IntPtr hdc = GetDC(GetDesktopWindow()); + + List<(LogFontA, TextMetricA)> list = new List<(LogFontA, TextMetricA)>(); + + LogFontA font2 = new LogFontA() + { + //FaceName = font.Family, + Weight = ((font.Style & FontStyle.Bold) != 0) ? FontWeight.Bold : FontWeight.Regular, + Italic = (font.Style & FontStyle.Italic) != 0, + CharSet = DEFAULT_CHARSET + }; + + Console.WriteLine(font2.FaceName); + + EnumFontFamiliesExProc proc = (in LogFontA font, in TextMetricA metric, int type, IntPtr lparam) => + { + list.Add((font, metric)); + return 0; + }; + + EnumFontFamiliesExA(hdc, font2, proc, IntPtr.Zero, 0); + } + + private const string gdi32 = "Gdi32.dll"; + private const string user32 = "User32.dll"; + + [DllImport(gdi32)] + private static extern int EnumFontFamiliesExA( + IntPtr hdc, + in LogFontA font, + [MarshalAs(UnmanagedType.FunctionPtr)] EnumFontFamiliesExProc proc, + IntPtr lparam, + int flags /* Should be zero. */); + + [DllImport(user32)] + private static extern IntPtr /* HWND */ GetDesktopWindow(); + + [DllImport(user32)] + private static extern IntPtr /* HDC */ GetDC(IntPtr hwnd); + + private delegate int EnumFontFamiliesExProc(in LogFontA font, in TextMetricA metric, int fontType, IntPtr lParam); + + private struct LogFontA + { + public long Height; + public long Width; + public long Escapement; + public long Orientation; + public FontWeight Weight; + [MarshalAs(UnmanagedType.U1)] + public bool Italic; + [MarshalAs(UnmanagedType.U1)] + public bool Underline; + [MarshalAs(UnmanagedType.U1)] + public bool StrikeOut; + public byte CharSet; + public byte OutPrecision; + public byte ClipPrecision; + public byte PitchAndFamily; + private unsafe fixed byte aFaceName[32]; + public unsafe string FaceName + { + get + { + fixed (byte* str = aFaceName) + { + int len = 0; + for (; str[len] != 0 && len < 32; len++) ; + return Encoding.UTF8.GetString(str, len); + } + } + + set + { + fixed (byte *str = aFaceName) + { + Span span = new Span(str, 32); + Encoding.UTF8.GetBytes(value, span); + span[31] = 0; + } + } + } + } + + private struct TextMetricA + { + public long Height; + public long Ascent; + public long Descent; + public long InternalLeading; + public long ExternalLeading; + public long AveCharWidth; + public long MaxCharWidth; + public long Weight; + public long Overhang; + public long DigitizedAspectX; + public long DigitizedAspectY; + public byte FirstChar; + public byte LastChar; + public byte DefaultChar; + public byte BreakChar; + public byte Italic; + public byte Underlined; + public byte StruckOut; + public byte PitchAndFamily; + public byte CharSet; + } + + private enum FontWeight : long + { + DontCare = 0, + Thin = 100, + ExtraLight = 200, + UltraLight = 200, + Light = 300, + Normal = 400, + Regular = 400, + Medium = 500, + Semibold = 600, + Demibold = 600, + Bold = 700, + Extrabold = 800, + Ultrabold = 800, + Heavy = 900, + Black = 900 + } + } + + +} +#endif diff --git a/Quik.OpenTK/OpenTKPlatform.cs b/Quik.OpenTK/OpenTKPlatform.cs index 30180e6..c75150b 100644 --- a/Quik.OpenTK/OpenTKPlatform.cs +++ b/Quik.OpenTK/OpenTKPlatform.cs @@ -87,5 +87,18 @@ namespace Quik.OpenTK public void PortShow(IQuikPortHandle port, bool shown = true) => ((OpenTKPort)port).Show(shown); public void PortPaint(IQuikPortHandle port, CommandList commands) => ((OpenTKPort)port).Paint(commands); + + public void GetMaximumImage(out int width, out int height) + { + GL.Get(GLEnum.GL_MAX_TEXTURE_SIZE, out int value); + width = height = value; + } + + public void GetMaximumImage(out int width, out int height, out int depth) + { + GetMaximumImage(out width, out height); + GL.Get(GLEnum.GL_MAX_ARRAY_TEXTURE_LAYERS, out int value); + depth = value; + } } } \ No newline at end of file diff --git a/Quik.OpenTK/OpenTKPort.cs b/Quik.OpenTK/OpenTKPort.cs index df6dcea..f0206b1 100644 --- a/Quik.OpenTK/OpenTKPort.cs +++ b/Quik.OpenTK/OpenTKPort.cs @@ -78,6 +78,7 @@ namespace Quik.OpenTK if (!_glDriver.IsInit) _glDriver.Init(); + GL.Clear(GLEnum.GL_COLOR_BUFFER_BIT); _glDriver.Draw(_vertexEngine.DrawQueue, view); _window.Context.SwapBuffers(); diff --git a/Quik/Media/Color/ImageBuffer.cs b/Quik/Media/Color/ImageBuffer.cs index 44b861d..428f153 100644 --- a/Quik/Media/Color/ImageBuffer.cs +++ b/Quik/Media/Color/ImageBuffer.cs @@ -1,17 +1,19 @@ using System; using System.Runtime.InteropServices; -namespace Quik.Media +namespace Quik.Media.Color { public class QImageBuffer : QImage { private byte[] buffer; GCHandle handle; + private bool isSdf = false; public override QImageFormat InternalFormat { get; } public override int Width { get; } public override int Height { get; } public override int Depth { get; } + public override bool IsSdf => isSdf; public QImageBuffer(QImageFormat format, int width, int height, int depth = 1) { @@ -20,7 +22,7 @@ namespace Quik.Media Height = height; Depth = depth; - buffer = new byte[width * height * depth * format.BytesPerPixel()]; + buffer = new byte[width * height * depth]; } ~QImageBuffer() { @@ -29,7 +31,7 @@ namespace Quik.Media private QImageLock Lock() { - handle.Free(); + if (handle.IsAllocated) handle.Free(); handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); return new QImageLock(InternalFormat, Width, Height, Depth, ptr); @@ -37,56 +39,32 @@ namespace Quik.Media protected override void Dispose(bool disposing) { - if (handle.IsAllocated) handle.Free(); buffer = null; + if (handle.IsAllocated) handle.Free(); GC.SuppressFinalize(this); } public override void LockBits2d(out QImageLock imageLock, QImageLockOptions options) { - if (options.Format != options.Format) throw new InvalidOperationException("This image type cannot be converted."); - if (Depth > 1) throw new InvalidOperationException("This texture has a depth component."); - - UnlockBits(); - - handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); - imageLock = new QImageLock(InternalFormat, Width, Height, Depth, ptr); + imageLock = Lock(); } public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options) { - if (options.Format != options.Format) throw new InvalidOperationException("This image type cannot be converted."); - UnlockBits(); - - handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); - imageLock = new QImageLock(InternalFormat, Width, Height, Depth, ptr); + imageLock = Lock(); } public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options, int depth) { - if (options.Format != options.Format) throw new InvalidOperationException("This image type cannot be converted."); - if (depth < 0 || depth > Depth) - throw new ArgumentOutOfRangeException(nameof(depth), "Depth must be in range."); - - UnlockBits(); - - handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); - imageLock = new QImageLock( - InternalFormat, - Width, - Height, - 1, - ptr + (depth * Width * Height * InternalFormat.BytesPerPixel())); + imageLock = Lock(); } public override void UnlockBits() { - if (handle.IsAllocated) - handle.Free(); + handle.Free(); } + + public void SetSdf(bool value = true) => isSdf = value; } } \ No newline at end of file diff --git a/Quik/Media/Font/FontAtlas.cs b/Quik/Media/Font/FontAtlas.cs index 1900748..6e8edb1 100644 --- a/Quik/Media/Font/FontAtlas.cs +++ b/Quik/Media/Font/FontAtlas.cs @@ -1,5 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Quik.Media.Color; namespace Quik.Media.Font { @@ -17,11 +21,28 @@ namespace Quik.Media.Font private readonly Dictionary glyphs = new Dictionary(); private int index = 0; private AtlasPage last = null; + private bool isSdf = false; + private int expansion; - public FontAtlas(int width, int height) + public bool IsSdf + { + get => isSdf; + set + { + foreach (AtlasPage page in atlases) + { + (page.Image as QImageBuffer).SetSdf(value); + } + isSdf = value; + } + } + + public FontAtlas(int width, int height, bool isSdf, int expansion = 4) { this.width = width; this.height = height; + IsSdf = isSdf; + this.expansion = expansion; } public bool GetGlyph(int codepoint, out FontAtlasGlyphInfo info) @@ -33,7 +54,7 @@ namespace Quik.Media.Font { info = new FontAtlasGlyphInfo() { Codepoint = codepoint }; - if (last == null || last.IsFull) + if (last == null || !last.WouldFit(source)) { AddPage(); } @@ -51,7 +72,8 @@ namespace Quik.Media.Font } else { - last = new AtlasPage(width, height); + last = new AtlasPage(width, height, expansion); + (last.Image as QImageBuffer).SetSdf(IsSdf); atlases.Add(last); } } @@ -92,12 +114,14 @@ namespace Quik.Media.Font public QImage Image; public int PointerX, PointerY; public int RowHeight; + public int Expansion; public bool IsFull => PointerX > Image.Width || PointerY > Image.Height; - public AtlasPage(int width, int height) + public AtlasPage(int width, int height, int expansion) { Image = new QImageBuffer(QImageFormat.RedU8, width, height); + Expansion = expansion; Reset(); } @@ -110,8 +134,8 @@ namespace Quik.Media.Font src.CopyTo(dst, PointerX, PointerY); Image.UnlockBits(); - QVec2 min = new QVec2(PointerX/Image.Width, PointerY/Image.Width); - QVec2 size = new QVec2(src.Width/Image.Width, src.Height/Image.Height); + QVec2 min = new QVec2((float)PointerX/Image.Width, (float)PointerY/Image.Height); + QVec2 size = new QVec2((float)src.Width/Image.Width, (float)src.Height/Image.Height); prototype.Image = Image; prototype.UVs = new QRectangle(min + size, min); @@ -127,7 +151,7 @@ namespace Quik.Media.Font public void AdvanceRow() { PointerX = 0; - PointerY += RowHeight; + PointerY += RowHeight + Expansion; RowHeight = 0; } @@ -135,7 +159,7 @@ namespace Quik.Media.Font public void AdvanceColumn(int width, int height) { RowHeight = Math.Max(RowHeight, height); - PointerX += width; + PointerX += width + Expansion; if (PointerX > Image.Width) { @@ -153,6 +177,11 @@ namespace Quik.Media.Font isDisposed = true; } + + internal bool WouldFit(QImageLock source) + { + return !IsFull || PointerX + source.Width > Image.Width || PointerY + source.Height > Image.Height; + } } } } \ No newline at end of file diff --git a/Quik/Media/QFont.cs b/Quik/Media/QFont.cs index ac42f84..e11845a 100644 --- a/Quik/Media/QFont.cs +++ b/Quik/Media/QFont.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using Quik.Media; using Quik.Media.Font; @@ -17,8 +18,23 @@ namespace Quik.Media public FontStretch Stretch => Face.Stretch; public abstract bool HasRune(int rune); - public abstract QFontPage RasterizePage(int codepage, float size, in FontRasterizerOptions options); - public QFontPage RasterizePage(int codepage, float size) => RasterizePage(codepage, size, FontRasterizerOptions.Default); + protected abstract QImage Render(out QGlyphMetrics metrics, int codepoint, float size, in FontRasterizerOptions options); + + + private readonly Dictionary _atlasses = new Dictionary(); + + public void Get(int codepoint, float size, out FontGlyph glyph) + { + SizedFontCollection collection; + + if (!_atlasses.TryGetValue(size, out collection)) + { + collection = new SizedFontCollection(size); + _atlasses.Add(size, collection); + } + + collection.Get(codepoint, out glyph, this); + } // IDisposable private bool isDisposed = false; @@ -32,6 +48,71 @@ namespace Quik.Media } protected virtual void Dispose(bool disposing) { } public void Dispose() => DisposePrivate(true); + + private class SizedFontCollection + { + public float Size { get; } + private readonly Dictionary glyphs = new Dictionary(); + private readonly FontAtlas atlas; + + public SizedFontCollection(float size) + { + Size = size; + + QuikApplication.Current.Platform.GetMaximumImage(out int height, out int width); + + // Do no allow to create a texture that is greater than 16 square characters at 200 DPI. + width = Math.Min(width, (int)(size * 200 * 16)); + height = Math.Min(height, (int)(size * 200 * 16)); + // width = height = 256; + + atlas = new FontAtlas(width, height, QuikApplication.Current.FontProvider.RasterizerOptions.Sdf); + } + + public void Get(int codepoint, out FontGlyph glyph, QFont font) + { + if (glyphs.TryGetValue(codepoint, out glyph)) + return; + + QImage image = font.Render( + out QGlyphMetrics metrics, + codepoint, + Size, + QuikApplication.Current.FontProvider.RasterizerOptions); + + if (image != null) + { + image.LockBits2d(out QImageLock l, QImageLockOptions.Default); + atlas.PutGlyph(codepoint, l, out FontAtlasGlyphInfo glyphInfo); + image.UnlockBits(); + image.Dispose(); + + glyph = new FontGlyph(codepoint, glyphInfo.Image, metrics, glyphInfo.UVs); + } + else + { + glyph = new FontGlyph(codepoint, null, metrics, default); + } + + glyphs[codepoint] = glyph; + } + } + } + + public readonly struct FontGlyph + { + public readonly int CodePoint; + public readonly QImage Image; + public readonly QGlyphMetrics Metrics; + public readonly QRectangle UVs; + + public FontGlyph(int codepoint, QImage image, in QGlyphMetrics metrics, in QRectangle uvs) + { + CodePoint = codepoint; + Image = image; + Metrics = metrics; + UVs = uvs; + } } public struct FontRasterizerOptions @@ -42,53 +123,7 @@ namespace Quik.Media public static readonly FontRasterizerOptions Default = new FontRasterizerOptions() { Resolution = 96.0f, - Sdf = true + Sdf = false }; } - - public class QFontPage : IDisposable - { - public QFont Font { get; } - public int CodePage { get; } - public float Size { get; } - public virtual QImage Image { get; } = null; - public virtual QGlyphMetrics[] Metrics { get; } = Array.Empty(); - - public FontRasterizerOptions Options { get; } - public float Resolution => Options.Resolution; - public bool Sdf => Options.Sdf; - - public void Dispose() => DisposeInternal(false); - - protected QFontPage(QFont font, int codepage, float size, in FontRasterizerOptions options) - { - Font = font; - CodePage = codepage; - Size = size; - Options = options; - } - public QFontPage(QFont font, int codepage, float size, in FontRasterizerOptions options, QImage image, QGlyphMetrics[] metrics) - : this(font, codepage, size, options) - { - Image = image; - Metrics = metrics; - } - - private bool isDisposed = false; - private void DisposeInternal(bool disposing) - { - if (isDisposed) return; - - Dispose(disposing); - isDisposed = true; - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - Image?.Dispose(); - } - } - } } \ No newline at end of file diff --git a/Quik/Media/QImage.cs b/Quik/Media/QImage.cs index 4cd8bdc..98d1dab 100644 --- a/Quik/Media/QImage.cs +++ b/Quik/Media/QImage.cs @@ -78,7 +78,7 @@ namespace Quik.Media int bpp = Format.BytesPerPixel(); for (int i = 0; i < Height; i++) { - IntPtr srcPtr = (IntPtr)((long)ImagePtr + i * bpp); + IntPtr srcPtr = (IntPtr)((long)ImagePtr + i * Width * bpp); long dstPos = x + i * destination.Width; IntPtr dstPtr = (IntPtr)((long)destination.ImagePtr + dstPos * bpp); diff --git a/Quik/OpenGL/GL21Driver.cs b/Quik/OpenGL/GL21Driver.cs index 9dc671f..5583a96 100644 --- a/Quik/OpenGL/GL21Driver.cs +++ b/Quik/OpenGL/GL21Driver.cs @@ -5,6 +5,7 @@ using Quik.VertexGenerator; using static Quik.OpenGL.GLEnum; using Quik.Media; using System.Linq; +using System.Diagnostics; namespace Quik.OpenGL { @@ -105,7 +106,7 @@ namespace Quik.OpenGL GL.UseProgram(program); GL.Uniform1(fMaxZ, (float)(queue.ZDepth+1)); GL.UniformMatrix4(m4Transforms, false, in matrix); - GL.Uniform1(fSdfThreshold, 0.0f); + GL.Uniform1(fSdfThreshold, 0.5f); GL.Uniform1(tx2d, 0); GL.Enable(GL_BLEND); GL.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); diff --git a/Quik/OpenGL/GLEnum.cs b/Quik/OpenGL/GLEnum.cs index f358433..b0fb06e 100644 --- a/Quik/OpenGL/GLEnum.cs +++ b/Quik/OpenGL/GLEnum.cs @@ -11,6 +11,9 @@ namespace Quik.OpenGL GL_RENDERER = 0x1F01, GL_VERSION = 0x1F02, GL_EXTENSIONS = 0x1F03, + GL_MAX_TEXTURE_SIZE = 0x0D33, + GL_MAX_3D_TEXTURE_SIZE = 0x8073, + GL_MAX_ARRAY_TEXTURE_LAYERS = 0x88FF, GL_MULTISAMPLE = 0x809D, GL_BLEND = 0x0BE2, diff --git a/Quik/PAL/IQuikPlatform.cs b/Quik/PAL/IQuikPlatform.cs index d481fd5..d7006cd 100644 --- a/Quik/PAL/IQuikPlatform.cs +++ b/Quik/PAL/IQuikPlatform.cs @@ -55,5 +55,7 @@ namespace Quik.PAL void PortFocus(IQuikPortHandle port); void PortShow(IQuikPortHandle port, bool shown = true); void PortPaint(IQuikPortHandle port, CommandList commands); + void GetMaximumImage(out int width, out int height); + void GetMaximumImage(out int width, out int height, out int depth); } } \ No newline at end of file diff --git a/Quik/Typography/FontProvider.cs b/Quik/Typography/FontProvider.cs new file mode 100644 index 0000000..cdfa878 --- /dev/null +++ b/Quik/Typography/FontProvider.cs @@ -0,0 +1,71 @@ +using Quik.Media; +using Quik.Media.Font; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Quik.Typography +{ + /// + /// The font provider is a caching object that provides fonts for typesetting classes. + /// + public class FontProvider : IDisposable + { + private Dictionary Fonts { get; } = new Dictionary(); + private HashSet UsedFonts { get; } = new HashSet(); + public readonly FontRasterizerOptions RasterizerOptions; + private readonly QuikApplication App; + + public QFont this[FontFace info] + { + get + { + if (!Fonts.TryGetValue(info, out QFont font)) + { + font = (QFont)App.GetMedia(info, MediaHint.Font); + Fonts[info] = font; + } + + UsedFonts.Add(font); + return font; + } + } + + public FontProvider(QuikApplication app, in FontRasterizerOptions options) + { + RasterizerOptions = options; + App = app; + } + public FontProvider(QuikApplication app) + : this(app, FontRasterizerOptions.Default) + { + } + + /// + /// Tracks the use of fonts used by this typesetter and removes any that haven't been referenced since the last cycle. + /// + public void Collect() + { + // foreach (FontJar jar in Fonts.Values.ToArray()) + // { + // if (!UsedFonts.Contains(jar)) + // { + // Fonts.Remove(jar.Info); + // } + // } + // UsedFonts.Clear(); + } + + private bool isDisposed = false; + public void Dispose() + { + if (isDisposed) return; + + isDisposed = true; + foreach (QFont font in Fonts.Values) + { + font.Dispose(); + } + } + } +} diff --git a/Quik/Typography/TextLayout.cs b/Quik/Typography/TextLayout.cs index 4e03ce5..a90c196 100644 --- a/Quik/Typography/TextLayout.cs +++ b/Quik/Typography/TextLayout.cs @@ -226,7 +226,7 @@ namespace Quik.Typography pen.Y -= PostSpace; - group.BoundingBox = new QRectangle(width, 0, 0, pen.Y); + group.BoundingBox = new QRectangle(width, pen.Y, 0, 0); group.Translate(-pen); } @@ -353,13 +353,13 @@ namespace Quik.Typography public struct TypesetCharacter { public int Character; - public QuikTexture Texture; + public QImage Texture; public QRectangle Position; public QRectangle UV; public TypesetCharacter( int chr, - QuikTexture texture, + QImage texture, in QRectangle position, in QRectangle uv) { diff --git a/Quik/Typography/Typesetter.cs b/Quik/Typography/Typesetter.cs new file mode 100644 index 0000000..46e8a92 --- /dev/null +++ b/Quik/Typography/Typesetter.cs @@ -0,0 +1,155 @@ +using Quik.CommandMachine; +using Quik.Media; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Quik.Typography +{ + public static class Typesetter + { + private ref struct LineEnumerator + { + private ReadOnlySpan Entire, Segment; + private bool Final; + + public ReadOnlySpan Current => Segment; + + public LineEnumerator(ReadOnlySpan value) + { + Entire = value; + Segment = ReadOnlySpan.Empty; + Final = false; + } + + public void Reset() + { + Segment = ReadOnlySpan.Empty; + Final = false; + } + + public bool MoveNext() + { + if (Final) + { + return false; + } + else if (Segment == ReadOnlySpan.Empty) + { + int index = Entire.IndexOf('\n'); + if (index == -1) + { + Segment = Entire; + } + else + { + Segment = Entire.Slice(0, index); + } + + return true; + } + else + { + Entire.Overlaps(Segment, out int offset); + + if (offset + Segment.Length >= Entire.Length) + { + return false; + } + + ReadOnlySpan rest = Entire.Slice(offset + Segment.Length + 1); + + int index = rest.IndexOf('\n'); + if (index == -1) + { + Segment = rest; + Final = true; + } + else + { + Segment = rest.Slice(0, index); + } + return true; + } + } + } + + public static void TypesetHorizontalDirect(this CommandList list, string str, QVec2 origin, float size, QFont font) + { + Dictionary drawInfo = new Dictionary(); + var enumerator = new LineEnumerator(str.AsSpan()); + + QVec2 pen = origin; + while (enumerator.MoveNext()) + { + ReadOnlySpan line = enumerator.Current; + float rise = 0.0f; + float fall = 0.0f; + + // Find out all the code pages required, and the line height. + foreach (Rune r in line.EnumerateRunes()) + { + int codepoint = r.Value; + font.Get(codepoint, size, out FontGlyph glyph); + float crise = glyph.Metrics.HorizontalBearing.Y; + float cfall = glyph.Metrics.Size.Y - crise; + + rise = Math.Max(crise, rise); + fall = Math.Max(cfall, fall); + } + + pen += new QVec2(0, rise); + + foreach (Rune r in line.EnumerateRunes()) + { + FontDrawInfo info; + int codepoint = r.Value; + + font.Get(codepoint, size, out FontGlyph glyph); + ref readonly QGlyphMetrics metrics = ref glyph.Metrics; + QImage image = glyph.Image; + + if (image == null) + { + pen += new QVec2(metrics.Advance.X, 0); + continue; + } + + if (!drawInfo.TryGetValue(image, out info)) + { + info = new FontDrawInfo(); + info.Image = image; + info.rectangles = new List(); + drawInfo[image] = info; + } + + QRectangle dest = new QRectangle( + pen + new QVec2(metrics.HorizontalBearing.X + metrics.Size.X, metrics.Size.Y - metrics.HorizontalBearing.Y), + pen + new QVec2(metrics.HorizontalBearing.X, -metrics.HorizontalBearing.Y)); + + info.rectangles.Add(dest); + info.rectangles.Add(glyph.UVs); + + pen.X += metrics.Advance.X; + } + + pen.X = origin.X; + pen.Y += fall; + } + + // Now for each rectangle we can dispatch draw calls. + foreach (FontDrawInfo info in drawInfo.Values) + { + list.Image(info.Image, info.rectangles.ToArray(), true); + } + } + + private struct FontDrawInfo + { + public QImage Image; + public List rectangles; + } + } +} diff --git a/Quik/VertexGenerator/VertexCommandEngine.cs b/Quik/VertexGenerator/VertexCommandEngine.cs index 9b9333c..c616c53 100644 --- a/Quik/VertexGenerator/VertexCommandEngine.cs +++ b/Quik/VertexGenerator/VertexCommandEngine.cs @@ -1063,7 +1063,7 @@ namespace Quik.VertexGenerator DrawQueue.AddVertex(vertex); vertex.Position = new QVec2(rect.Right, rect.Top); - vertex.TextureCoordinates = new QVec2(uvs.Right, uvs.Right); + vertex.TextureCoordinates = new QVec2(uvs.Right, uvs.Top); DrawQueue.AddVertex(vertex); DrawQueue.AddElement(0); DrawQueue.AddElement(2); DrawQueue.AddElement(3); diff --git a/Quik/res/gl21.frag b/Quik/res/gl21.frag index d8507f3..2dac44e 100644 --- a/Quik/res/gl21.frag +++ b/Quik/res/gl21.frag @@ -13,9 +13,8 @@ out vec4 fragColor; uniform int iEnableSdf; uniform int iEnableTexture; uniform int iAlphaDiscard; -uniform float fSdfThreshold; +uniform float fSdfThreshold = 0.5; uniform sampler2D tx2d; -uniform sampler2DArray tx2dArray; const float fAlphaThreshold = 0.01; @@ -23,7 +22,13 @@ vec4 getTexture() { if (iEnableTexture == 3) { - return texture(tx2dArray, vec3(fv2TexPos, ffTexLayer)); + // return texture(tx2dArray, vec3(fv2TexPos, ffTexLayer)); + } + else if (iEnableSdf == 1) + { + vec2 texelSz = 1.0/vec2(textureSize(tx2d, 0)); + vec2 txCoord2 = fv2TexPos + texelSz * (1 - mod(fv2TexPos, texelSz)); + return texture(tx2d, txCoord2); } else { @@ -41,12 +46,7 @@ void main(void) if (iEnableSdf != 0) { - float a = max(value.r, value.a); - - value = - (a >= fSdfThreshold) ? - vec4(1.0,1.0,1.0,1.0) : - vec4(0.0,0.0,0.0,0.0); + value = vec4(vec3(1.0), smoothstep(fSdfThreshold-0.1, fSdfThreshold+0.1, value.r)); } if (iAlphaDiscard != 0 && value.a <= fAlphaThreshold) diff --git a/tests/QuikDemo/Program.cs b/tests/QuikDemo/Program.cs index 3b35b4a..d5574f5 100644 --- a/tests/QuikDemo/Program.cs +++ b/tests/QuikDemo/Program.cs @@ -1,8 +1,12 @@ -using Quik; +using System; +using Quik; using Quik.CommandMachine; using Quik.Controls; using Quik.OpenTK; - +using Quik.Media.Defaults; +using Quik.Media; +using Quik.Typography; +using Quik.PAL; namespace QuikDemo { @@ -18,11 +22,23 @@ namespace QuikDemo public class EmptyView : View { - protected override void PaintBegin(CommandQueue cmd) + private QFont font; + + protected override void PaintBegin(CommandList cmd) { base.PaintBegin(cmd); - cmd.Rectangle(new QRectangle(0, 0, 16, 16)); + if (font == null) + { + IFontDataBase db = FontDataBaseProvider.Instance; + font = new QFontFreeType(db.FontFileInfo(db.Sans).OpenRead()); + } + + cmd.Rectangle(new QRectangle(16, 16, 0, 0)); + cmd.TypesetHorizontalDirect( + "The quick brown fox jumps over the lazy dog.\n" + + "hi?", + new QVec2(64.33f, 0.77f), 9, font); } } } \ No newline at end of file