diff --git a/binding/Binding.Shared/HashCode.cs b/binding/Binding.Shared/HashCode.cs index 4fe4c85c12..50ef82cf31 100644 --- a/binding/Binding.Shared/HashCode.cs +++ b/binding/Binding.Shared/HashCode.cs @@ -145,5 +145,186 @@ public int ToHashCode () hash = MixFinal (hash); return (int)hash; } + + public static int Combine (T1 value1) + { + // Provide a way of diffusing bits from something with a limited + // input hash space. For example, many enums only have a few + // possible hashes, only using the bottom few bits of the code. Some + // collections are built on the assumption that hashes are spread + // over a larger space, so diffusing the bits may help the + // collection work more efficiently. + + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + + uint hash = MixEmptyState (); + hash += 4; + + hash = QueueRound (hash, hc1); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + + uint hash = MixEmptyState (); + hash += 8; + + hash = QueueRound (hash, hc1); + hash = QueueRound (hash, hc2); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + + uint hash = MixEmptyState (); + hash += 12; + + hash = QueueRound (hash, hc1); + hash = QueueRound (hash, hc2); + hash = QueueRound (hash, hc3); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + uint hash = MixState (v1, v2, v3, v4); + hash += 16; + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + uint hc5 = (uint)(value5?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + uint hash = MixState (v1, v2, v3, v4); + hash += 20; + + hash = QueueRound (hash, hc5); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + uint hc5 = (uint)(value5?.GetHashCode () ?? 0); + uint hc6 = (uint)(value6?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + uint hash = MixState (v1, v2, v3, v4); + hash += 24; + + hash = QueueRound (hash, hc5); + hash = QueueRound (hash, hc6); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + uint hc5 = (uint)(value5?.GetHashCode () ?? 0); + uint hc6 = (uint)(value6?.GetHashCode () ?? 0); + uint hc7 = (uint)(value7?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + uint hash = MixState (v1, v2, v3, v4); + hash += 28; + + hash = QueueRound (hash, hc5); + hash = QueueRound (hash, hc6); + hash = QueueRound (hash, hc7); + + hash = MixFinal (hash); + return (int)hash; + } + + public static int Combine (T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + uint hc1 = (uint)(value1?.GetHashCode () ?? 0); + uint hc2 = (uint)(value2?.GetHashCode () ?? 0); + uint hc3 = (uint)(value3?.GetHashCode () ?? 0); + uint hc4 = (uint)(value4?.GetHashCode () ?? 0); + uint hc5 = (uint)(value5?.GetHashCode () ?? 0); + uint hc6 = (uint)(value6?.GetHashCode () ?? 0); + uint hc7 = (uint)(value7?.GetHashCode () ?? 0); + uint hc8 = (uint)(value8?.GetHashCode () ?? 0); + + Initialize (out uint v1, out uint v2, out uint v3, out uint v4); + + v1 = Round (v1, hc1); + v2 = Round (v2, hc2); + v3 = Round (v3, hc3); + v4 = Round (v4, hc4); + + v1 = Round (v1, hc5); + v2 = Round (v2, hc6); + v3 = Round (v3, hc7); + v4 = Round (v4, hc8); + + uint hash = MixState (v1, v2, v3, v4); + hash += 32; + + hash = MixFinal (hash); + return (int)hash; + } } } diff --git a/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs b/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs index 8b39ae75d9..2a9d193440 100644 --- a/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs +++ b/source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; namespace SkiaSharp.HarfBuzz { @@ -30,8 +33,10 @@ public static void DrawShapedText(this SKCanvas canvas, string text, float x, fl if (string.IsNullOrEmpty(text)) return; - using var shaper = new SKShaper(font.Typeface); + var shaper = GetShaper(font.Typeface); canvas.DrawShapedText(shaper, text, x, y, textAlign, font, paint); + if (cacheDuration == 0) + shaper.Dispose(); } [Obsolete("Use DrawShapedText(SKShaper shaper, string text, SKPoint p, SKTextAlign textAlign, SKFont font, SKPaint paint) instead.")] @@ -70,7 +75,7 @@ public static void DrawShapedText(this SKCanvas canvas, SKShaper shaper, string font.Typeface = shaper.Typeface; // shape the text - var result = shaper.Shape(text, x, y, font); + var result = GetShapeResult(shaper, text, font); // create the text blob using var builder = new SKTextBlobBuilder(); @@ -99,7 +104,95 @@ public static void DrawShapedText(this SKCanvas canvas, SKShaper shaper, string } // draw the text - canvas.DrawText(textBlob, xOffset, 0, paint); + canvas.DrawText(textBlob, x + xOffset, y, paint); + + if (clearCacheTimer is null && cacheDuration > 0) + clearCacheTimer = new Timer(_ => ClearCache(), null, 0, cacheDuration); + } + + private static uint cacheDuration = 0; + + public static void SetShaperCacheDuration(this SKCanvas canvas, uint milliseconds) => SetShaperCacheDuration(milliseconds); + public static void SetShaperCacheDuration(uint milliseconds) + { + cacheDuration = milliseconds; + + clearCacheTimer?.Change(0, cacheDuration); + } + + private static readonly Dictionary shaperCache = new(); + private static readonly Dictionary shapeResultCache = new(); + + private static SKShaper GetShaper(SKTypeface typeface) + { + if (cacheDuration == 0) + return new SKShaper(typeface); + + var key = HashCode.Combine(typeface.FamilyName, typeface.IsBold, typeface.IsItalic); + + SKShaper shaper; + lock (shaperCache) + { + shaper = shaperCache.TryGetValue(key, out var value) + ? value.shaper + : new SKShaper(typeface); + + shaperCache[key] = (shaper, DateTime.Now); // update timestamp + } + return shaper; + } + + private static SKShaper.Result GetShapeResult(SKShaper shaper, string text, SKFont font) + { + if (cacheDuration == 0) + return shaper.Shape(text, 0, 0, font); + + var key = HashCode.Combine(font.Typeface.FamilyName, font.Size, font.Typeface.IsBold, font.Typeface.IsItalic, text); + + SKShaper.Result result; + lock (shapeResultCache) + { + result = shapeResultCache.TryGetValue(key, out var value) + ? value.shapeResult + : shaper.Shape(text, 0, 0, font); + + shapeResultCache[key] = (result, DateTime.Now); // update timestamp + } + return result; + } + + private static Timer clearCacheTimer = null; + + private static void ClearCache() + { + var outdated = DateTime.Now - TimeSpan.FromMilliseconds(cacheDuration); + + lock (shaperCache) + { + foreach (var kv in shaperCache.AsEnumerable()) + { + if (kv.Value.cachedAt < outdated) + { + if (shaperCache.Remove(kv.Key)) + kv.Value.shaper.Dispose(); + } + } + } + + lock (shapeResultCache) + { + foreach (var kv in shapeResultCache.AsEnumerable()) + { + if (kv.Value.cachedAt < outdated) + shapeResultCache.Remove(kv.Key); + } + } + + if ((shaperCache.Count == 0 && shapeResultCache.Count == 0) || cacheDuration == 0) + { + clearCacheTimer?.Dispose(); + clearCacheTimer = null; + } } } } diff --git a/tests/Tests/SkiaSharp/CanvasExtensionTest.cs b/tests/Tests/SkiaSharp/CanvasExtensionTest.cs new file mode 100644 index 0000000000..adab4e3d03 --- /dev/null +++ b/tests/Tests/SkiaSharp/CanvasExtensionTest.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using SkiaSharp.Tests; +using Xunit; + +namespace SkiaSharp.HarfBuzz.Tests +{ + public class CanvasExtensionTest : SKTest + { + private SKCanvas canvas; + private SKFont font; + private SKPaint paint; + + #region Access private fields + + FieldInfo shaperCacheField = null; + public Dictionary ShaperCache + { + get + { + if (shaperCacheField is null) + shaperCacheField = typeof(CanvasExtensions).GetField("shaperCache", BindingFlags.NonPublic | BindingFlags.Static); + + return (Dictionary)shaperCacheField!.GetValue(null)!; + } + } + + FieldInfo shapeResultCacheField = null; + public Dictionary ShapeResultCache + { + get + { + if (shapeResultCacheField is null) + shapeResultCacheField = typeof(CanvasExtensions).GetField("shapeResultCache", BindingFlags.NonPublic | BindingFlags.Static); + + return (Dictionary)shapeResultCacheField!.GetValue(null)!; + } + } + + #endregion Access private fields + + + public CanvasExtensionTest() + { + canvas = new SKCanvas(new SKBitmap(320, 200)); + font = SKTypeface.FromFile(Path.Combine(PathToFonts, "content-font.ttf")).ToFont(20); + paint = new SKPaint() { Color = SKColors.LimeGreen, IsAntialias = true, IsStroke = false }; + } + + [SkippableFact] + public void CacheGetsFilled() + { + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30_000); + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + + canvas.DrawShapedText("Hello world!", 0, 0, font, paint); + + Assert.Single(ShaperCache); + Assert.Single(ShapeResultCache); + + canvas.SetShaperCacheDuration(0); + } + + [SkippableFact] + public async Task CacheGetsClearedWhenSettingDurationToZero() + { + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30_000); + + canvas.DrawShapedText("Hello world!", 0, 0, font, paint); + + Assert.Single(ShaperCache); + Assert.Single(ShapeResultCache); + + canvas.SetShaperCacheDuration(0); + await Task.Delay(10); // give Timer a chance to run + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + } + + [SkippableFact] + public async Task CacheGetsClearedAutomatically() + { + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30); + + canvas.DrawShapedText("Hello world!", 0, 0, font, paint); + + Assert.Single(ShaperCache); + Assert.Single(ShapeResultCache); + + await Task.Delay(50); + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + + canvas.SetShaperCacheDuration(0); + } + + [SkippableFact] + public void TwoStringsGetBothCached() + { + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30_000); + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + + canvas.DrawShapedText("Hello", 0, 0, font, paint); + canvas.DrawShapedText("world!", 100, 100, font, paint); + + Assert.Single(ShaperCache); + Assert.Equal(2, ShapeResultCache.Count); + + canvas.SetShaperCacheDuration(0); + } + + [SkippableFact] + public void ShaperGetsCachedForDifferentFonts() + { + var font2 = SKTypeface.FromFile(Path.Combine(PathToFonts, "segoeui.ttf")).ToFont(20); + + canvas.SetShaperCacheDuration(0); + canvas.SetShaperCacheDuration(30_000); + + Assert.Empty(ShaperCache); + Assert.Empty(ShapeResultCache); + + canvas.DrawShapedText("Hello", 0, 0, font, paint); + + Assert.Single(ShaperCache); + Assert.Single(ShapeResultCache); + + canvas.DrawShapedText("world!", 100, 0, font2, paint); + + Assert.Equal(2, ShaperCache.Count); + Assert.Equal(2, ShapeResultCache.Count); + + canvas.SetShaperCacheDuration(0); + } + } +}