Hello,

I have another patch here for review that optimizes the performance of
glyph drawing, primarily when using non-unit kerning values, and fixes a
few other minor issues. It's dependent on the earlier patch from me that
stores unicode codepoints in a Rune type, typedef'd to uint_least32_t.

This patch is a pretty big change to xdraws so your scrutiny is
appreciated.

First, some performance numbers. I used Yu-Jie Lin termfps.sh shell
script to benchmark before and after, and you can find it in the
attachments. On my Kaveri A10 7850k machine, I get the following
results:

Before Patch
============

1) Font: "Liberation Mono:pixelsize=12:antialias=false:autohint=false"
   cwscale: 1.0, chscale: 1.0
   For 273x83 100 frames.
   Elapsed time :     1.553
   Frames/second:    64.352
   Chars /second: 1,458,159

2) Font: "Inconsolata:pixelsize=14:antialias=true:autohint=true"
   cwscale: 1.001, chscale: 1.001
   For 239x73 100 frames.
   Elapsed time :   159.286
   Frames/second:     0.627
   Chars /second:    10,953

After Patch
===========

3) Font: "Liberation Mono:pixelsize=12:antialias=false:autohint=false"
   cwscale: 1.0, chscale: 1.0
   For 273x83 100 frames.
   Elapsed time :     1.544
   Frames/second:    64.728
   Chars /second: 1,466,690

4) Font: "Inconsolata:pixelsize=14:antialias=true:autohint=true"
   cwscale: 1.001, chscale: 1.001
   For 239x73 100 frames.
   Elapsed time :     1.955
   Frames/second:    51.146
   Chars /second:   892,361

As you can see, while the improvements for fonts with unit-kerning is
marginal, there's a huge ~81x performance increase with the patch when
using kerning values other than 1.0.

So what does the patch do?

The `xdraws' function would render each glyph one at a time if non-unit
kerning values were configured, and this was the primary cause of the
slow down. Xft provides a handful of functions which allow you to render
multiple characters or glyphs at time, each with a unique <x,y> position,
so it was simply a matter of massaging the data into a format that would
allow us to use one of these functions.

I've split `xdraws' up into two functions. In the first pass with
`xmakeglyphfontspecs' it will iterate over all of the glyphs in a given
row and it will build up an array of corresponding XftGlyphFontSpec
records. Much of the old logic for resolving fonts for glyphs using Xft
and fontconfig went into this function.

The second pass is done with `xrenderglyphfontspecs' which contains the
old logic for determining colors, clearing the background, and finally
rendering the array of XftGlyphFontSpec records.

There's a couple of other things that have been improved by this patch.
For instance, the UTF-32 codepoints in the Line's were being re-encoded
back into UTF-8 strings to be passed to `xdraws' which in turn would then
decode back to UTF-32 to verify that the Font contained a matching glyph
for the code point. Next, the UTF-8 string was being passed to
`XftDrawStringUtf8' which internally mallocs a scratch buffer and decodes
back to UTF-32 and does the lookup of the glyphs all over again.

This patch gets rid of all of this redundant round-trip encoding and
decoding of characters to be rendered and only looks up the glyph index
once (per font) during the font resolution phase. So this is probably
what's esponsible for the marginal improvements seen when kerning values
are kept to 1.0.

I imagine there are other performance improvements here too, not seen in
the above benchmarks, if the user has lots of non-ASCII code plane characters
on the screen, or several different fonts are being utilized during
screen redraw.

Anyway, if you see any problems, please let me know and I can fix them.
diff --git a/st.c b/st.c
index 4add638..6621fcc 100644
--- a/st.c
+++ b/st.c
@@ -58,7 +58,6 @@ char *argv0;
 #define ESC_ARG_SIZ   16
 #define STR_BUF_SIZ   ESC_BUF_SIZ
 #define STR_ARG_SIZ   ESC_ARG_SIZ
-#define DRAW_BUF_SIZ  20*1024
 #define XK_ANY_MOD    UINT_MAX
 #define XK_NO_MOD     0
 #define XK_SWITCH_MOD (1<<13)
