tauto-sync: draw on idle to avoid flicker/tearing - st - [fork] customized buil… | |
git clone git://src.adamsgaard.dk/st | |
Log | |
Files | |
Refs | |
README | |
LICENSE | |
--- | |
commit 1d590910652519268152eae6b97cf30ace4e90c0 | |
parent d6ea0a1a61853dd892029a7126e7fdb70c371878 | |
Author: Avi Halachmi (:avih) <[email protected]> | |
Date: Tue, 26 Feb 2019 22:37:49 +0200 | |
auto-sync: draw on idle to avoid flicker/tearing | |
st could easily tear/flicker with animation or other unattended | |
output. This commit eliminates most of the tear/flicker. | |
Before this commit, the display timing had two "modes": | |
- Interactively, st was waiting fixed `1000/xfps` ms after forwarding | |
the kb/mouse event to the application and before drawing. | |
- Unattended, and specifically with animations, the draw frequency was | |
throttled to `actionfps`. Animation at a higher rate would throttle | |
and likely tear, and at lower rates it was tearing big frames | |
(specifically, when one `read` didn't get a full "frame"). | |
The interactive behavior was decent, but it was impossible to get good | |
unattended-draw behavior even with carefully chosen configuration. | |
This commit changes the behavior such that it draws on idle instead of | |
using fixed latency/frequency. This means that it tries to draw only | |
when it's very likely that the application has completed its output | |
(or after some duration without idle), so it mostly succeeds to avoid | |
ttear, flicker, and partial drawing. | |
The config values minlatency/maxlatency replace xfps/actionfps and | |
define the range which the algorithm is allowed to wait from the | |
initial draw-trigger until the actual draw. The range enables the | |
flexibility to choose when to draw - when least likely to flicker. | |
It also unifies the interactive and unattended behavior and config | |
values, which makes the code simpler as well - without sacrificing | |
latency during interactive use, because typically interactively idle | |
arrives very quickly, so the wait is typically minlatency. | |
While it only slighly improves interactive behavior, for animations | |
and other unattended-drawing it improves greatly, as it effectively | |
adapts to any [animation] output rate without tearing, throttling, | |
redundant drawing, or unnecessary delays (sounds impossible, but it | |
works). | |
Diffstat: | |
M config.def.h | 11 ++++++++--- | |
M x.c | 120 +++++++++++++++--------------… | |
2 files changed, 65 insertions(+), 66 deletions(-) | |
--- | |
diff --git a/config.def.h b/config.def.h | |
t@@ -43,9 +43,14 @@ static unsigned int tripleclicktimeout = 600; | |
/* alt screens */ | |
int allowaltscreen = 1; | |
-/* frames per second st should at maximum draw to the screen */ | |
-static unsigned int xfps = 120; | |
-static unsigned int actionfps = 30; | |
+/* | |
+ * draw latency range in ms - from new content/keypress/etc until drawing. | |
+ * within this range, st draws when content stops arriving (idle). mostly it's | |
+ * near minlatency, but it waits longer for slow updates to avoid partial draw. | |
+ * low minlatency will tear/flicker more, as it can "detect" idle too early. | |
+ */ | |
+static double minlatency = 8; | |
+static double maxlatency = 33; | |
/* | |
* blinking timeout (set to 0 to disable blinking) for the terminal blinking | |
diff --git a/x.c b/x.c | |
t@@ -1867,10 +1867,9 @@ run(void) | |
XEvent ev; | |
int w = win.w, h = win.h; | |
fd_set rfd; | |
- int xfd = XConnectionNumber(xw.dpy), xev, blinkset = 0, dodraw = 0; | |
- int ttyfd; | |
- struct timespec drawtimeout, *tv = NULL, now, last, lastblink; | |
- long deltatime; | |
+ int xfd = XConnectionNumber(xw.dpy), ttyfd, xev, drawing; | |
+ struct timespec seltv, *tv, now, lastblink, trigger; | |
+ double timeout; | |
/* Waiting for window mapping */ | |
do { | |
t@@ -1891,82 +1890,77 @@ run(void) | |
ttyfd = ttynew(opt_line, shell, opt_io, opt_cmd); | |
cresize(w, h); | |
- clock_gettime(CLOCK_MONOTONIC, &last); | |
- lastblink = last; | |
- | |
- for (xev = actionfps;;) { | |
+ for (timeout = -1, drawing = 0, lastblink = (struct timespec){0};;) { | |
FD_ZERO(&rfd); | |
FD_SET(ttyfd, &rfd); | |
FD_SET(xfd, &rfd); | |
+ if (XPending(xw.dpy)) | |
+ timeout = 0; /* existing events might not set xfd */ | |
+ | |
+ seltv.tv_sec = timeout / 1E3; | |
+ seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec); | |
+ tv = timeout >= 0 ? &seltv : NULL; | |
+ | |
if (pselect(MAX(xfd, ttyfd)+1, &rfd, NULL, NULL, tv, NULL) < 0… | |
if (errno == EINTR) | |
continue; | |
die("select failed: %s\n", strerror(errno)); | |
} | |
- if (FD_ISSET(ttyfd, &rfd)) { | |
- ttyread(); | |
- if (blinktimeout) { | |
- blinkset = tattrset(ATTR_BLINK); | |
- if (!blinkset) | |
- MODBIT(win.mode, 0, MODE_BLINK); | |
- } | |
- } | |
+ clock_gettime(CLOCK_MONOTONIC, &now); | |
- if (FD_ISSET(xfd, &rfd)) | |
- xev = actionfps; | |
+ if (FD_ISSET(ttyfd, &rfd)) | |
+ ttyread(); | |
- clock_gettime(CLOCK_MONOTONIC, &now); | |
- drawtimeout.tv_sec = 0; | |
- drawtimeout.tv_nsec = (1000 * 1E6)/ xfps; | |
- tv = &drawtimeout; | |
- | |
- dodraw = 0; | |
- if (blinktimeout && TIMEDIFF(now, lastblink) > blinktimeout) { | |
- tsetdirtattr(ATTR_BLINK); | |
- win.mode ^= MODE_BLINK; | |
- lastblink = now; | |
- dodraw = 1; | |
- } | |
- deltatime = TIMEDIFF(now, last); | |
- if (deltatime > 1000 / (xev ? xfps : actionfps)) { | |
- dodraw = 1; | |
- last = now; | |
+ xev = 0; | |
+ while (XPending(xw.dpy)) { | |
+ xev = 1; | |
+ XNextEvent(xw.dpy, &ev); | |
+ if (XFilterEvent(&ev, None)) | |
+ continue; | |
+ if (handler[ev.type]) | |
+ (handler[ev.type])(&ev); | |
} | |
- if (dodraw) { | |
- while (XPending(xw.dpy)) { | |
- XNextEvent(xw.dpy, &ev); | |
- if (XFilterEvent(&ev, None)) | |
- continue; | |
- if (handler[ev.type]) | |
- (handler[ev.type])(&ev); | |
+ /* | |
+ * To reduce flicker and tearing, when new content or event | |
+ * triggers drawing, we first wait a bit to ensure we got | |
+ * everything, and if nothing new arrives - we draw. | |
+ * We start with trying to wait minlatency ms. If more content | |
+ * arrives sooner, we retry with shorter and shorter preiods, | |
+ * and eventually draw even without idle after maxlatency ms. | |
+ * Typically this results in low latency while interacting, | |
+ * maximum latency intervals during `cat huge.txt`, and perfect | |
+ * sync with periodic updates from animations/key-repeats/etc. | |
+ */ | |
+ if (FD_ISSET(ttyfd, &rfd) || xev) { | |
+ if (!drawing) { | |
+ trigger = now; | |
+ drawing = 1; | |
} | |
+ timeout = (maxlatency - TIMEDIFF(now, trigger)) \ | |
+ / maxlatency * minlatency; | |
+ if (timeout > 0) | |
+ continue; /* we have time, try to find idle */ | |
+ } | |
- draw(); | |
- XFlush(xw.dpy); | |
- | |
- if (xev && !FD_ISSET(xfd, &rfd)) | |
- xev--; | |
- if (!FD_ISSET(ttyfd, &rfd) && !FD_ISSET(xfd, &rfd)) { | |
- if (blinkset) { | |
- if (TIMEDIFF(now, lastblink) \ | |
- > blinktimeout) { | |
- drawtimeout.tv_nsec = 1000; | |
- } else { | |
- drawtimeout.tv_nsec = (1E6 * \ | |
- (blinktimeout - \ | |
- TIMEDIFF(now, | |
- lastblink))); | |
- } | |
- drawtimeout.tv_sec = \ | |
- drawtimeout.tv_nsec / 1E9; | |
- drawtimeout.tv_nsec %= (long)1E9; | |
- } else { | |
- tv = NULL; | |
- } | |
+ /* idle detected or maxlatency exhausted -> draw */ | |
+ timeout = -1; | |
+ if (blinktimeout && tattrset(ATTR_BLINK)) { | |
+ timeout = blinktimeout - TIMEDIFF(now, lastblink); | |
+ if (timeout <= 0) { | |
+ if (-timeout > blinktimeout) /* start visible … | |
+ win.mode |= MODE_BLINK; | |
+ win.mode ^= MODE_BLINK; | |
+ tsetdirtattr(ATTR_BLINK); | |
+ lastblink = now; | |
+ timeout = blinktimeout; | |
} | |
} | |
+ | |
+ draw(); | |
+ XFlush(xw.dpy); | |
+ drawing = 0; | |
} | |
} | |