Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add caching to DrawShapedText #3033

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions binding/Binding.Shared/HashCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,186 @@ public int ToHashCode ()
hash = MixFinal (hash);
return (int)hash;
}

public static int Combine<T1> (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, T2> (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, T2, T3> (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, T2, T3, T4> (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, T2, T3, T4, T5> (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, T2, T3, T4, T5, T6> (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, T2, T3, T4, T5, T6, T7> (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, T2, T3, T4, T5, T6, T7, T8> (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;
}
}
}
93 changes: 90 additions & 3 deletions source/SkiaSharp.HarfBuzz/SkiaSharp.HarfBuzz/CanvasExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace SkiaSharp.HarfBuzz
{
Expand Down Expand Up @@ -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.")]
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -99,7 +104,89 @@ 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<int, (SKShaper shaper, DateTime cachedAt)> shaperCache = new();
private static readonly Dictionary<int, (SKShaper.Result shapeResult, DateTime cachedAt)> 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
Comment on lines +136 to +140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how GetOrAdd could help, but I tried AddOrUpdate here:

var newTuple = shaperCache.AddOrUpdate(key,
	_ => (new SKShaper(typeface), DateTime.Now),
	(_, old) => (old.shaper, DateTime.Now));

But the simple Dictionary with a lock is still faster.

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);

foreach (var kv in shaperCache.AsEnumerable())
{
if (kv.Value.cachedAt < outdated)
{
if (shaperCache.Remove(kv.Key))
kv.Value.shaper.Dispose();
}
}

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;
}
}
}
}
Loading