@@ -232,6 +231,7 @@ typedef struct {
 	Line *line;   /* screen */
 	Line *alt;    /* alternate screen */
 	bool *dirty;  /* dirtyness of lines */
+	XftGlyphFontSpec *specbuf; /* font spec buffer used for rendering */
 	TCursor c;    /* cursor */
 	int top;      /* top    scroll limit */
 	int bot;      /* bottom scroll limit */
@@ -418,7 +418,8 @@ static void ttywrite(const char *, size_t);
 static void tstrsequence(uchar);
 
 static inline ushort sixd_to_16bit(int);
-static void xdraws(char *, Glyph, int, int, int, int);
+static void xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
+static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
 static void xdrawglyph(Glyph, int, int);
 static void xhints(void);
 static void xclear(int, int, int, int);
@@ -2819,6 +2820,9 @@ tresize(int col, int row) {
 		free(term.alt[i]);
 	}
 
+	/* resize to new width */
+	term.specbuf = xrealloc(term.specbuf, col * sizeof(XftGlyphFontSpec));
+
 	/* resize to new height */
 	term.line = xrealloc(term.line, row * sizeof(Line));
 	term.alt  = xrealloc(term.alt,  row * sizeof(Line));
@@ -3258,35 +3262,143 @@ xinit(void) {
 }
 
 void
-xdraws(char *s, Glyph base, int x, int y, int charlen, int bytelen) {
-	int winx = borderpx + x * xw.cw, winy = borderpx + y * xw.ch,
-	    width = charlen * xw.cw, xp, i;
-	int frcflags, charexists;
-	int u8fl, u8fblen, u8cblen, doesexist;
-	char *u8c, *u8fs;
-	Rune unicodep;
+xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
+{
+	float winx = borderpx + x * xw.cw, winy = borderpx + y * xw.ch, xp, yp;
+	ushort mode, prevmode = USHRT_MAX;
 	Font *font = &dc.font;
+	int frcflags = FRC_NORMAL;
+	float runewidth = xw.cw;
+	Rune rune;
+	FT_UInt glyphidx;
 	FcResult fcres;
 	FcPattern *fcpattern, *fontpattern;
 	FcFontSet *fcsets[] = { NULL };
 	FcCharSet *fccharset;
+	int i, f;
+
+	for(i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i, xp += runewidth) {
+		/* Fetch rune and mode for current glyph. */
+		rune = glyphs[i].u;
+		mode = glyphs[i].mode;
+
+		/* Determine font for glyph if different from previous glyph. */
+		if(prevmode != mode) {
+			prevmode = mode;
+			font = &dc.font;
+			frcflags = FRC_NORMAL;
+			runewidth = xw.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f);
+			if((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
+				font = &dc.ibfont;
+				frcflags = FRC_ITALICBOLD;
+			} else if(mode & ATTR_ITALIC) {
+				font = &dc.ifont;
+				frcflags = FRC_ITALIC;
+			} else if(mode & ATTR_BOLD) {
+				font = &dc.bfont;
+				frcflags = FRC_BOLD;
+			}
+		}
+
+		/* Lookup character index with default font. */
+		glyphidx = XftCharIndex(xw.dpy, font->match, rune);
+		if(glyphidx) {
+			specs[i].font = font->match;
+			specs[i].glyph = glyphidx;
+			specs[i].x = (short)xp;
+			specs[i].y = (short)yp;
+			continue;
+		}
+
+		/* Fallback on font cache, search the font cache for match. */
+		for(f = 0; f < frclen; f++) {
+			glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
+			/* Everything correct. */
+			if(glyphidx && frc[f].flags == frcflags)
+				break;
+			/* We got a default font for a not found glyph. */
+			if(!glyphidx && frc[f].flags == frcflags
+					&& frc[f].unicodep == rune) {
+				break;
+			}
+		}
+
+		/* Nothing was found. Use fontconfig to find matching font. */
+		if(f >= frclen) {
+			if(!font->set)
+				font->set = FcFontSort(0, font->pattern,
+				                       FcTrue, 0, &fcres);
+			fcsets[0] = font->set;
+
+			/*
+			 * Nothing was found in the cache. Now use
+			 * some dozen of Fontconfig calls to get the
+			 * font for one single character.
+			 *
+			 * Xft and fontconfig are design failures.
+			 */
+			fcpattern = FcPatternDuplicate(font->pattern);
+			fccharset = FcCharSetCreate();
+
+			FcCharSetAddChar(fccharset, rune);
+			FcPatternAddCharSet(fcpattern, FC_CHARSET,
+					fccharset);
+			FcPatternAddBool(fcpattern, FC_SCALABLE,
+					FcTrue);
+
+			FcConfigSubstitute(0, fcpattern,
+					FcMatchPattern);
+			FcDefaultSubstitute(fcpattern);
+
+			fontpattern = FcFontSetMatch(0, fcsets, 1,
+					fcpattern, &fcres);
+
+			/*
+			 * Overwrite or create the new cache entry.
+			 */
+			if(frclen >= LEN(frc)) {
+				frclen = LEN(frc) - 1;
+				XftFontClose(xw.dpy, frc[frclen].font);
+				frc[frclen].unicodep = 0;
+			}
+
+			frc[frclen].font = XftFontOpenPattern(xw.dpy,
+					fontpattern);
+			frc[frclen].flags = frcflags;
+			frc[frclen].unicodep = rune;
+
+			glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune);
+
+			f = frclen;
+			frclen++;
+
+			FcPatternDestroy(fcpattern);
+			FcCharSetDestroy(fccharset);
+		}
+
+		specs[i].font = frc[f].font;
+		specs[i].glyph = glyphidx;
+		specs[i].x = (short)xp;
+		specs[i].y = (short)(winy + frc[f].font->ascent);
+	}
+}
+
+void
+xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) {
+	int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
+	int winx = borderpx + x * xw.cw, winy = borderpx + y * xw.ch,
+	    width = charlen * xw.cw;
 	Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
 	XRenderColor colfg, colbg;
 	XRectangle r;
-	int oneatatime;
-
-	frcflags = FRC_NORMAL;
 
+	/* Determine foreground and background colors based on mode. */
 	if(base.mode & ATTR_ITALIC) {
 		if(base.fg == defaultfg)
 			base.fg = defaultitalic;
-		font = &dc.ifont;
-		frcflags = FRC_ITALIC;
 	} else if((base.mode & ATTR_ITALIC) && (base.mode & ATTR_BOLD)) {
 		if(base.fg == defaultfg)
 			base.fg = defaultitalic;
-		font = &dc.ibfont;
-		frcflags = FRC_ITALICBOLD;
 	} else if(base.mode & ATTR_UNDERLINE) {
 		if(base.fg == defaultfg)
 			base.fg = defaultunderline;
@@ -3321,14 +3433,6 @@ xdraws(char *s, Glyph base, int x, int y, int charlen, int bytelen) {
 		 */
 		if(BETWEEN(base.fg, 0, 7) && !(base.mode & ATTR_FAINT))
 			fg = &dc.col[base.fg + 8];
-
-		if(base.mode & ATTR_ITALIC) {
-			font = &dc.ibfont;
-			frcflags = FRC_ITALICBOLD;
-		} else {
-			font = &dc.bfont;
-			frcflags = FRC_BOLD;
-		}
 	}
 
 	if(IS_SET(MODE_REVERSE)) {
@@ -3401,136 +3505,17 @@ xdraws(char *s, Glyph base, int x, int y, int charlen, int bytelen) {
 	r.width = width;
 	XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1);
 
-	for(xp = winx; bytelen > 0;) {
-		/*
-		 * Search for the range in the to be printed string of glyphs
-		 * that are in the main font. Then print that range. If
-		 * some glyph is found that is not in the font, do the
-		 * fallback dance.
-		 */
-		u8fs = s;
-		u8fblen = 0;
-		u8fl = 0;
-		oneatatime = font->width != xw.cw;
-		for(;;) {
-			u8c = s;
-			u8cblen = utf8decode(s, &unicodep, UTF_SIZ);
-			s += u8cblen;
-			bytelen -= u8cblen;
-
-			doesexist = XftCharExists(xw.dpy, font->match, unicodep);
-			if(doesexist) {
-					u8fl++;
-					u8fblen += u8cblen;
-					if(!oneatatime && bytelen > 0)
-							continue;
-			}
-
-			if(u8fl > 0) {
-				XftDrawStringUtf8(xw.draw, fg,
-						font->match, xp,
-						winy + font->ascent,
-						(FcChar8 *)u8fs,
-						u8fblen);
-				xp += xw.cw * u8fl;
-			}
-			break;
-		}
-		if(doesexist) {
-			if(oneatatime)
-				continue;
-			break;
-		}
-
-		/* Search the font cache. */
-		for(i = 0; i < frclen; i++) {
-			charexists = XftCharExists(xw.dpy, frc[i].font, unicodep);
-			/* Everything correct. */
-			if(charexists && frc[i].flags == frcflags)
-				break;
-			/* We got a default font for a not found glyph. */
-			if(!charexists && frc[i].flags == frcflags \
-					&& frc[i].unicodep == unicodep) {
-				break;
-			}
-		}
-
-		/* Nothing was found. */
-		if(i >= frclen) {
-			if(!font->set)
-				font->set = FcFontSort(0, font->pattern,
-				                       FcTrue, 0, &fcres);
-			fcsets[0] = font->set;
-
-			/*
-			 * Nothing was found in the cache. Now use
-			 * some dozen of Fontconfig calls to get the
-			 * font for one single character.
-			 *
-			 * Xft and fontconfig are design failures.
-			 */
-			fcpattern = FcPatternDuplicate(font->pattern);
-			fccharset = FcCharSetCreate();
-
-			FcCharSetAddChar(fccharset, unicodep);
-			FcPatternAddCharSet(fcpattern, FC_CHARSET,
-					fccharset);
-			FcPatternAddBool(fcpattern, FC_SCALABLE,
-					FcTrue);
-
-			FcConfigSubstitute(0, fcpattern,
-					FcMatchPattern);
-			FcDefaultSubstitute(fcpattern);
-
-			fontpattern = FcFontSetMatch(0, fcsets, 1,
-					fcpattern, &fcres);
-
-			/*
-			 * Overwrite or create the new cache entry.
-			 */
-			if(frclen >= LEN(frc)) {
-				frclen = LEN(frc) - 1;
-				XftFontClose(xw.dpy, frc[frclen].font);
-				frc[frclen].unicodep = 0;
-			}
-
-			frc[frclen].font = XftFontOpenPattern(xw.dpy,
-					fontpattern);
-			frc[frclen].flags = frcflags;
-			frc[frclen].unicodep = unicodep;
-
-			i = frclen;
-			frclen++;
-
-			FcPatternDestroy(fcpattern);
-			FcCharSetDestroy(fccharset);
-		}
-
-		XftDrawStringUtf8(xw.draw, fg, frc[i].font,
-				xp, winy + frc[i].font->ascent,
-				(FcChar8 *)u8c, u8cblen);
-
-		xp += xw.cw * wcwidth(unicodep);
-	}
-
-	/*
-	 * This is how the loop above actually should be. Why does the
-	 * application have to care about font details?
-	 *
-	 * I have to repeat: Xft and Fontconfig are design failures.
-	 */
-	/*
-	XftDrawStringUtf8(xw.draw, fg, font->set, winx,
-			winy + font->ascent, (FcChar8 *)s, bytelen);
-	*/
+	/* Render the glyphs. */
+	XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
 
+	/* Render underline and strikethrough. */
 	if(base.mode & ATTR_UNDERLINE) {
-		XftDrawRect(xw.draw, fg, winx, winy + font->ascent + 1,
+		XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent + 1,
 				width, 1);
 	}
 
 	if(base.mode & ATTR_STRUCK) {
-		XftDrawRect(xw.draw, fg, winx, winy + 2 * font->ascent / 3,
+		XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3,
 				width, 1);
 	}
 
@@ -3540,11 +3525,9 @@ xdraws(char *s, Glyph base, int x, int y, int charlen, int bytelen) {
 
 void
 xdrawglyph(Glyph g, int x, int y) {
-	static char buf[UTF_SIZ];
-	size_t len = utf8encode(g.u, buf);
-	int width = g.mode & ATTR_WIDE ? 2 : 1;
-
-	xdraws(buf, g, x, y, width, len);
+	XftGlyphFontSpec spec;
+	xmakeglyphfontspecs(&spec, &g, 1, x, y);
+	xdrawglyphfontspecs(&spec, g, 1, x, y);
 }
 
 void
@@ -3658,9 +3641,9 @@ draw(void) {
 
 void
 drawregion(int x1, int y1, int x2, int y2) {
-	int ic, ib, x, y, ox;
+	int i, x, y, ox;
 	Glyph base, new;
-	char buf[DRAW_BUF_SIZ];
+	XftGlyphFontSpec* specs;
 	bool ena_sel = sel.ob.x != -1 && sel.alt == IS_SET(MODE_ALTSCREEN);
 
 	if(!(xw.state & WIN_VISIBLE))
@@ -3672,29 +3655,29 @@ drawregion(int x1, int y1, int x2, int y2) {
 
 		xtermclear(0, y, term.col, y);
 		term.dirty[y] = 0;
-		base = term.line[y][0];
-		ic = ib = ox = 0;
-		for(x = x1; x < x2; x++) {
+
+		specs = term.specbuf;
+		xmakeglyphfontspecs(specs, &term.line[y][0], x2 - x1, x1, y);
+
+		i = ox = 0;
+		for(x = x1; x < x2; i++, x++) {
 			new = term.line[y][x];
 			if(new.mode == ATTR_WDUMMY)
 				continue;
 			if(ena_sel && selected(x, y))
 				new.mode ^= ATTR_REVERSE;
-			if(ib > 0 && (ATTRCMP(base, new)
-					|| ib >= DRAW_BUF_SIZ-UTF_SIZ)) {
-				xdraws(buf, base, ox, y, ic, ib);
-				ic = ib = 0;
+			if(i > 0 && ATTRCMP(base, new)) {
+				xdrawglyphfontspecs(specs, base, i, ox, y);
+				specs += i;
+				i = 0;
 			}
-			if(ib == 0) {
+			if(i == 0) {
 				ox = x;
 				base = new;
 			}
-
-			ib += utf8encode(new.u, buf+ib);
-			ic += (new.mode & ATTR_WIDE)? 2 : 1;
 		}
-		if(ib > 0)
-			xdraws(buf, base, ox, y, ic, ib);
+		if(i > 0)
+			xdrawglyphfontspecs(specs, base, i, ox, y);
 	}
 	xdrawcursor();
 }
#! /bin/bash
# Copyright (c) 2010, 2013 Yu-Jie Lin
#
# Permission is hereby granted, free of charge, to any person obtaining a copy 
of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Usage: termfps.sh [FRAMES] [COLUMNS] [LINES]

FRAMES=${1:-100}
COLUMNS=${2:-$(tput cols)}
LINES=${3:-$(tput lines)}
CHARS=$((COLUMNS * LINES * FRAMES))

printf -v dummy_line   "%${COLUMNS}s"     ''
printf -v dummy_line_n "%${COLUMNS}s\n"   ''
printf -v dummy_frame  "%$((LINES - 1))s" ''

for ((ch = 0; ch < 10; ch++)); do
  line_str=${dummy_line// /$ch}
  line_n_str=${dummy_line_n// /$ch}
  frame_str="${dummy_frame// /$line_n_str}$line_str"
  frame_str_r="frame_str$ch"

  eval "$frame_str_r=\"$frame_str\""
done

t_start=$(date +%s.%N)
for ((i = 0; i < FRAMES; i++)); do
  for ((ch = 0; ch < 10; ch++)); do
    frame_str="frame_str$ch"
    echo -ne "\033[H${!frame_str}"
  done
done
t_end=$(date +%s.%N)

echo
echo "For ${COLUMNS}x${LINES} $FRAMES frames."
printf "Elapsed time : %'9.3f\n" "$(bc <<< "scale=3;           ($t_end - 
$t_start) / 1")"
printf "Frames/second: %'9.3f\n" "$(bc <<< "scale=3; $FRAMES / ($t_end - 
$t_start)")"
printf "Chars /second: %'9d\n"   "$(bc <<< "scale=0; $CHARS  / ($t_end - 
$t_start)")"

Attachment: signature.asc
Description: Digital signature

Reply via email to