using System; using System.Buffers; using System.Collections.Generic; using Avalonia.Media; using Avalonia.Skia; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; using SkiaSharp; namespace Avalonia.Benchmarks.Text; /// /// Compares obtaining glyph ink bounds via the table-based /// GlyphTypeface.TryGetGlyphBounds against Skia's /// SKFont.GetGlyphWidths(..., bounds) — the path GlyphRunImpl uses today. /// Both sides compute bounds only (Skia is passed a null widths array), so the /// comparison is like-for-like. Two scenario pairs isolate the pure bounds-read cost /// (font pre-created) from the realistic per-run cost (Skia pays SKFont creation, which /// the table path avoids entirely). /// [MemoryDiagnoser] public class GlyphBoundsBenchmark : IDisposable { private const float Size = 16f; private readonly IDisposable _app; private readonly GlyphTypeface _glyphTypeface; private readonly SkiaTypeface _skiaTypeface; private readonly SKFont _font; private readonly ushort[] _glyphPool; private ushort[] _glyphIndices = Array.Empty(); private SKRect[] _skBounds = Array.Empty(); private GlyphBounds[] _bounds = Array.Empty(); [Params(1, 16, 256)] public int GlyphCount { get; set; } public GlyphBoundsBenchmark() { _app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With( renderInterface: new PlatformRenderInterface(), fontManagerImpl: new FontManagerImpl())); _glyphTypeface = Typeface.Default.GlyphTypeface; _skiaTypeface = (SkiaTypeface)_glyphTypeface.PlatformTypeface; _font = _skiaTypeface.CreateSKFont(Size); var map = _glyphTypeface.CharacterToGlyphMap; var pool = new List(); for (var c = '!'; c <= '~'; c++) { if (map.ContainsGlyph(c)) { pool.Add(map[c]); } } _glyphPool = pool.ToArray(); } [GlobalSetup] public void Setup() { _glyphIndices = new ushort[GlyphCount]; for (var i = 0; i < GlyphCount; i++) { _glyphIndices[i] = _glyphPool[i % _glyphPool.Length]; } _skBounds = new SKRect[GlyphCount]; _bounds = new GlyphBounds[GlyphCount]; } // ---- Kernel: SKFont already created; measure only the bounds query. ---- [Benchmark(Baseline = true)] public void Skia_BoundsOnly() { _font.GetGlyphWidths(_glyphIndices, null, _skBounds.AsSpan(0, GlyphCount)); } [Benchmark] public void Table_BoundsOnly() { _glyphTypeface.TryGetGlyphBounds(_glyphIndices, _bounds.AsSpan(0, GlyphCount)); } // ---- Realistic per-run: Skia pays SKFont creation; the table path doesn't. ---- [Benchmark] public void Skia_PerRun() { using var font = _skiaTypeface.CreateSKFont(Size); var bounds = ArrayPool.Shared.Rent(GlyphCount); font.GetGlyphWidths(_glyphIndices, null, bounds.AsSpan(0, GlyphCount)); ArrayPool.Shared.Return(bounds); } [Benchmark] public void Table_PerRun() { Span bounds = GlyphCount <= 256 ? stackalloc GlyphBounds[GlyphCount] : new GlyphBounds[GlyphCount]; _glyphTypeface.TryGetGlyphBounds(_glyphIndices, bounds); } public void Dispose() { _font.Dispose(); _app?.Dispose(); } }