Skip to content

Commit

Permalink
libgui|Font: Improved thread-safety
Browse files Browse the repository at this point in the history
Native fonts are now designed to be used only in a single thread,
and internally in de::Font there is a separate native font instance per
thread. This allows text operations to proceed without blocking in
all the background threads.
  • Loading branch information
skyjake committed Feb 10, 2017
1 parent 456532f commit cf3d754
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 95 deletions.
3 changes: 3 additions & 0 deletions doomsday/sdk/libgui/include/de/text/nativefont.h
Expand Up @@ -35,6 +35,9 @@ namespace de {
* string of text, and draw the text onto an Image. This is an abstract base class for
* concrete implementations of native fonts.
*
* Each NativeFont is specific to a single thread. This allows text rendering to be
* done in multiple background threads without synchronization.
*
* @ingroup gui
*/
class LIBGUI_PUBLIC NativeFont : public Asset
Expand Down
86 changes: 20 additions & 66 deletions doomsday/sdk/libgui/src/text/coretextnativefont_macx.cpp
Expand Up @@ -24,6 +24,7 @@
#include <QThread>
#include <CoreGraphics/CoreGraphics.h>
#include <CoreText/CoreText.h>
#include <atomic>

namespace de {

Expand Down Expand Up @@ -109,7 +110,7 @@ struct CoreTextFontCache : public Lockable
return font;
}

#ifdef DENG2_DEBUG
#if 0
float fontSize(CTFontRef font) const
{
DENG2_FOR_EACH_CONST(Fonts, i, fonts)
Expand Down Expand Up @@ -137,26 +138,6 @@ struct CoreTextFontCache : public Lockable
#endif
};

#ifdef DENG2_DEBUG
struct LineCounter : public Lockable {
int count = 0;
~LineCounter() {
qDebug() << "[CoreTextNativeFont] Cached line count:" << count;
}
void inc() {
lock();
++count;
unlock();
}
void dec() {
lock();
--count;
unlock();
}
};
static LineCounter ctLineCounter;
#endif

static CoreTextFontCache fontCache;

DENG2_PIMPL(CoreTextNativeFont)
Expand All @@ -167,48 +148,27 @@ DENG2_PIMPL(CoreTextNativeFont)
float height;
float lineSpacing;

// Note that fonts may be used from multiple threads, so we keep a thread-specific
// cache of the most recently created line.
struct CachedLine
{
String lineText;
CTLineRef line = nullptr;

~CachedLine()
{
release();
}

void release()
{
if (line)
{
CFRelease(line);
line = nullptr;
#ifdef DENG2_DEBUG
ctLineCounter.dec();
#endif
}
lineText.clear();
}
};
struct Cache : public QHash<QThread *, CachedLine>, public Lockable
{
~Cache()
{
clear();
}

void clear()
{
for (CachedLine &entry : *this)
{
entry.release();
}
}

CachedLine &cachedLineForCurrentThread()
{
DENG2_GUARD(this);
return (*this)[QThread::currentThread()];
}
};
Cache cache;
CachedLine cache;

Impl(Public *i)
: Base(i)
Expand Down Expand Up @@ -252,7 +212,7 @@ DENG2_PIMPL(CoreTextNativeFont)
void release()
{
font = 0;
cache.clear();
cache.release();
}

void updateFontAndMetrics()
Expand All @@ -271,14 +231,13 @@ DENG2_PIMPL(CoreTextNativeFont)

CachedLine &makeLine(String const &text, CGColorRef color = 0)
{
auto &cachedLine = cache.cachedLineForCurrentThread();
if (cachedLine.lineText == text)
if (cache.lineText == text)
{
return cachedLine; // Already got it.
return cache; // Already got it.
}

cachedLine.release();
cachedLine.lineText = text;
cache.release();
cache.lineText = text;

void const *keys[] = { kCTFontAttributeName, kCTForegroundColorAttributeName };
void const *values[] = { font, color };
Expand All @@ -287,16 +246,12 @@ DENG2_PIMPL(CoreTextNativeFont)

CFStringRef textStr = CFStringCreateWithCharacters(nil, (UniChar *) text.data(), text.size());
CFAttributedStringRef as = CFAttributedStringCreate(0, textStr, attribs);
cachedLine.line = CTLineCreateWithAttributedString(as);

#ifdef DENG2_DEBUG
ctLineCounter.inc();
#endif
cache.line = CTLineCreateWithAttributedString(as);

CFRelease(attribs);
CFRelease(textStr);
CFRelease(as);
return cachedLine;
return cache;
}
};

