From 1555ba95b3b5acb6932d608ee78156afd327440a Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sat, 13 Sep 2025 22:59:43 +0200 Subject: [PATCH 01/24] removed allocations in the DrawText function --- Scribe/FontSystem.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Scribe/FontSystem.cs b/Scribe/FontSystem.cs index 3630278..5517031 100644 --- a/Scribe/FontSystem.cs +++ b/Scribe/FontSystem.cs @@ -457,12 +457,17 @@ 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,7 +492,12 @@ 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; } } From 934a61e45050c68e2f8f02578d00c417b291c9fd Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sat, 13 Sep 2025 23:11:19 +0200 Subject: [PATCH 02/24] pooling for text layouts --- Scribe/FontSystem.cs | 24 +++++++++++++++++++++--- Scribe/TextLayout.cs | 2 -- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Scribe/FontSystem.cs b/Scribe/FontSystem.cs index 5517031..28672c4 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 = GetTextLayoutFromPool(); empty.UpdateLayout(text, settings, this); return empty; } if (!CacheLayouts) { - var direct = new TextLayout(); + var direct = GetTextLayoutFromPool(); 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 = GetTextLayoutFromPool(); layout.UpdateLayout(text, settings, this); layoutCache.Add(key, layout); @@ -457,6 +457,17 @@ public void DrawText(string text, Vector2 position, FontColor color, TextLayoutS DrawLayout(layout, position, color); } + private Stack _textLayouts = new Stack(); + + private TextLayout GetTextLayoutFromPool() + { + if (_textLayouts.TryPop(out TextLayout layout)) + { + return layout; + } + + return new TextLayout(); + } List _vertices = new List(); List _indices = new List(); @@ -504,8 +515,15 @@ public void DrawLayout(TextLayout layout, Vector2 position, FontColor color) 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 + } + + _textLayouts.Push(layout); } #endregion diff --git a/Scribe/TextLayout.cs b/Scribe/TextLayout.cs index a0bcc0e..dda471b 100644 --- a/Scribe/TextLayout.cs +++ b/Scribe/TextLayout.cs @@ -40,8 +40,6 @@ private void LayoutText(FontSystem fontSystem) int i = 0; bool hasTrailingNewline = false; - Lines.Clear(); - var line = new Line(new Vector2(0, currentY), 0); // Hoist Settings & constants From 232517d0a0cf147907c9280febe5708dbf891942 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sat, 13 Sep 2025 23:17:05 +0200 Subject: [PATCH 03/24] use predefined arrays in image drawing --- Scribe/MarkdownLayoutEngine.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index dfe5d6b..aa97077 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -234,20 +234,21 @@ public static void Render(MarkdownDisplayList dl, FontSystem fontSystem, IFontRe } 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); } } } + 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) From 6ea5c768b3df300779f6d2e7a630c2839f0db168 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sat, 13 Sep 2025 23:27:13 +0200 Subject: [PATCH 04/24] use collection marshalling to reduce .ToArray() calls --- Scribe/MarkdownLayoutEngine.cs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index aa97077..3ec8121 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.InteropServices; namespace Prowl.Scribe { @@ -693,8 +694,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; @@ -751,16 +754,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. @@ -856,7 +869,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) From f7d6efc4754e705e11435d5189257f1e760a94cc Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sat, 13 Sep 2025 23:41:49 +0200 Subject: [PATCH 05/24] use a static string builder over a new one each frame --- Scribe/MarkdownParser.cs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Scribe/MarkdownParser.cs b/Scribe/MarkdownParser.cs index ff8f28f..8e9fb42 100644 --- a/Scribe/MarkdownParser.cs +++ b/Scribe/MarkdownParser.cs @@ -293,7 +293,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 +370,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 +625,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 +635,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 +707,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 +735,7 @@ void Flush() { if (it.Kind == InlineKind.Text) { - sb ??= new StringBuilder(); sb.Append(it.Text); + sb.Append(it.Text); } else { Flush(); res.Add(it); } } From 15c734a9f6a41660aad59f5c6e29348d8304db17 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sat, 13 Sep 2025 23:42:09 +0200 Subject: [PATCH 06/24] use static lists for markdown engine --- Scribe/FontSystem.cs | 1 - Scribe/MarkdownLayoutEngine.cs | 18 ++++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Scribe/FontSystem.cs b/Scribe/FontSystem.cs index 28672c4..49f6b51 100644 --- a/Scribe/FontSystem.cs +++ b/Scribe/FontSystem.cs @@ -520,7 +520,6 @@ public void DrawLayout(TextLayout layout, Vector2 position, FontColor color) #else renderer.DrawQuads(atlasTexture, vertices.ToArray(), indices.ToArray()); #endif - } _textLayouts.Push(layout); diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index 3ec8121..70a2d5a 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using System.Text; namespace Prowl.Scribe { @@ -607,12 +608,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) { From addd165856c5c2a4b40495169fd0ac7c1166e655 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sat, 13 Sep 2025 23:56:34 +0200 Subject: [PATCH 07/24] moved font selector to textlayout, not markdown layout engine --- Scribe/MarkdownLayoutEngine.cs | 25 ++++++++++++++++++------- Scribe/Primitives.cs | 5 ++++- Scribe/TextLayout.cs | 23 ++++++++++++++++++++--- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index 70a2d5a..2f0a034 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -7,6 +7,7 @@ using Prowl.Scribe.Internal; using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -313,7 +314,9 @@ private static float LayoutTextSegment(List inlines, float x, float y, M 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(); @@ -470,7 +473,8 @@ private static float LayoutCode(CodeBlock cb, float x, float y, MarkdownDisplayL 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]; + // float[] minCol = new float[cols]; + var minCol = ArrayPool.Shared.Rent(cols); float wAvail = widthOverride ?? settings.Width; // pass 1: min widths via NoWrap measure @@ -487,7 +491,9 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList 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); @@ -496,7 +502,8 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList // distribute to fit content width float totalMin = minCol.Sum(); - float[] colW = new float[cols]; + // float[] colW = new float[cols]; + var colW = ArrayPool.Shared.Rent(cols); if (totalMin <= wAvail) { float extra = wAvail - totalMin; @@ -510,13 +517,15 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList } // Precompute column x positions for grid lines - float[] colX = new float[cols + 1]; + // 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 = 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++) @@ -537,7 +546,9 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList 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); diff --git a/Scribe/Primitives.cs b/Scribe/Primitives.cs index 1080dfd..3846673 100644 --- a/Scribe/Primitives.cs +++ b/Scribe/Primitives.cs @@ -79,6 +79,8 @@ 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 @@ -91,7 +93,8 @@ public struct TextLayoutSettings TabSize = 4, WrapMode = TextWrapMode.NoWrap, Alignment = TextAlignment.Left, - MaxWidth = 0 + MaxWidth = 0, + StyleSpans = new List() }; } diff --git a/Scribe/TextLayout.cs b/Scribe/TextLayout.cs index dda471b..20bc19e 100644 --- a/Scribe/TextLayout.cs +++ b/Scribe/TextLayout.cs @@ -224,9 +224,11 @@ 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 = Settings.Font; + FontFile font = ResolveFontForIndex(j, fontSystem, Settings.Font, Settings.StyleSpans, + Settings.LayoutSettings); + // if (Settings.FontSelector != null) + // font = Settings.FontSelector(j); var g = fontSystem.GetOrCreateGlyph(c, pixelSize, font); //var g = fontSystem.GetOrCreateGlyph(c, pixelSize, Settings.PreferredFont); @@ -255,6 +257,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( From 147797d7663ed3a7079938fde88ee0233ac7331c Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 00:01:12 +0200 Subject: [PATCH 08/24] move get cols to a function, not delegate --- Scribe/MarkdownLayoutEngine.cs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index 2f0a034..fecb318 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -470,10 +470,20 @@ private static float LayoutCode(CodeBlock cb, float x, float y, MarkdownDisplayL 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; @@ -502,7 +512,6 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList // distribute to fit content width float totalMin = minCol.Sum(); - // float[] colW = new float[cols]; var colW = ArrayPool.Shared.Rent(cols); if (totalMin <= wAvail) { @@ -517,14 +526,12 @@ 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) @@ -590,7 +597,11 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList Color = settings.ColorRule }); } - + + ArrayPool.Shared.Return(minCol); + ArrayPool.Shared.Return(colW); + ArrayPool.Shared.Return(colX); + ArrayPool.Shared.Return(perRowHeights); return tableBottom + settings.ParagraphSpacing; } From 45129ce4bc80fa392f76e80d1eb6330ce0d5f15f Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 00:10:49 +0200 Subject: [PATCH 09/24] remove delegate creation inside of the layout text function --- Scribe/TextLayout.cs | 59 ++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/Scribe/TextLayout.cs b/Scribe/TextLayout.cs index 20bc19e..5f3865f 100644 --- a/Scribe/TextLayout.cs +++ b/Scribe/TextLayout.cs @@ -10,10 +10,13 @@ public class TextLayout public Vector2 Size { get; private set; } public TextLayoutSettings Settings { get; private set; } public string Text { get; private set; } + + private static int _createdLayouts = 0; public TextLayout() { Lines = new List(); + _createdLayouts++; } internal void UpdateLayout(string text, TextLayoutSettings settings, FontSystem fontSystem) @@ -33,6 +36,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 = 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 + } + private void LayoutText(FontSystem fontSystem) { float currentX = 0f; @@ -59,24 +82,6 @@ private void LayoutText(FontSystem fontSystem) 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 - } while (i < len) { @@ -205,7 +210,7 @@ void EmitGlyph(AtlasGlyph glyph, FontFile font, char c, float offsetX, float off 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; } @@ -240,9 +245,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; } @@ -284,8 +289,7 @@ private int LayoutLongWordFast( float tabWidth, float spaceAdvance, bool wrapEnabled, - float maxWidth, - Func getAscender) + float maxWidth) { float pixelSize = Settings.PixelSize; @@ -295,9 +299,10 @@ 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); + // if (Settings.FontSelector != null) + // font = Settings.FontSelector(i); var g = fontSystem.GetOrCreateGlyph(c, pixelSize, font); //var g = fontSystem.GetOrCreateGlyph(c, pixelSize, Settings.PreferredFont); @@ -329,7 +334,7 @@ private int LayoutLongWordFast( } // Emit glyph - float a = getAscender(g.Font); + float a = GetAscender(fontSystem, g.Font, pixelSize); var gi = new GlyphInstance(g, new Vector2(currentX + g.Metrics.OffsetX, g.Metrics.OffsetY + a), c, adv, i); line.Glyphs.Add(gi); currentX += adv; From aa4876c8b77deb433c79110f3f9f20a09338da8e Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 00:13:57 +0200 Subject: [PATCH 10/24] cleaned up text layout --- Scribe/TextLayout.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Scribe/TextLayout.cs b/Scribe/TextLayout.cs index 5f3865f..a70c2b0 100644 --- a/Scribe/TextLayout.cs +++ b/Scribe/TextLayout.cs @@ -80,9 +80,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 - + _ascenderCache.Clear(); + while (i < len) { char ch = text[i]; @@ -154,9 +153,8 @@ private void LayoutText(FontSystem fontSystem) { 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); @@ -228,12 +226,9 @@ private void LayoutText(FontSystem fontSystem) for (int j = wordStart; j < wordEnd; j++) { char c = text[j]; - - // FontFile font = Settings.Font; + FontFile font = ResolveFontForIndex(j, fontSystem, Settings.Font, Settings.StyleSpans, Settings.LayoutSettings); - // if (Settings.FontSelector != null) - // font = Settings.FontSelector(j); var g = fontSystem.GetOrCreateGlyph(c, pixelSize, font); //var g = fontSystem.GetOrCreateGlyph(c, pixelSize, Settings.PreferredFont); @@ -301,8 +296,6 @@ private int LayoutLongWordFast( FontFile font = ResolveFontForIndex(i, fontSystem, Settings.Font, Settings.StyleSpans, Settings.LayoutSettings); - // if (Settings.FontSelector != null) - // font = Settings.FontSelector(i); var g = fontSystem.GetOrCreateGlyph(c, pixelSize, font); //var g = fontSystem.GetOrCreateGlyph(c, pixelSize, Settings.PreferredFont); From 2e5e21bd12079554970f00e0772b571123ef7310 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 13:38:09 +0200 Subject: [PATCH 11/24] implemented pooling for lines, textlayout, and glyphinstances --- Scribe/FontSystem.cs | 20 ++-------- Scribe/MarkdownLayoutEngine.cs | 3 ++ Scribe/Primitives.cs | 70 +++++++++++++++++++++++++++++++++- Scribe/TextLayout.cs | 39 ++++++++++++++----- 4 files changed, 105 insertions(+), 27 deletions(-) diff --git a/Scribe/FontSystem.cs b/Scribe/FontSystem.cs index 49f6b51..15faaee 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 = GetTextLayoutFromPool(); + var empty = TextLayout.Get(); empty.UpdateLayout(text, settings, this); return empty; } if (!CacheLayouts) { - var direct = GetTextLayoutFromPool(); + 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 = GetTextLayoutFromPool(); + var layout = TextLayout.Get(); layout.UpdateLayout(text, settings, this); layoutCache.Add(key, layout); @@ -457,18 +457,6 @@ public void DrawText(string text, Vector2 position, FontColor color, TextLayoutS DrawLayout(layout, position, color); } - private Stack _textLayouts = new Stack(); - - private TextLayout GetTextLayoutFromPool() - { - if (_textLayouts.TryPop(out TextLayout layout)) - { - return layout; - } - - return new TextLayout(); - } - List _vertices = new List(); List _indices = new List(); public void DrawLayout(TextLayout layout, Vector2 position, FontColor color) @@ -522,7 +510,7 @@ public void DrawLayout(TextLayout layout, Vector2 position, FontColor color) #endif } - _textLayouts.Push(layout); + TextLayout.Return(layout); } #endregion diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index fecb318..c68c2ea 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -247,6 +247,8 @@ public static void Render(MarkdownDisplayList dl, FontSystem fontSystem, IFontRe renderer.DrawQuads(img.Texture, _vertsImg, _idxImg); } } + + GlyphInstance.ResetLayoutCount(); } static IFontRenderer.Vertex[] _vertsImg = new IFontRenderer.Vertex[4]; @@ -507,6 +509,7 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList var tl = fontSystem.CreateLayout(text, tls); minCol[c] = MathF.Max(minCol[c], tl.Size.X); + TextLayout.Return(tl); } } diff --git a/Scribe/Primitives.cs b/Scribe/Primitives.cs index 3846673..a9eb063 100644 --- a/Scribe/Primitives.cs +++ b/Scribe/Primitives.cs @@ -106,6 +106,10 @@ public struct GlyphInstance public float AdvanceWidth; public int CharIndex; + private static Stack _pool = new Stack(); + private static int _created = 0; + private static int _returned = 0; + public GlyphInstance(AtlasGlyph glyph, Vector2 position, char character, float advanceWidth, int charIndex) { Glyph = glyph; @@ -114,6 +118,37 @@ 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) + { + // Console.WriteLine($"Num of outstanding glyphs: {_pool.Count}"); + if (!_pool.TryPop(out GlyphInstance instance)) + { + instance = new GlyphInstance(glyph, position, character, advanceWidth, charIndex); + _created++; + } + + instance.Glyph = glyph; + instance.Position = position; + instance.Character = character; + instance.AdvanceWidth = advanceWidth; + instance.CharIndex = charIndex; + + return instance; + } + + public static void Return(GlyphInstance instance) + { + // Console.WriteLine($"Returning instance to the pool. Size: {_pool.Count + 1}"); + _pool.Push(instance); + _returned++; + } + + public static void ResetLayoutCount() + { + _returned = 0; + _created = 0; + } } public struct Line @@ -124,7 +159,9 @@ 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 static int _created = 0; + public static int _returned = 0; public Line(Vector2 position, int startIndex) { Glyphs = new List(); @@ -134,5 +171,36 @@ 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); + _created++; + } + + 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); + _returned++; + } + + public static void ResetCounters() + { + _returned = 0; + _created = 0; + } } } diff --git a/Scribe/TextLayout.cs b/Scribe/TextLayout.cs index a70c2b0..1742863 100644 --- a/Scribe/TextLayout.cs +++ b/Scribe/TextLayout.cs @@ -10,13 +10,32 @@ public class TextLayout public Vector2 Size { get; private set; } public TextLayoutSettings Settings { get; private set; } public string Text { get; private set; } + + private static Stack _pool = new Stack(); - private static int _createdLayouts = 0; + 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(); + _pool.Push(layout); + } public TextLayout() { Lines = new List(); - _createdLayouts++; } internal void UpdateLayout(string text, TextLayoutSettings settings, FontSystem fontSystem) @@ -50,7 +69,7 @@ float GetAscender(FontSystem fontSystem, FontFile font, float pixelSize) 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 = new GlyphInstance(glyph, new Vector2(x + offsetX, offsetY + a), c, advanceBase, charIndex); + 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 @@ -63,7 +82,7 @@ private void LayoutText(FontSystem fontSystem) int i = 0; bool hasTrailingNewline = false; - var line = new Line(new Vector2(0, currentY), 0); + var line = Line.Get(new Vector2(0, currentY), 0); // Hoist Settings & constants var text = Text; @@ -93,7 +112,7 @@ private void LayoutText(FontSystem fontSystem) 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; @@ -124,7 +143,7 @@ private void LayoutText(FontSystem fontSystem) 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 { @@ -200,8 +219,8 @@ private void LayoutText(FontSystem fontSystem) 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) @@ -313,7 +332,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) @@ -328,7 +347,7 @@ private int LayoutLongWordFast( // Emit glyph float a = GetAscender(fontSystem, g.Font, pixelSize); - var gi = new GlyphInstance(g, new Vector2(currentX + g.Metrics.OffsetX, g.Metrics.OffsetY + a), c, adv, i); + 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; From b5115f89b314d0e7f73a0050b8660a6c065595fa Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 14:08:17 +0200 Subject: [PATCH 12/24] first object pooling for markdown draw commands --- Scribe/MarkdownLayoutEngine.cs | 139 ++++++++++++++++++++++++++------- Scribe/Primitives.cs | 10 --- 2 files changed, 111 insertions(+), 38 deletions(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index c68c2ea..3fd3679 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -10,6 +10,7 @@ using System.Buffers; using System.Collections.Generic; using System.Linq; +using System.Net.Mime; using System.Numerics; using System.Runtime.InteropServices; using System.Text; @@ -46,18 +47,103 @@ public struct DrawText : IDrawOp 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 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 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; } } @@ -199,7 +285,7 @@ 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; @@ -215,6 +301,7 @@ 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); } } @@ -234,6 +321,8 @@ 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) { @@ -245,10 +334,10 @@ public static void Render(MarkdownDisplayList dl, FontSystem fontSystem, IFontRe _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); } } - - GlyphInstance.ResetLayoutCount(); } static IFontRenderer.Vertex[] _vertsImg = new IFontRenderer.Vertex[4]; @@ -321,10 +410,13 @@ private static float LayoutTextSegment(List inlines, float x, float y, M // 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); @@ -344,7 +436,7 @@ 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 @@ -375,10 +467,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; } @@ -399,7 +488,7 @@ 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 { @@ -412,7 +501,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 @@ -445,7 +534,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; } @@ -467,8 +556,8 @@ 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; } @@ -561,10 +650,10 @@ private static float LayoutTable(Table t, float x, float y, MarkdownDisplayList // 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); @@ -585,20 +674,14 @@ 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); diff --git a/Scribe/Primitives.cs b/Scribe/Primitives.cs index a9eb063..a9d45bc 100644 --- a/Scribe/Primitives.cs +++ b/Scribe/Primitives.cs @@ -160,8 +160,6 @@ public struct Line public int StartIndex; // character index in original string public int EndIndex; // character index in original string public static Stack _pool = new Stack(); - public static int _created = 0; - public static int _returned = 0; public Line(Vector2 position, int startIndex) { Glyphs = new List(); @@ -177,7 +175,6 @@ public static Line Get(Vector2 position, int startIndex) if (!_pool.TryPop(out Line line)) { line = new Line(position, startIndex); - _created++; } line.Position = position; @@ -194,13 +191,6 @@ public static void Return(Line line) } line.Glyphs.Clear(); _pool.Push(line); - _returned++; - } - - public static void ResetCounters() - { - _returned = 0; - _created = 0; } } } From bc67a96d3de30891bf7c06889bd2b1a048cec701 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 14:20:24 +0200 Subject: [PATCH 13/24] changed drawcommands to classes to remove allocations The structs were already being allocated to the heap, but as a struct, the object pool wasn't working correctly, so it was still allocating to the heap. By changing it to a class, it fixes this issue. --- Scribe/MarkdownLayoutEngine.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index 3fd3679..a126e0b 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -40,7 +40,7 @@ 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; @@ -95,7 +95,7 @@ public static void ResetCounters() } } - public struct DrawQuad : IDrawOp + public class DrawQuad : IDrawOp { public RectangleF Rect; public FontColor Color; @@ -120,7 +120,7 @@ public static void Return(DrawQuad text) } } - public struct DrawImage : IDrawOp + public class DrawImage : IDrawOp { public RectangleF Rect; public object Texture; @@ -291,8 +291,10 @@ public static void Render(MarkdownDisplayList dl, FontSystem fontSystem, IFontRe 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) @@ -307,7 +309,11 @@ public static void Render(MarkdownDisplayList dl, FontSystem fontSystem, IFontRe 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 From cf4b61892ea92f97beda989c4a12454a517a5226 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 14:24:32 +0200 Subject: [PATCH 14/24] use array pool when assigning link hitboxes --- Scribe/MarkdownLayoutEngine.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index a126e0b..f1bf71c 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -1016,7 +1016,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; @@ -1044,6 +1044,8 @@ private static void AddLinkHitBoxes(MarkdownDisplayList dl, DrawText t, List.Shared.Return(g2t); } } From e996ae08e1d035aa1cba2fa29311b912344a2c19 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 14:37:20 +0200 Subject: [PATCH 15/24] use array pool for bitmap rasterization --- Scribe/Internal/Bitmap.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Scribe/Internal/Bitmap.cs b/Scribe/Internal/Bitmap.cs index 701ca4e..0ebfb40 100644 --- a/Scribe/Internal/Bitmap.cs +++ b/Scribe/Internal/Bitmap.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Numerics; using static Prowl.Scribe.Internal.Common; @@ -327,8 +328,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] = new Edge(); int n = 0; // number of produced edges int m = 0; // running index into pts per winding @@ -368,6 +370,8 @@ private void RasterizeContours(Vector2[] pts, int[] wcount, int windings, float SortEdgesInsSort(edgePtr, n); RasterizeSortedEdges(edgePtr, n, vsubsample, offX, offY); + + ArrayPool.Shared.Return(edges); } private Vector2[] FlattenCurves(GlyphVertex[] vertices, int numVerts, float objspaceFlatness, out int[] contourLengths, out int numContours) From 6ecbfbf58f6e95e666b74185357d88ce3f3abec3 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 14:37:41 +0200 Subject: [PATCH 16/24] use object pooling for TextLayoutSettings --- Scribe/FontSystem.cs | 4 +-- Scribe/MarkdownLayoutEngine.cs | 10 +++---- Scribe/Primitives.cs | 49 ++++++++++++++++++++++++---------- Scribe/TextLayout.cs | 3 +++ 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Scribe/FontSystem.cs b/Scribe/FontSystem.cs index 15faaee..f2b5560 100644 --- a/Scribe/FontSystem.cs +++ b/Scribe/FontSystem.cs @@ -423,7 +423,7 @@ 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; @@ -440,7 +440,7 @@ 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; diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index f1bf71c..ad95386 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -404,7 +404,7 @@ private static float LayoutTextSegment(List inlines, float x, float y, M var (text, decos, linkSpans, styles) = FlattenInlines(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; @@ -499,7 +499,7 @@ private static float LayoutList(ListBlock list, float x, float y, int depth, Mar 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; @@ -552,7 +552,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; @@ -591,7 +591,7 @@ 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; @@ -644,7 +644,7 @@ 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; diff --git a/Scribe/Primitives.cs b/Scribe/Primitives.cs index a9d45bc..c120d9d 100644 --- a/Scribe/Primitives.cs +++ b/Scribe/Primitives.cs @@ -82,8 +82,8 @@ public struct TextLayoutSettings public List StyleSpans; public MarkdownLayoutSettings LayoutSettings; - public Func FontSelector; // optional: index in the full string -> font - + private static Stack _pool = new Stack(); + public static TextLayoutSettings Default => new TextLayoutSettings { PixelSize = 16, Font = null, @@ -96,6 +96,39 @@ public struct TextLayoutSettings 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() + { + if (!_pool.TryPop(out TextLayoutSettings settings)) + { + settings = new TextLayoutSettings(); + } + + settings.SetDefaultValues(); + return settings; + } + + public static void Return(TextLayoutSettings settings) + { + settings.StyleSpans.Clear(); + _pool.Push(settings); + } } public struct GlyphInstance @@ -107,8 +140,6 @@ public struct GlyphInstance public int CharIndex; private static Stack _pool = new Stack(); - private static int _created = 0; - private static int _returned = 0; public GlyphInstance(AtlasGlyph glyph, Vector2 position, char character, float advanceWidth, int charIndex) { @@ -121,11 +152,9 @@ public GlyphInstance(AtlasGlyph glyph, Vector2 position, char character, float a public static GlyphInstance Get(AtlasGlyph glyph, Vector2 position, char character, float advanceWidth, int charIndex) { - // Console.WriteLine($"Num of outstanding glyphs: {_pool.Count}"); if (!_pool.TryPop(out GlyphInstance instance)) { instance = new GlyphInstance(glyph, position, character, advanceWidth, charIndex); - _created++; } instance.Glyph = glyph; @@ -139,15 +168,7 @@ public static GlyphInstance Get(AtlasGlyph glyph, Vector2 position, char charact public static void Return(GlyphInstance instance) { - // Console.WriteLine($"Returning instance to the pool. Size: {_pool.Count + 1}"); _pool.Push(instance); - _returned++; - } - - public static void ResetLayoutCount() - { - _returned = 0; - _created = 0; } } diff --git a/Scribe/TextLayout.cs b/Scribe/TextLayout.cs index 1742863..f40083a 100644 --- a/Scribe/TextLayout.cs +++ b/Scribe/TextLayout.cs @@ -31,6 +31,9 @@ public static void Return(TextLayout layout) } layout.Lines.Clear(); + + TextLayoutSettings.Return(layout.Settings); + _pool.Push(layout); } public TextLayout() From d48e80f8e2091aecf20681bd53abb5b8803f45fe Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 15:03:05 +0200 Subject: [PATCH 17/24] implemented a document cache for markdown parsing --- Scribe/MarkdownLayoutEngine.cs | 1 + Scribe/MarkdownParser.cs | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index ad95386..90885f1 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -283,6 +283,7 @@ public static MarkdownDisplayList Layout(Document doc, FontSystem fontSystem, Ma } dl.Size = new Vector2(settings.Width, cursorY); + return dl; } diff --git a/Scribe/MarkdownParser.cs b/Scribe/MarkdownParser.cs index 8e9fb42..661a074 100644 --- a/Scribe/MarkdownParser.cs +++ b/Scribe/MarkdownParser.cs @@ -212,9 +212,37 @@ private Inline(InlineKind kind, string text = null, InlineStyle style = InlineSt public static class Markdown { + private static Dictionary _documentCache = new Dictionary(); // Entry point + + + public static void ClearDocumentCache() + { + _documentCache.Clear(); + } + private static ulong ComputeFNV1aHash(string input) + { + const ulong FNVOffsetBasis = 14695981039346656037UL; + const ulong FNVPrime = 1099511628211UL; + + ulong hash = FNVOffsetBasis; + foreach (byte b in System.Text.Encoding.UTF8.GetBytes(input)) + { + hash ^= b; + hash *= FNVPrime; + } + return hash; + } + 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 +262,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 From 642b65fb882c6f5b570cf59b22b6a05dd6db9637 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 15:04:53 +0200 Subject: [PATCH 18/24] removed some unneeded lines --- Scribe/MarkdownParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scribe/MarkdownParser.cs b/Scribe/MarkdownParser.cs index 661a074..cbe0c2b 100644 --- a/Scribe/MarkdownParser.cs +++ b/Scribe/MarkdownParser.cs @@ -226,7 +226,7 @@ private static ulong ComputeFNV1aHash(string input) const ulong FNVPrime = 1099511628211UL; ulong hash = FNVOffsetBasis; - foreach (byte b in System.Text.Encoding.UTF8.GetBytes(input)) + foreach (byte b in Encoding.UTF8.GetBytes(input)) { hash ^= b; hash *= FNVPrime; From 91a7fb151888d87e2174817ef84a33cf1b8c6e02 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 15:15:54 +0200 Subject: [PATCH 19/24] using pooling for inline lists in markdown layout --- Scribe/MarkdownLayoutEngine.cs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index 90885f1..d333273 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -376,7 +376,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) @@ -399,11 +399,29 @@ 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.Get(); tls.PixelSize = sizeOverride ?? settings.BaseSize; @@ -447,7 +465,9 @@ private static float LayoutImage(Inline img, float x, float y, MarkdownDisplayLi return y + h; } // fallback to alt text - var alt = new List { Inline.TextRun(img.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); } From c47e622ff543fb287c52b94821ddeec24b8b3bc1 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 15:17:09 +0200 Subject: [PATCH 20/24] small cleanup for markdown parser --- Scribe/MarkdownParser.cs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Scribe/MarkdownParser.cs b/Scribe/MarkdownParser.cs index cbe0c2b..a8e39dc 100644 --- a/Scribe/MarkdownParser.cs +++ b/Scribe/MarkdownParser.cs @@ -213,30 +213,16 @@ private Inline(InlineKind kind, string text = null, InlineStyle style = InlineSt public static class Markdown { private static Dictionary _documentCache = new Dictionary(); - // Entry point - public static void ClearDocumentCache() { _documentCache.Clear(); } - private static ulong ComputeFNV1aHash(string input) - { - const ulong FNVOffsetBasis = 14695981039346656037UL; - const ulong FNVPrime = 1099511628211UL; - ulong hash = FNVOffsetBasis; - foreach (byte b in Encoding.UTF8.GetBytes(input)) - { - hash ^= b; - hash *= FNVPrime; - } - return hash; - } - + // Entry point public static Document Parse(string input) { - ulong hash = ComputeFNV1aHash(input); + ulong hash = ComputeFnv1AHash(input); if (_documentCache.TryGetValue(hash, out Document document)) { @@ -928,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 } From 2125b26b266819835d963a4d5901c4397537da43 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 15:17:38 +0200 Subject: [PATCH 21/24] removed commented line --- Scribe/MarkdownLayoutEngine.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index d333273..2571b6b 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -465,7 +465,6 @@ private static float LayoutImage(Inline img, float x, float y, MarkdownDisplayLi 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); From d542af915a81680b9b9281b9cca8aed2d4bf3d37 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 15:22:51 +0200 Subject: [PATCH 22/24] use object pooling for MarkdownDisplayList --- Scribe/MarkdownLayoutEngine.cs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Scribe/MarkdownLayoutEngine.cs b/Scribe/MarkdownLayoutEngine.cs index 2571b6b..7bea543 100644 --- a/Scribe/MarkdownLayoutEngine.cs +++ b/Scribe/MarkdownLayoutEngine.cs @@ -236,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 @@ -246,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; @@ -345,6 +364,8 @@ public static void Render(MarkdownDisplayList dl, FontSystem fontSystem, IFontRe DrawImage.Return(img); } } + + MarkdownDisplayList.Return(dl); } static IFontRenderer.Vertex[] _vertsImg = new IFontRenderer.Vertex[4]; From 70de99b627fd73b0ab8cf43433394d314c120c37 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Sun, 14 Sep 2025 15:55:25 +0200 Subject: [PATCH 23/24] use object pooling for active edges --- Scribe/Internal/Bitmap.cs | 69 +++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/Scribe/Internal/Bitmap.cs b/Scribe/Internal/Bitmap.cs index 0ebfb40..1ef974d 100644 --- a/Scribe/Internal/Bitmap.cs +++ b/Scribe/Internal/Bitmap.cs @@ -1,5 +1,7 @@ using System; using System.Buffers; +using System.Collections; +using System.Collections.Generic; using System.Numerics; using static Prowl.Scribe.Internal.Common; @@ -12,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, @@ -214,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 @@ -300,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; @@ -330,7 +342,7 @@ private void RasterizeContours(Vector2[] pts, int[] wcount, int windings, float // +1 sentinel edge int edgesLength = totalEdges + 1; var edges = ArrayPool.Shared.Rent(edgesLength); - for (int i = 0; i < edgesLength; ++i) edges[i] = new Edge(); + 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 @@ -371,7 +383,12 @@ private void RasterizeContours(Vector2[] pts, int[] wcount, int windings, float 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) @@ -616,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 @@ -623,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); + } } } } From 087375c057edce0df9c0ee3402952556bb053a84 Mon Sep 17 00:00:00 2001 From: Trey Ramm Date: Fri, 26 Sep 2025 22:56:30 +0200 Subject: [PATCH 24/24] fix: made text layout settings a list to remove memory leak --- Scribe/FontSystem.cs | 3 +++ Scribe/Primitives.cs | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Scribe/FontSystem.cs b/Scribe/FontSystem.cs index f2b5560..4245033 100644 --- a/Scribe/FontSystem.cs +++ b/Scribe/FontSystem.cs @@ -429,6 +429,7 @@ public Vector2 MeasureText(string text, float pixelSize, FontFile font, float le settings.LetterSpacing = letterSpacing; var layout = CreateLayout(text, settings); + TextLayoutSettings.Return(settings); return layout.Size; } @@ -446,6 +447,8 @@ public void DrawText(string text, Vector2 position, FontColor color, float pixel settings.LetterSpacing = letterSpacing; DrawText(text, position, color, settings); + + TextLayoutSettings.Return(settings); } diff --git a/Scribe/Primitives.cs b/Scribe/Primitives.cs index c120d9d..34315af 100644 --- a/Scribe/Primitives.cs +++ b/Scribe/Primitives.cs @@ -82,7 +82,8 @@ public struct TextLayoutSettings public List StyleSpans; public MarkdownLayoutSettings LayoutSettings; - private static Stack _pool = new Stack(); + private static List _pool = new List(); + private static int _currIdx = 0; public static TextLayoutSettings Default => new TextLayoutSettings { PixelSize = 16, @@ -115,9 +116,16 @@ private void SetDefaultValues() public static TextLayoutSettings Get() { - if (!_pool.TryPop(out TextLayoutSettings settings)) + TextLayoutSettings settings; + if (_currIdx == 0) { settings = new TextLayoutSettings(); + _pool.Add(settings); + } + else + { + settings = _pool[_currIdx]; + _currIdx--; } settings.SetDefaultValues(); @@ -127,7 +135,9 @@ public static TextLayoutSettings Get() public static void Return(TextLayoutSettings settings) { settings.StyleSpans.Clear(); - _pool.Push(settings); + if(_currIdx+1 < _pool.Count) _currIdx++; + } + } }