diff --git a/Scribe/FontSystem.cs b/Scribe/FontSystem.cs index 3630278..4245033 100644 --- a/Scribe/FontSystem.cs +++ b/Scribe/FontSystem.cs @@ -389,14 +389,14 @@ public TextLayout CreateLayout(string text, TextLayoutSettings settings) { if (string.IsNullOrEmpty(text)) { - var empty = new TextLayout(); + var empty = TextLayout.Get(); empty.UpdateLayout(text, settings, this); return empty; } if (!CacheLayouts) { - var direct = new TextLayout(); + var direct = TextLayout.Get(); direct.UpdateLayout(text, settings, this); return direct; } @@ -406,7 +406,7 @@ public TextLayout CreateLayout(string text, TextLayoutSettings settings) if (layoutCache.TryGetValue(key, out var cached)) return cached; - var layout = new TextLayout(); + var layout = TextLayout.Get(); layout.UpdateLayout(text, settings, this); layoutCache.Add(key, layout); @@ -423,12 +423,13 @@ LayoutCacheKey GenerateLayoutCacheKey(string text, TextLayoutSettings s) public Vector2 MeasureText(string text, float pixelSize, FontFile font, float letterSpacing = 0) { - var settings = TextLayoutSettings.Default; + var settings = TextLayoutSettings.Get(); settings.PixelSize = pixelSize; settings.Font = font; settings.LetterSpacing = letterSpacing; var layout = CreateLayout(text, settings); + TextLayoutSettings.Return(settings); return layout.Size; } @@ -440,12 +441,14 @@ public Vector2 MeasureText(string text, TextLayoutSettings settings) public void DrawText(string text, Vector2 position, FontColor color, float pixelSize, FontFile font, float letterSpacing = 0) { - var settings = TextLayoutSettings.Default; + var settings = TextLayoutSettings.Get(); settings.PixelSize = pixelSize; settings.Font = font; settings.LetterSpacing = letterSpacing; DrawText(text, position, color, settings); + + TextLayoutSettings.Return(settings); } @@ -457,12 +460,16 @@ public void DrawText(string text, Vector2 position, FontColor color, TextLayoutS DrawLayout(layout, position, color); } + List _vertices = new List(); + List _indices = new List(); public void DrawLayout(TextLayout layout, Vector2 position, FontColor color) { if (layout.Lines.Count == 0) return; - var vertices = new List(); - var indices = new List(); + _vertices.Clear(); + var vertices = _vertices; + _indices.Clear(); + var indices = _indices; int vertexCount = 0; foreach (var line in layout.Lines) @@ -487,15 +494,26 @@ public void DrawLayout(TextLayout layout, Vector2 position, FontColor color) vertices.Add(new IFontRenderer.Vertex(new Vector3(glyphX + glyphW, glyphY + glyphH, 0), color, new Vector2(glyph.U1, glyph.V1))); // Create quad indices - indices.AddRange(new[] { vertexCount, vertexCount + 1, vertexCount + 2, vertexCount + 1, vertexCount + 3, vertexCount + 2 }); + indices.Add(vertexCount); + indices.Add(vertexCount + 1); + indices.Add(vertexCount + 2); + indices.Add(vertexCount + 1); + indices.Add(vertexCount + 3); + indices.Add(vertexCount + 2); vertexCount += 4; } } if (vertices.Count > 0) { + #if NET5_0_OR_GREATER + renderer.DrawQuads(atlasTexture, CollectionsMarshal.AsSpan(vertices), CollectionsMarshal.AsSpan(indices)); + #else renderer.DrawQuads(atlasTexture, vertices.ToArray(), indices.ToArray()); +#endif } + + TextLayout.Return(layout); } #endregion diff --git a/Scribe/Internal/Bitmap.cs b/Scribe/Internal/Bitmap.cs index 701ca4e..1ef974d 100644 --- a/Scribe/Internal/Bitmap.cs +++ b/Scribe/Internal/Bitmap.cs @@ -1,4 +1,7 @@ using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; using System.Numerics; using static Prowl.Scribe.Internal.Common; @@ -11,7 +14,7 @@ internal class Bitmap public int h; // Height in pixels public int stride; // Row stride in bytes public FakePtr pixels; // Pixel buffer (8-bit coverage) - + private List _createdActiveEdges = new List(); /// Flatten curves → rasterize. public void Rasterize( float flatnessInPixels, @@ -213,15 +216,16 @@ private static void FillActiveEdges(float[] scanline, int scanlineFill, int len, e = e.next; } } - + /// Main scanline rasterization over sorted edges. Uses a sentinel head for the active edge list. private void RasterizeSortedEdges(FakePtr edges, int count, int vsubsample, int offX, int offY) { // Active list sentinel (dummy head) - var head = new ActiveEdge { next = null }; + var head = ActiveEdge.Get(); + _createdActiveEdges.Add(head); // Scratch scanlines: coverage + running sums - float[] scanline = w > 64 ? new float[w * 2 + 1] : new float[129]; + float[] scanline = w > 64 ? ArrayPool.Shared.Rent(w * 2 + 1) : ArrayPool.Shared.Rent(129); int scanlineFill = w; // second half holds column sums int y = offY; // absolute y in destination bitmap @@ -299,11 +303,20 @@ private void RasterizeSortedEdges(FakePtr edges, int count, int vsubsample ++y; ++row; } + + foreach (ActiveEdge edge in _createdActiveEdges) + { + ActiveEdge.Return(edge); + } + + _createdActiveEdges.Clear(); + ArrayPool.Shared.Return(scanline); } private ActiveEdge NewActive(Edge e, int offX, float startY) { - var z = new ActiveEdge(); + var z = ActiveEdge.Get(); + _createdActiveEdges.Add(z); float dxdy = (e.x1 - e.x0) / (e.y1 - e.y0); // safe: edges generated with y0 != y1 z.fdx = dxdy; z.fdy = dxdy != 0.0f ? 1.0f / dxdy : 0.0f; @@ -327,8 +340,9 @@ private void RasterizeContours(Vector2[] pts, int[] wcount, int windings, float for (int i = 0; i < windings; ++i) totalEdges += wcount[i]; // +1 sentinel edge - var edges = new Edge[totalEdges + 1]; - for (int i = 0; i < edges.Length; ++i) edges[i] = new Edge(); + int edgesLength = totalEdges + 1; + var edges = ArrayPool.Shared.Rent(edgesLength); + for (int i = 0; i < edgesLength; ++i) edges[i] = Edge.Get(); int n = 0; // number of produced edges int m = 0; // running index into pts per winding @@ -368,6 +382,13 @@ private void RasterizeContours(Vector2[] pts, int[] wcount, int windings, float SortEdgesInsSort(edgePtr, n); RasterizeSortedEdges(edgePtr, n, vsubsample, offX, offY); + + for (int i = 0; i < edgesLength; i++) + { + Edge.Return(edges[i]); + } + ArrayPool.Shared.Return(edges); + edgePtr.Clear(edgesLength); } private Vector2[] FlattenCurves(GlyphVertex[] vertices, int numVerts, float objspaceFlatness, out int[] contourLengths, out int numContours) @@ -612,6 +633,26 @@ private class ActiveEdge public float fx; public ActiveEdge next; public float sy; + + private static Stack _pool = new Stack(); + + public static ActiveEdge Get() + { + if (_pool.TryPop(out ActiveEdge edge)) + { + edge.next = null; + return edge; + } + + return new ActiveEdge(); + } + + public static void Return(ActiveEdge edge) + { + if (edge == null) + throw new InvalidOperationException("Objects cannot be null when going into the stack!"); + _pool.Push(edge); + } } private class Edge @@ -619,6 +660,26 @@ private class Edge public int invert; public float x0, x1; public float y0, y1; + + private static Stack _pool = new Stack(); + + public static Edge Get() + { + if (_pool.TryPop(out Edge edge)) + { + return edge; + } + + return new Edge(); + } + + public static void Return(Edge edge) + { + if (edge == null) + throw new InvalidOperationException("Objects cannot be null when going into the stack!"); + + _pool.Push(edge); + } } } } diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index dfe5d6b..7bea543 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -7,9 +7,13 @@ using Prowl.Scribe.Internal; using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; +using System.Net.Mime; using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; namespace Prowl.Scribe { @@ -36,25 +40,110 @@ public struct RectangleF public RectangleF(float x, float y, float w, float h) { X = x; Y = y; Width = w; Height = h; } } - public struct DrawText : IDrawOp + public class DrawText : IDrawOp { public TextLayout Layout; public Vector2 Pos; public FontColor Color; public List Decorations; // optional public List LinkRanges; + + public static Stack _pool = new Stack(); + public static int _created = 0; + public static int _returned = 0; + + public void AddLinkRange(IntRange range) + { + LinkRanges.Add(range); + } + + public void AddDecoration(DecorationSpan deco) + { + Decorations.Add(deco); + } + + public static DrawText Get(TextLayout layout, Vector2 position, FontColor color, List decorations = null) + { + if (!_pool.TryPop(out DrawText text)) + { + text = new DrawText(); + _created++; + } + + text.Layout = layout; + text.Pos = position; + text.Color = color; + + if (text.Decorations == null) text.Decorations = new List(); + text.Decorations.Clear(); + if (text.LinkRanges == null) text.LinkRanges = new List(); + text.LinkRanges.Clear(); + + return text; + } + + public static void Return(DrawText text) + { + _pool.Push(text); + _returned++; + } + + public static void ResetCounters() + { + _returned = 0; + _created = 0; + } } - public struct DrawQuad : IDrawOp + public class DrawQuad : IDrawOp { public RectangleF Rect; public FontColor Color; + public static Stack _pool = new Stack(); + + public static DrawQuad Get(RectangleF rectangle, FontColor color) + { + if (!_pool.TryPop(out DrawQuad quad)) + { + quad = new DrawQuad(); + } + + quad.Rect = rectangle; + quad.Color = color; + + return quad; + } + + public static void Return(DrawQuad text) + { + _pool.Push(text); + } } - public struct DrawImage : IDrawOp + public class DrawImage : IDrawOp { public RectangleF Rect; public object Texture; + + public static Stack _pool = new Stack(); + + public static DrawImage Get(RectangleF rectangle, object texture) + { + if (!_pool.TryPop(out DrawImage quad)) + { + quad = new DrawImage(); + } + + quad.Rect = rectangle; + quad.Texture = texture; + + return quad; + } + + public static void Return(DrawImage text) + { + _pool.Push(text); + } } public struct IntRange { public int Start, End; public IntRange(int s, int e) { Start = s; End = e; } } @@ -147,6 +236,25 @@ public sealed class MarkdownDisplayList public readonly List Ops = new List(); public readonly List Links = new List(); public Vector2 Size; // overall width/height used + + private static Stack _pool = new Stack(); + + public static MarkdownDisplayList Get() + { + if (!_pool.TryPop(out MarkdownDisplayList list)) + { + list = new MarkdownDisplayList(); + } + + return list; + } + + public static void Return(MarkdownDisplayList list) + { + list.Ops.Clear(); + list.Links.Clear(); + _pool.Push(list); + } } #endregion @@ -157,7 +265,7 @@ public static class MarkdownLayoutEngine public static MarkdownDisplayList Layout(Document doc, FontSystem fontSystem, MarkdownLayoutSettings settings, IMarkdownImageProvider? imageProvider = null) { - var dl = new MarkdownDisplayList(); + var dl = MarkdownDisplayList.Get(); float cursorY = 0; float maxRight = 0; @@ -194,16 +302,19 @@ public static MarkdownDisplayList Layout(Document doc, FontSystem fontSystem, Ma } dl.Size = new Vector2(settings.Width, cursorY); + return dl; } - + public static void Render(MarkdownDisplayList dl, FontSystem fontSystem, IFontRenderer renderer, Vector2 position, MarkdownLayoutSettings settings) { if (dl == null || dl.Ops.Count == 0) return; // Batch shape quads into a single DrawQuads call using the font atlas texture. - var verts = new List(128); - var idx = new List(256); + _verts.Clear(); + var verts = _verts; + _indices.Clear(); + var idx = _indices; int vbase = 0; foreach (var op in dl.Ops) @@ -212,12 +323,17 @@ public static void Render(MarkdownDisplayList dl, FontSystem fontSystem, IFontRe { var offsetRect = new RectangleF(q.Rect.X + position.X, q.Rect.Y + position.Y, q.Rect.Width, q.Rect.Height); AddQuad(ref verts, ref idx, ref vbase, offsetRect, q.Color); + DrawQuad.Return(q); } } if (verts.Count > 0) { + #if NET5_0_OR_GREATER + renderer.DrawQuads(fontSystem.Texture, CollectionsMarshal.AsSpan(verts), CollectionsMarshal.AsSpan(idx)); + #else renderer.DrawQuads(fontSystem.Texture, verts.ToArray(), idx.ToArray()); + #endif } // Draw text and images in submission order @@ -231,23 +347,30 @@ public static void Render(MarkdownDisplayList dl, FontSystem fontSystem, IFontRe DrawLinkOverprint(t, position, fontSystem, renderer, settings); if (t.Decorations != null && t.Decorations.Count > 0) DrawDecorations(t, position, fontSystem, renderer, settings); + + DrawText.Return(t); } else if (op is DrawImage img) { - var vertsImg = new IFontRenderer.Vertex[4]; - var idxImg = new int[] { 0, 2, 1, 1, 2, 3 }; var r = img.Rect; float offsetX = r.X + position.X; float offsetY = r.Y + position.Y; - vertsImg[0] = new IFontRenderer.Vertex(new Vector3(offsetX, offsetY, 0), FontColor.White, new Vector2(0, 0)); - vertsImg[1] = new IFontRenderer.Vertex(new Vector3(offsetX + r.Width, offsetY, 0), FontColor.White, new Vector2(1, 0)); - vertsImg[2] = new IFontRenderer.Vertex(new Vector3(offsetX, offsetY + r.Height, 0), FontColor.White, new Vector2(0, 1)); - vertsImg[3] = new IFontRenderer.Vertex(new Vector3(offsetX + r.Width, offsetY + r.Height, 0), FontColor.White, new Vector2(1, 1)); - renderer.DrawQuads(img.Texture, vertsImg, idxImg); + _vertsImg[0] = new IFontRenderer.Vertex(new Vector3(offsetX, offsetY, 0), FontColor.White, new Vector2(0, 0)); + _vertsImg[1] = new IFontRenderer.Vertex(new Vector3(offsetX + r.Width, offsetY, 0), FontColor.White, new Vector2(1, 0)); + _vertsImg[2] = new IFontRenderer.Vertex(new Vector3(offsetX, offsetY + r.Height, 0), FontColor.White, new Vector2(0, 1)); + _vertsImg[3] = new IFontRenderer.Vertex(new Vector3(offsetX + r.Width, offsetY + r.Height, 0), FontColor.White, new Vector2(1, 1)); + renderer.DrawQuads(img.Texture, _vertsImg, _idxImg); + + DrawImage.Return(img); } } + + MarkdownDisplayList.Return(dl); } + static IFontRenderer.Vertex[] _vertsImg = new IFontRenderer.Vertex[4]; + static int[] _idxImg = new int[] { 0, 2, 1, 1, 2, 3 }; + public static bool TryGetLinkAt(MarkdownDisplayList dl, Vector2 point, Vector2 renderOffset, out string href) { foreach (var link in dl.Links) @@ -274,7 +397,7 @@ private static float LayoutParagraph(Paragraph p, float x, float y, MarkdownDisp float? sizeOverride = null, float? lineHeightOverride = null, FontFile? fontOverride = null, float? widthOverride = null) { float wAvail = widthOverride ?? settings.Width; - var segment = new List(); + var segment = GetInlineList(); foreach (var inline in p.Inlines) { if (inline.Kind == InlineKind.Image) @@ -297,26 +420,49 @@ private static float LayoutParagraph(Paragraph p, float x, float y, MarkdownDisp return y + settings.ParagraphSpacing; } + private static Stack> _inlineListPool = new Stack>(); + + private static List GetInlineList() + { + if (!_inlineListPool.TryPop(out List list)) + { + list = new List(); + } + + return list; + } + + private static void ReturnInlineList(List list) + { + list.Clear(); + _inlineListPool.Push(list); + } + private static float LayoutTextSegment(List inlines, float x, float y, MarkdownDisplayList dl, FontSystem fontSystem, MarkdownLayoutSettings settings, float? sizeOverride, float? lineHeightOverride, FontFile? fontOverride, float width) { var (text, decos, linkSpans, styles) = FlattenInlines(inlines); - + ReturnInlineList(inlines); var baseFont = fontOverride ?? settings.ParagraphFont; - var tls = TextLayoutSettings.Default; + var tls = TextLayoutSettings.Get(); tls.PixelSize = sizeOverride ?? settings.BaseSize; tls.LineHeight = lineHeightOverride ?? settings.LineHeight; tls.WrapMode = TextWrapMode.Wrap; tls.MaxWidth = width; tls.Alignment = TextAlignment.Left; tls.Font = baseFont; - tls.FontSelector = (charIndex) => ResolveFontForIndex(charIndex, fontSystem, baseFont, styles, settings); + tls.StyleSpans = styles; + tls.LayoutSettings = settings; + // tls.FontSelector = (charIndex) => ResolveFontForIndex(charIndex, fontSystem, baseFont, styles, settings); var tl = fontSystem.CreateLayout(text, tls); - var linkRanges = new List(); - foreach (var ls in linkSpans) linkRanges.Add(ls.Range); - var op = new DrawText { Layout = tl, Pos = new Vector2(x, y), Color = settings.ColorText, Decorations = decos, LinkRanges = linkRanges }; + var op = DrawText.Get(tl, new Vector2(x, y), settings.ColorText, decos); + foreach (var ls in linkSpans) + { + op.AddLinkRange(ls.Range); + } + dl.Ops.Add(op); if (linkSpans.Count > 0) AddLinkHitBoxes(dl, op, linkSpans); @@ -336,11 +482,12 @@ private static float LayoutImage(Inline img, float x, float y, MarkdownDisplayLi w = widthAvail; h *= scale; } - dl.Ops.Add(new DrawImage { Texture = tex, Rect = new RectangleF(x, y, w, h) }); + dl.Ops.Add(DrawImage.Get(new RectangleF(x, y, w, h), tex)); return y + h; } // fallback to alt text - var alt = new List { Inline.TextRun(img.Text) }; + var alt = GetInlineList(); + alt.Add(Inline.TextRun(img.Text)); return LayoutTextSegment(alt, x, y, dl, fontSystem, settings, sizeOverride, lineHeightOverride, fontOverride, widthAvail); } @@ -367,10 +514,7 @@ private static float LayoutQuote(BlockQuote q, float x, float y, MarkdownDisplay float h = yAfter - y - settings.ParagraphSpacing; // prepend left bar quad (ensure it renders under text by ordering) - dl.Ops.Insert(beforeOpsCount, new DrawQuad { - Rect = new RectangleF(x, y, settings.BlockQuoteBarWidth, h), - Color = settings.ColorQuoteBar - }); + dl.Ops.Insert(beforeOpsCount, DrawQuad.Get(new RectangleF(x, y, settings.BlockQuoteBarWidth, h), settings.ColorQuoteBar)); return yAfter; } @@ -391,12 +535,12 @@ private static float LayoutList(ListBlock list, float x, float y, int depth, Mar float r = settings.BaseSize * 0.2f; float bx = x + depth * settings.ListIndent + (bulletBox - 2 * r) * 0.5f; float by = lineTop + settings.BaseSize * 0.35f; // approximate baseline offset - dl.Ops.Add(new DrawQuad { Rect = new RectangleF(bx, by, 2 * r, 2 * r), Color = settings.ColorText }); + dl.Ops.Add(DrawQuad.Get(new RectangleF(bx, by, 2 * r, 2 * r), settings.ColorText)); } else { // right-aligned number inside bulletBox - var tlsNum = TextLayoutSettings.Default; + var tlsNum = TextLayoutSettings.Get(); tlsNum.PixelSize = settings.BaseSize; tlsNum.LineHeight = settings.LineHeight; tlsNum.WrapMode = TextWrapMode.NoWrap; @@ -404,7 +548,7 @@ private static float LayoutList(ListBlock list, float x, float y, int depth, Mar tlsNum.Alignment = TextAlignment.Right; tlsNum.Font = settings.ParagraphFont; var tlNum = fontSystem.CreateLayout($"{index}.", tlsNum); - dl.Ops.Add(new DrawText { Layout = tlNum, Pos = new Vector2(x + depth * settings.ListIndent, lineTop), Color = settings.ColorText }); + dl.Ops.Add(DrawText.Get(tlNum, new Vector2(x + depth * settings.ListIndent, lineTop), settings.ColorText)); } // lead line @@ -437,7 +581,7 @@ private static float LayoutList(ListBlock list, float x, float y, int depth, Mar private static float LayoutHr(float x, float y, MarkdownDisplayList dl, MarkdownLayoutSettings settings) { y += settings.HrSpacing; - dl.Ops.Add(new DrawQuad { Rect = new RectangleF(x, y, settings.Width, settings.HrThickness), Color = settings.ColorRule }); + dl.Ops.Add(DrawQuad.Get(new RectangleF(x, y, settings.Width, settings.HrThickness),settings.ColorRule)); y += settings.HrThickness + settings.HrSpacing; return y; } @@ -449,7 +593,7 @@ private static float LayoutCode(CodeBlock cb, float x, float y, MarkdownDisplayL float innerX = x + pad; float innerW = MathF.Max(0, wAvail - 2 * pad); - var tls = TextLayoutSettings.Default; + var tls = TextLayoutSettings.Get(); tls.PixelSize = settings.BaseSize * 0.95f; tls.LineHeight = 1.25f; tls.WrapMode = TextWrapMode.Wrap; @@ -459,15 +603,26 @@ private static float LayoutCode(CodeBlock cb, float x, float y, MarkdownDisplayL var tl = fontSystem.CreateLayout(cb.Code.Replace("\r\n", "\n"), tls); float h = tl.Size.Y + 2 * pad; - dl.Ops.Add(new DrawQuad { Rect = new RectangleF(x, y, wAvail, h), Color = settings.ColorCodeBg }); - dl.Ops.Add(new DrawText { Layout = tl, Pos = new Vector2(innerX, y + pad), Color = settings.ColorText }); + dl.Ops.Add(DrawQuad.Get(new RectangleF(x, y, wAvail, h),settings.ColorCodeBg)); + dl.Ops.Add(DrawText.Get(tl, new Vector2(innerX, y + pad), settings.ColorText)); return y + h + settings.ParagraphSpacing; } + private static int GetTableMaxCells(Table table) + { + int cols = 0; + foreach (TableRow row in table.Rows) + { + cols = Math.Max(cols, row.Cells.Count); + } + + return cols; + } + private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList dl, FontSystem fontSystem, MarkdownLayoutSettings settings, float? widthOverride = null) { - int cols = t.Rows.Max(r => r.Cells.Count); - float[] minCol = new float[cols]; + int cols = GetTableMaxCells(t); + var minCol = ArrayPool.Shared.Rent(cols); float wAvail = widthOverride ?? settings.Width; // pass 1: min widths via NoWrap measure @@ -477,23 +632,26 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList { var cell = row.Cells[c]; var (text, _, _, styles) = FlattenInlines(cell.Inlines); - var tls = TextLayoutSettings.Default; + var tls = TextLayoutSettings.Get(); tls.PixelSize = settings.BaseSize; tls.LineHeight = settings.LineHeight; tls.WrapMode = TextWrapMode.NoWrap; tls.MaxWidth = float.MaxValue; tls.Alignment = AlignToText(cell.Align); tls.Font = settings.ParagraphFont; - tls.FontSelector = (charIndex) => ResolveFontForIndex(charIndex, fontSystem, settings.ParagraphFont, styles, settings); + tls.StyleSpans = styles; + tls.LayoutSettings = settings; + // tls.FontSelector = (charIndex) => ResolveFontForIndex(charIndex, fontSystem, settings.ParagraphFont, styles, settings); var tl = fontSystem.CreateLayout(text, tls); minCol[c] = MathF.Max(minCol[c], tl.Size.X); + TextLayout.Return(tl); } } // distribute to fit content width float totalMin = minCol.Sum(); - float[] colW = new float[cols]; + var colW = ArrayPool.Shared.Rent(cols); if (totalMin <= wAvail) { float extra = wAvail - totalMin; @@ -507,13 +665,13 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList } // Precompute column x positions for grid lines - float[] colX = new float[cols + 1]; + var colX = ArrayPool.Shared.Rent(cols + 1); colX[0] = x; for (int c = 0; c < cols; c++) colX[c + 1] = colX[c] + colW[c]; float tableTop = y; float rowY = y; - var perRowHeights = new float[t.Rows.Count]; + var perRowHeights = ArrayPool.Shared.Rent(t.Rows.Count); // Pass 2: layout rows (we'll emit text now and draw grid after we know full height) for (int r = 0; r < t.Rows.Count; r++) @@ -527,20 +685,22 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList var cell = row.Cells[c]; var (text, decos, linkSpans, styles) = FlattenInlines(cell.Inlines); - var tls = TextLayoutSettings.Default; + var tls = TextLayoutSettings.Get(); tls.PixelSize = settings.BaseSize; tls.LineHeight = settings.LineHeight; tls.WrapMode = TextWrapMode.Wrap; tls.MaxWidth = colW[c]; tls.Alignment = AlignToText(cell.Align); tls.Font = settings.ParagraphFont; - tls.FontSelector = (charIndex) => ResolveFontForIndex(charIndex, fontSystem, settings.ParagraphFont, styles, settings); + tls.StyleSpans = styles; + tls.LayoutSettings = settings; + // tls.FontSelector = (charIndex) => ResolveFontForIndex(charIndex, fontSystem, settings.ParagraphFont, styles, settings); var tl = fontSystem.CreateLayout(text, tls); - - var linkRanges = new List(); - foreach (var ls in linkSpans) linkRanges.Add(ls.Range); - var op = new DrawText { Layout = tl, Pos = new Vector2(cx, rowY), Color = settings.ColorText, Decorations = decos, LinkRanges = linkRanges }; + + var op = DrawText.Get(tl, new Vector2(cx, rowY), settings.ColorText, decos); + foreach (var ls in linkSpans) op.AddLinkRange(ls.Range); + dl.Ops.Add(op); if (linkSpans.Count > 0) AddLinkHitBoxes(dl, op, linkSpans); rowHeight = MathF.Max(rowHeight, tl.Size.Y); @@ -561,22 +721,20 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList float yCursor = tableTop; for (int r = 0; r <= t.Rows.Count; r++) { - dl.Ops.Insert(0, new DrawQuad { - Rect = new RectangleF(x, yCursor - th * 0.5f, wAvail, th), - Color = settings.ColorRule - }); + dl.Ops.Insert(0, DrawQuad.Get(new RectangleF(x, yCursor - th * 0.5f, wAvail, th), settings.ColorRule)); if (r < t.Rows.Count) yCursor += perRowHeights[r]; } } // Vertical lines: at each column boundary for (int c = 0; c < colX.Length; c++) { - dl.Ops.Insert(0, new DrawQuad { - Rect = new RectangleF(colX[c] - th * 0.5f, tableTop, th, tableBottom - tableTop), - Color = settings.ColorRule - }); + dl.Ops.Insert(0, DrawQuad.Get(new RectangleF(colX[c] - th * 0.5f, tableTop, th, tableBottom - tableTop), settings.ColorRule)); } - + + ArrayPool.Shared.Return(minCol); + ArrayPool.Shared.Return(colW); + ArrayPool.Shared.Return(colX); + ArrayPool.Shared.Return(perRowHeights); return tableBottom + settings.ParagraphSpacing; } @@ -605,12 +763,21 @@ private static FontFile ResolveFontForIndex(int idx, FontSystem fs, FontFile bas #region Inline flattening & decorations + static StringBuilder _stringBuilder = new StringBuilder(); + static List _decorationSpans = new List(); + static List _linkSpans = new List(); + static List _styleSpans = new List(); + private static (string text, List decos, List links, List styles) FlattenInlines(List inlines) { - var sb = new System.Text.StringBuilder(); - var decos = new List(); - var links = new List(); - var styles = new List(); + _stringBuilder.Clear(); + var sb = _stringBuilder; + _decorationSpans.Clear(); + var decos = _decorationSpans; + _linkSpans.Clear(); + var links = _linkSpans; + _styleSpans.Clear(); + var styles = _styleSpans; void EmitText(string s, bool bold, bool italic) { @@ -692,8 +859,10 @@ private static void DrawLinkOverprint(DrawText t, Vector2 position, FontSystem f var layout = t.Layout; if (layout.Lines == null || layout.Lines.Count == 0) return; - var verts = new List(256); - var idx = new List(512); + _verts.Clear(); + var verts = _verts; + _indices.Clear(); + var idx = _indices; int vbase = 0; string text = layout.Text ?? string.Empty; @@ -750,16 +919,26 @@ private static void DrawLinkOverprint(DrawText t, Vector2 position, FontSystem f } if (verts.Count > 0) + { +#if NET5_0_OR_GREATER + renderer.DrawQuads(fontSystem.Texture, CollectionsMarshal.AsSpan(verts), CollectionsMarshal.AsSpan(idx)); + #else renderer.DrawQuads(fontSystem.Texture, verts.ToArray(), idx.ToArray()); +#endif + } } + static List _verts = new List(128); + static List _indices = new List(256); private static void DrawDecorations(DrawText t, Vector2 position, FontSystem fontSystem, IFontRenderer renderer, MarkdownLayoutSettings settings) { var layout = t.Layout; if (layout.Lines == null || layout.Lines.Count == 0) return; - var verts = new List(128); - var idx = new List(256); + _verts.Clear(); + var verts = _verts; + _indices.Clear(); + var idx = _indices; int vbase = 0; // We will map each line's glyphs to absolute character indices in layout.Text. @@ -855,7 +1034,13 @@ private static void DrawDecorations(DrawText t, Vector2 position, FontSystem fon } if (verts.Count > 0) + { +#if NET5_0_OR_GREATER + renderer.DrawQuads(fontSystem.Texture, CollectionsMarshal.AsSpan(verts), CollectionsMarshal.AsSpan(idx)); +#else renderer.DrawQuads(fontSystem.Texture, verts.ToArray(), idx.ToArray()); +#endif + } } private static void AddLinkHitBoxes(MarkdownDisplayList dl, DrawText t, List links) @@ -872,7 +1057,7 @@ private static void AddLinkHitBoxes(MarkdownDisplayList dl, DrawText t, List.Shared.Rent(gCount); for (int gi = 0; gi < gCount; gi++) { char gc = glyphs[gi].Character; @@ -900,6 +1085,8 @@ private static void AddLinkHitBoxes(MarkdownDisplayList dl, DrawText t, List.Shared.Return(g2t); } } diff --git a/Scribe/MarkdownParser.cs b/Scribe/MarkdownParser.cs index ff8f28f..a8e39dc 100644 --- a/Scribe/MarkdownParser.cs +++ b/Scribe/MarkdownParser.cs @@ -212,9 +212,23 @@ private Inline(InlineKind kind, string text = null, InlineStyle style = InlineSt public static class Markdown { + private static Dictionary _documentCache = new Dictionary(); + + public static void ClearDocumentCache() + { + _documentCache.Clear(); + } + // Entry point public static Document Parse(string input) { + ulong hash = ComputeFnv1AHash(input); + + if (_documentCache.TryGetValue(hash, out Document document)) + { + return document; + } + var text = Normalize(input); var pos = 0; var blocks = new List(); @@ -234,7 +248,11 @@ public static Document Parse(string input) // Paragraph (until blank line or next block) blocks.Add(Block.From(ParseParagraph(text, ref pos))); } - return new Document(blocks); + + document = new Document(blocks); + + _documentCache.Add(hash, document); + return document; } #region Block helpers @@ -293,7 +311,8 @@ private static bool TryParseBlockQuote(string text, ref int pos, out BlockQuote quote = default; if (!AtLineStart(text, pos)) return false; int i = pos; - var sb = new StringBuilder(); + var sb = _stringBuilder; + sb.Clear(); bool any = false; while (i < text.Length) { @@ -369,7 +388,8 @@ private static bool TryParseList(string text, ref int pos, out ListBlock list) // Gather any following indented lines as the item's continuation int j = NextLineStart(text, le); - var cont = new StringBuilder(); + var cont = _stringBuilder; + cont.Clear(); while (j < text.Length) { int le2 = LineEnd(text, j); @@ -623,6 +643,7 @@ private static List TokenizeInline(string text) return list; } + private static StringBuilder _stringBuilder = new StringBuilder(); private static List ApplyStyles(List tokens) { // Join Text runs first @@ -632,7 +653,8 @@ private static List ApplyStyles(List tokens) { if (t.Kind != InlineKind.Text) { output.Add(t); continue; } string s = t.Text; - var sb = new StringBuilder(); + var sb = _stringBuilder; + sb.Clear(); int i = 0; while (i < s.Length) { @@ -703,11 +725,26 @@ private static int IndexOfClosing(string s, char ch, int count, int from) return -1; } + private static Stack _inlinePool; + + private static Inline GetInlineFromPool() + { + if (_inlinePool.TryPop(out Inline inline)) + { + return inline; + } + + return new Inline(); + } + private static List CoalesceText(List list) { if (list.Count == 0) return list; + var res = new List(list.Count); - var sb = (StringBuilder)null; + var sb = _stringBuilder; + sb.Clear(); + void Flush() { if (sb != null && sb.Length > 0) { res.Add(Inline.TextRun(sb.ToString())); sb.Clear(); } @@ -716,7 +753,7 @@ void Flush() { if (it.Kind == InlineKind.Text) { - sb ??= new StringBuilder(); sb.Append(it.Text); + sb.Append(it.Text); } else { Flush(); res.Add(it); } } @@ -877,6 +914,20 @@ private static TableAlign[] ParseAligns(string underline) return arr; } + private static ulong ComputeFnv1AHash(string input) + { + ulong fnvOffsetBasis = 14695981039346656037UL; + ulong fnvPrime = 1099511628211UL; + + ulong hash = fnvOffsetBasis; + foreach (byte b in Encoding.UTF8.GetBytes(input)) + { + hash ^= b; + hash *= fnvPrime; + } + return hash; + } + #endregion } diff --git a/Scribe/Primitives.cs b/Scribe/Primitives.cs index 1080dfd..34315af 100644 --- a/Scribe/Primitives.cs +++ b/Scribe/Primitives.cs @@ -79,9 +79,12 @@ public struct TextLayoutSettings public TextWrapMode WrapMode; public TextAlignment Alignment; public float MaxWidth; // for wrapping, 0 = no limit + public List StyleSpans; + public MarkdownLayoutSettings LayoutSettings; - public Func FontSelector; // optional: index in the full string -> font - + private static List _pool = new List(); + private static int _currIdx = 0; + public static TextLayoutSettings Default => new TextLayoutSettings { PixelSize = 16, Font = null, @@ -91,8 +94,51 @@ public struct TextLayoutSettings TabSize = 4, WrapMode = TextWrapMode.NoWrap, Alignment = TextAlignment.Left, - MaxWidth = 0 + MaxWidth = 0, + StyleSpans = new List() }; + + private void SetDefaultValues() + { + PixelSize = 16; + Font = null; + LetterSpacing = 0; + WordSpacing = 0; + LineHeight = 1.0f; + TabSize = 4; + WrapMode = TextWrapMode.NoWrap; + Alignment = TextAlignment.Left; + MaxWidth = 0; + + if (StyleSpans == null) StyleSpans = new List(); + StyleSpans.Clear(); + } + + public static TextLayoutSettings Get() + { + TextLayoutSettings settings; + if (_currIdx == 0) + { + settings = new TextLayoutSettings(); + _pool.Add(settings); + } + else + { + settings = _pool[_currIdx]; + _currIdx--; + } + + settings.SetDefaultValues(); + return settings; + } + + public static void Return(TextLayoutSettings settings) + { + settings.StyleSpans.Clear(); + if(_currIdx+1 < _pool.Count) _currIdx++; + } + + } } public struct GlyphInstance @@ -103,6 +149,8 @@ public struct GlyphInstance public float AdvanceWidth; public int CharIndex; + private static Stack _pool = new Stack(); + public GlyphInstance(AtlasGlyph glyph, Vector2 position, char character, float advanceWidth, int charIndex) { Glyph = glyph; @@ -111,6 +159,27 @@ public GlyphInstance(AtlasGlyph glyph, Vector2 position, char character, float a AdvanceWidth = advanceWidth; CharIndex = charIndex; } + + public static GlyphInstance Get(AtlasGlyph glyph, Vector2 position, char character, float advanceWidth, int charIndex) + { + if (!_pool.TryPop(out GlyphInstance instance)) + { + instance = new GlyphInstance(glyph, position, character, advanceWidth, charIndex); + } + + instance.Glyph = glyph; + instance.Position = position; + instance.Character = character; + instance.AdvanceWidth = advanceWidth; + instance.CharIndex = charIndex; + + return instance; + } + + public static void Return(GlyphInstance instance) + { + _pool.Push(instance); + } } public struct Line @@ -121,7 +190,7 @@ public struct Line public Vector2 Position; // relative to layout origin public int StartIndex; // character index in original string public int EndIndex; // character index in original string - + public static Stack _pool = new Stack(); public Line(Vector2 position, int startIndex) { Glyphs = new List(); @@ -131,5 +200,28 @@ public Line(Vector2 position, int startIndex) StartIndex = startIndex; EndIndex = startIndex; } + + public static Line Get(Vector2 position, int startIndex) + { + if (!_pool.TryPop(out Line line)) + { + line = new Line(position, startIndex); + } + + line.Position = position; + line.StartIndex = startIndex; + line.EndIndex = startIndex; + return line; + } + + public static void Return(Line line) + { + foreach (GlyphInstance instance in line.Glyphs) + { + GlyphInstance.Return(instance); + } + line.Glyphs.Clear(); + _pool.Push(line); + } } } diff --git a/Scribe/TextLayout.cs b/Scribe/TextLayout.cs index a0bcc0e..f40083a 100644 --- a/Scribe/TextLayout.cs +++ b/Scribe/TextLayout.cs @@ -11,6 +11,31 @@ public class TextLayout public TextLayoutSettings Settings { get; private set; } public string Text { get; private set; } + private static Stack _pool = new Stack(); + + public static TextLayout Get() + { + if (_pool.TryPop(out TextLayout layout)) + { + return layout; + } + + return new TextLayout(); + } + + public static void Return(TextLayout layout) + { + foreach (Line line in layout.Lines) + { + Line.Return(line); + } + + layout.Lines.Clear(); + + TextLayoutSettings.Return(layout.Settings); + + _pool.Push(layout); + } public TextLayout() { Lines = new List(); @@ -33,6 +58,26 @@ internal void UpdateLayout(string text, TextLayoutSettings settings, FontSystem CalculateSize(); } + new Dictionary _ascenderCache = new Dictionary(8); + + float GetAscender(FontSystem fontSystem, FontFile font, float pixelSize) + { + if (_ascenderCache.TryGetValue(font, out var a)) return a; + fontSystem.GetScaledVMetrics(font, pixelSize, out var asc, out _, out _); + _ascenderCache[font] = asc; + return asc; + } + + // Local to place a single glyph (no cross-line kerning) + void EmitGlyph(AtlasGlyph glyph, FontSystem fontSystem, FontFile font, float pixelSize, char c, float offsetX, float offsetY, float advanceBase, ref float x, List outList, int charIndex, ref int lastCodepointForKerning) + { + float a = GetAscender(fontSystem, font, pixelSize); + var gi = GlyphInstance.Get(glyph, new Vector2(x + offsetX, offsetY + a), c, advanceBase, charIndex); + outList.Add(gi); + x += advanceBase; + lastCodepointForKerning = c; // kerning only continues within the current word/run + } + private void LayoutText(FontSystem fontSystem) { float currentX = 0f; @@ -40,9 +85,7 @@ private void LayoutText(FontSystem fontSystem) int i = 0; bool hasTrailingNewline = false; - Lines.Clear(); - - var line = new Line(new Vector2(0, currentY), 0); + var line = Line.Get(new Vector2(0, currentY), 0); // Hoist Settings & constants var text = Text; @@ -59,27 +102,8 @@ private void LayoutText(FontSystem fontSystem) // Kerning baseline: do NOT kern across whitespace int lastCodepointForKerning = 0; - - // Ascender cache per font object; we only need 'a' to place the glyph vertically - var ascenderCache = new Dictionary(8); - float GetAscender(FontFile font) - { - if (ascenderCache.TryGetValue(font, out var a)) return a; - fontSystem.GetScaledVMetrics(font, pixelSize, out var asc, out _, out _); - ascenderCache[font] = asc; - return asc; - } - - // Local to place a single glyph (no cross-line kerning) - void EmitGlyph(AtlasGlyph glyph, FontFile font, char c, float offsetX, float offsetY, float advanceBase, ref float x, List outList, int charIndex) - { - float a = GetAscender(font); - var gi = new GlyphInstance(glyph, new Vector2(x + offsetX, offsetY + a), c, advanceBase, charIndex); - outList.Add(gi); - x += advanceBase; - lastCodepointForKerning = c; // kerning only continues within the current word/run - } - + _ascenderCache.Clear(); + while (i < len) { char ch = text[i]; @@ -91,7 +115,7 @@ void EmitGlyph(AtlasGlyph glyph, FontFile font, char c, float offsetX, float off currentX = 0f; currentY += lineHeight; i++; - line = new Line(new Vector2(0, currentY), i); + line = Line.Get(new Vector2(0, currentY), i); lastCodepointForKerning = 0; hasTrailingNewline = true; continue; @@ -122,7 +146,7 @@ void EmitGlyph(AtlasGlyph glyph, FontFile font, char c, float offsetX, float off FinalizeLine(ref line, currentY, lineHeight, s, currentX); currentX = 0f; currentY += lineHeight; - line = new Line(new Vector2(0, currentY), i); + line = Line.Get(new Vector2(0, currentY), i); } else { @@ -151,9 +175,8 @@ void EmitGlyph(AtlasGlyph glyph, FontFile font, char c, float offsetX, float off { char c = text[j]; - FontFile font = Settings.Font; - if (Settings.FontSelector != null) - font = Settings.FontSelector(j); + FontFile font = ResolveFontForIndex(i, fontSystem, Settings.Font, Settings.StyleSpans, + Settings.LayoutSettings); var g = fontSystem.GetOrCreateGlyph(c, pixelSize, font); //var g = fontSystem.GetOrCreateGlyph(c, pixelSize, Settings.PreferredFont); @@ -199,15 +222,15 @@ void EmitGlyph(AtlasGlyph glyph, FontFile font, char c, float offsetX, float off FinalizeLine(ref line, currentY, lineHeight, wordStart, currentX); currentX = 0f; currentY += lineHeight; - line = new Line(new Vector2(0, currentY), wordStart); - lastCodepointForKerning = 0; // new line: no leading kerning + line = Line.Get(new Vector2(0, currentY), wordStart); + lastCodepointForKerning = 0; // Line.Get: no leading kerning } // If the word itself is too long for an empty line, split it (char-level) if (wordWidthNoLeadingKerning > maxWidth) { i = LayoutLongWordFast(fontSystem, ref line, ref currentX, ref currentY, lineHeight, - wordStart, wordEnd, tabWidth, spaceAdvance, wrapEnabled, maxWidth, GetAscender); + wordStart, wordEnd, tabWidth, spaceAdvance, wrapEnabled, maxWidth); lastCodepointForKerning = 0; continue; } @@ -225,10 +248,9 @@ void EmitGlyph(AtlasGlyph glyph, FontFile font, char c, float offsetX, float off for (int j = wordStart; j < wordEnd; j++) { char c = text[j]; - - FontFile font = Settings.Font; - if (Settings.FontSelector != null) - font = Settings.FontSelector(j); + + FontFile font = ResolveFontForIndex(j, fontSystem, Settings.Font, Settings.StyleSpans, + Settings.LayoutSettings); var g = fontSystem.GetOrCreateGlyph(c, pixelSize, font); //var g = fontSystem.GetOrCreateGlyph(c, pixelSize, Settings.PreferredFont); @@ -240,9 +262,9 @@ void EmitGlyph(AtlasGlyph glyph, FontFile font, char c, float offsetX, float off if (k != 0f) currentX += k; } - EmitGlyph(g, g.Font, c, g.Metrics.OffsetX, g.Metrics.OffsetY, + EmitGlyph(g, fontSystem, g.Font, pixelSize, c, g.Metrics.OffsetX, g.Metrics.OffsetY, g.Metrics.AdvanceWidth + Settings.LetterSpacing, - ref currentX, line.Glyphs, j); + ref currentX, line.Glyphs, j, ref lastCodepointForKerning); prevForKern = c; } @@ -257,6 +279,21 @@ void EmitGlyph(AtlasGlyph glyph, FontFile font, char c, float offsetX, float off FinalizeLine(ref line, currentY, lineHeight, i, currentX); } + private static FontFile ResolveFontForIndex(int idx, FontSystem fs, FontFile baseFont, List spans, MarkdownLayoutSettings settings) + { + bool bold = false, italic = false; + for (int i = 0; i < spans.Count; i++) + { + var s = spans[i]; + if (idx >= s.Start && idx < s.End) { bold |= s.Bold; italic |= s.Italic; if (bold && italic) break; } + } + + if (bold && italic) return settings.BoldItalicFont; + if (bold) return settings.BoldFont; + if (italic) return settings.ItalicFont; + return baseFont; + } + // Split a too-long word across lines, char by char, with minimal overhead. // Note: we do not kern across line starts; inside a run we keep kerning. private int LayoutLongWordFast( @@ -269,8 +306,7 @@ private int LayoutLongWordFast( float tabWidth, float spaceAdvance, bool wrapEnabled, - float maxWidth, - Func getAscender) + float maxWidth) { float pixelSize = Settings.PixelSize; @@ -280,9 +316,8 @@ private int LayoutLongWordFast( { char c = Text[i]; - FontFile font = Settings.Font; - if (Settings.FontSelector != null) - font = Settings.FontSelector(i); + FontFile font = ResolveFontForIndex(i, fontSystem, Settings.Font, Settings.StyleSpans, + Settings.LayoutSettings); var g = fontSystem.GetOrCreateGlyph(c, pixelSize, font); //var g = fontSystem.GetOrCreateGlyph(c, pixelSize, Settings.PreferredFont); @@ -300,7 +335,7 @@ private int LayoutLongWordFast( FinalizeLine(ref line, currentY, lineHeight, i, currentX); currentX = 0f; currentY += lineHeight; - line = new Line(new Vector2(0, currentY), i); + line = Line.Get(new Vector2(0, currentY), i); lastKernCode = 0; // break kerning across lines } else if (wrapEnabled && line.Glyphs.Count == 0 && currentX + k + adv > maxWidth) @@ -314,8 +349,8 @@ private int LayoutLongWordFast( } // Emit glyph - float a = getAscender(g.Font); - var gi = new GlyphInstance(g, new Vector2(currentX + g.Metrics.OffsetX, g.Metrics.OffsetY + a), c, adv, i); + float a = GetAscender(fontSystem, g.Font, pixelSize); + var gi = GlyphInstance.Get(g, new Vector2(currentX + g.Metrics.OffsetX, g.Metrics.OffsetY + a), c, adv, i); line.Glyphs.Add(gi); currentX += adv; lastKernCode = c;