Expand Down Expand Up @@ -366,7 +321,7 @@ Rectanglei CoreTextNativeFont::nativeFontMeasure(String const &text) const
//CGLineGetImageBounds(d->line, d->gc); // more accurate but slow

Rectanglei rect(Vector2i(0, -d->ascent),
Vector2i(roundi(CTLineGetTypographicBounds(d->cache.cachedLineForCurrentThread().line, NULL, NULL, NULL)),
Vector2i(roundi(CTLineGetTypographicBounds(d->cache.line, NULL, NULL, NULL)),
d->descent));

return rect;
Expand All @@ -392,12 +347,11 @@ QImage CoreTextNativeFont::nativeFontRasterize(String const &text,
CGColorRef fgColor = CGColorCreate(fontCache.colorspace(), &fg.x);

// Ensure the color is used by recreating the attributed line string.
auto &cachedLine = d->cache.cachedLineForCurrentThread();
cachedLine.release();
d->cache.release();
d->makeLine(d->applyTransformation(text), fgColor);

// Set up the bitmap for drawing into.
Rectanglei const bounds = measure(cachedLine.lineText);
Rectanglei const bounds = measure(d->cache.lineText);
QImage backbuffer(QSize(bounds.width(), bounds.height()), QImage::Format_ARGB32);
backbuffer.fill(QColor(background.x, background.y, background.z, background.w).rgba());

Expand All @@ -409,11 +363,11 @@ QImage CoreTextNativeFont::nativeFontRasterize(String const &text,
kCGImageAlphaPremultipliedLast);

CGContextSetTextPosition(gc, 0, d->descent);
CTLineDraw(cachedLine.line, gc);
CTLineDraw(d->cache.line, gc);

CGColorRelease(fgColor);
CGContextRelease(gc);
cachedLine.release();
d->cache.release();

return backbuffer;
}
Expand Down
85 changes: 61 additions & 24 deletions doomsday/sdk/libgui/src/text/font.cpp
Expand Up @@ -23,6 +23,7 @@
#include <QFontDatabase>
#include <QImage>
#include <QPainter>
#include <QThreadStorage>

#if defined(MACOSX) && defined(MACOS_10_7)
# include "../src/text/coretextnativefont_macx.h"
Expand Down Expand Up @@ -71,10 +72,27 @@ namespace internal
}
}

DENG2_PIMPL(Font), public Lockable
DENG2_PIMPL(Font)
{
PlatformFont font;
QHash<internal::FontParams, PlatformFont *> fontMods;
QFont referenceFont;
struct ThreadFonts
{
PlatformFont font;
QHash<internal::FontParams, PlatformFont *> fontMods;

~ThreadFonts() {
qDeleteAll(fontMods.values());
}
};

/**
* Each thread uses its own independent set of native font instances. This allows
* background threads to freely measure and render text using the native font
* instances without any synchronization. Also note that these background threads are
* pooled, so there is only a fixed total number of threads accessing these objects.
*/
QThreadStorage<ThreadFonts> fontsForThread;

ConstantRule *heightRule;
ConstantRule *ascentRule;
ConstantRule *descentRule;
Expand All @@ -86,7 +104,7 @@ DENG2_PIMPL(Font), public Lockable
createRules();
}

Impl(Public *i, PlatformFont const &qfont) : Base(i), font(qfont)
Impl(Public *i, QFont const &qfont) : Base(i), referenceFont(qfont)
{
#if 0
// Development aid: list all available fonts and styles.
Expand All @@ -104,7 +122,7 @@ DENG2_PIMPL(Font), public Lockable

~Impl()
{
qDeleteAll(fontMods.values());
//qDeleteAll(fontMods.values());

releaseRef(heightRule);
releaseRef(ascentRule);
Expand All @@ -120,30 +138,44 @@ DENG2_PIMPL(Font), public Lockable
lineSpacingRule = new ConstantRule(0);
}

/**
* Initializes the current thread's platform fonts for this Font.
*/
ThreadFonts &getThreadFonts()
{
if (!fontsForThread.hasLocalData())
{
fontsForThread.localData().font = PlatformFont(referenceFont);
}
return fontsForThread.localData();
}

void updateMetrics()
{
ascent = font.ascent();
if (font.weight() != NativeFont::Normal)
auto &plat = getThreadFonts();

ascent = plat.font.ascent();
if (plat.font.weight() != NativeFont::Normal)
{
// Use the ascent of the normal weight for non-normal weights;
// we need to align content to baseline regardless of weight.
PlatformFont normalized(font);
PlatformFont normalized(plat.font);
normalized.setWeight(NativeFont::Normal);
ascent = normalized.ascent();
}

ascentRule->set(ascent);
descentRule->set(font.descent());
heightRule->set(font.height());
lineSpacingRule->set(font.lineSpacing());
ascentRule ->set(ascent);
descentRule ->set(plat.font.descent());
heightRule ->set(plat.font.height());
lineSpacingRule->set(plat.font.lineSpacing());
}

PlatformFont &getFontMod(internal::FontParams const &params)
{
DENG2_GUARD(this);
auto &plat = getThreadFonts();

auto found = fontMods.constFind(params);
if (found != fontMods.constEnd())
auto found = plat.fontMods.constFind(params);
if (found != plat.fontMods.constEnd())
{
return *found.value();
}
Expand All @@ -154,7 +186,7 @@ DENG2_PIMPL(Font), public Lockable
mod->setStyle(params.spec.style);
mod->setWeight(params.spec.weight);
mod->setTransform(params.spec.transform);
fontMods.insert(params, mod);
plat.fontMods.insert(params, mod);
return *mod;
}

Expand All @@ -168,9 +200,11 @@ DENG2_PIMPL(Font), public Lockable
*/
PlatformFont const &alteredFont(RichFormat::Iterator const &rich)
{
auto &plat = getThreadFonts();

if (!rich.isDefault())
{
internal::FontParams modParams(font);
internal::FontParams modParams(plat.font);

// Size change.
if (!fequal(rich.sizeFactor(), 1.f))
Expand All @@ -185,12 +219,12 @@ DENG2_PIMPL(Font), public Lockable
break;

case RichFormat::Regular:
modParams.family = font.family();
modParams.family = plat.font.family();
modParams.spec.style = NativeFont::Regular;
break;

case RichFormat::Italic:
modParams.family = font.family();
modParams.family = plat.font.family();
modParams.spec.style = NativeFont::Italic;
break;

Expand All @@ -199,7 +233,7 @@ DENG2_PIMPL(Font), public Lockable
{
if (Font const *altFont = rich.format.format().style().richStyleFont(rich.style()))
{
modParams = internal::FontParams(altFont->d->font);
modParams = internal::FontParams(altFont->d->getThreadFonts().font);
}
}
break;
Expand All @@ -214,14 +248,16 @@ DENG2_PIMPL(Font), public Lockable
}
return getFontMod(modParams);
}
return font;

// No alterations applied.
return plat.font;
}
};

Font::Font() : d(new Impl(this))
{}

Font::Font(Font const &other) : d(new Impl(this, other.d->font))
Font::Font(Font const &other) : d(new Impl(this, other.d->referenceFont))
{}

Font::Font(QFont const &font) : d(new Impl(this, font))
Expand Down Expand Up @@ -307,8 +343,9 @@ QImage Font::rasterize(String const &textLine,
Vector4ub fg = foreground;
Vector4ub bg = background;

auto const &plat = d->getThreadFonts();
QImage img(QSize(bounds.width(),
de::max(duint(d->font.height()), bounds.height())),
de::max(duint(plat.font.height()), bounds.height())),
QImage::Format_ARGB32);
img.fill(bgColor.rgba());

Expand All @@ -324,7 +361,7 @@ QImage Font::rasterize(String const &textLine,
iter.next();
if (iter.range().isEmpty()) continue;

PlatformFont const *font = &d->font;
PlatformFont const *font = &plat.font;

if (iter.isDefault())
{
Expand Down

0 comments on commit cf3d754

Please sign in to comment.