[wiki] [sites] [st][patch][sync] improved/controlled draw timing to reduce flicker || Avi Halachmi (:avih)

From: <git_AT_suckless.org>
Date: Mon, 27 Apr 2020 22:44:16 +0200

commit d84fb45fc27cc8cc70f75e432a1e4af6c7f6e0d8
Author: Avi Halachmi (:avih) <avihpit_AT_yahoo.com>
Date: Mon Apr 27 22:42:41 2020 +0300

    [st][patch][sync] improved/controlled draw timing to reduce flicker

diff --git a/st.suckless.org/patches/sync/index.md b/st.suckless.org/patches/sync/index.md
new file mode 100644
index 00000000..19166ab3
--- /dev/null
+++ b/st.suckless.org/patches/sync/index.md
_AT_@ -0,0 +1,83 @@
+Synchronized rendering
+======================
+
+Summary
+-------
+Better draw timing to reduce flicker/tearing and improve animation smoothness.
+
+Background
+----------
+
+Terminals have to guess when to draw and refresh the screen. This is because
+the terminal doesn't know whether the application has completed a "batch" of
+output, or whether it's about to have more output right after the refresh.
+
+This means that sometimes the terminal draws before the application has
+completed an output "batch", and usually this results in flicker or tearing.
+
+In st, the parameters which control the timing are `xfps` and `actionfps`.
+`xfps` determines how long st waits before drawing after interactive X events
+(KB/mouse), and `actionfps` determines the draw frequency for output which
+doesn't follow X events - i.e. unattended output - e.g. during animation.
+
+
+Part 1: auto-sync
+-----------------
+
+This patch replaces the timing algorithm and uses a range instead of fixed
+timing values. The range gives it the flexibility to choose when to draw, and
+it tries to draw once an output "batch" is complete, i.e. when there's some
+idle period where no new output arrived. Typically this eliminates flicker and
+tearing almost completely.
+
+The range is defined with the new configuration values `minlatency` and
+`maxlatency` (which replace xfps/actionfps), and you should ensure they're at
+your `config.h` file.
+
+This range has equal effect for both X events and unattended output; it doesn't
+care what the trigger was, and only cares when idle arrives. Interactively idle
+usually arrives very quickly so latency is near `minlatency`, while for
+animation it might take longer until the application completes its output.
+`maxlatency` is almost never reached, except e.g. during `cat huge.txt` where
+idle never happens until the whole file was printed.
+
+Note that the interactive timing (mouse/KB) was fine before this patch, so the
+main improvement is for animation e.g. `mpv --vo=tct`, `cava`, terminal games,
+etc, but interactive timing also benefits from this flexibility.
+
+Part 2: application-sync
+------------------------
+
+The problem of draw timing is not unique to st. All terminals have to deal
+with it, and a new suggested standard tries to solve it. It's called
+"Synchronized Updates" and it allows the application to tell the terminal when
+the output "batch" is complete so that the terminal knows not to draw partial
+output - hence "application sync".
+
+The suggestion - by iTerm2 author - is available here:
+https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
+
+This patch adds synchronized-updates/application-sync support in st. It
+requires the auto-sync patch above installed first. This patch has no effect
+except when an application uses the synchronized-update escape sequences.
+
+Note that currently there are very few terminals or applications which support
+it, but one application which does support it is `tmux` since 2020-04-18. With
+this patch nearly all cursor flicker is eliminated in tmux, and tmux detects
+it automatically via terminfo and enables it when st is installed correctly.
+
+
+Download
+--------
+Part 1 is independent, but part 2 needs part 1 first. Both files are git
+patches and can be applied with either `git am` or with `patch`. Both files
+add values at `config.def.h`, and part 2 also updates `st.info`.
+
+* Part 1: [st-autosync-0.8.3.diff](st-autosync-0.8.3.diff)
+* Part 2: [st-appsync-0.8.3.diff](st-appsync-0.8.3.diff)
+
+
+Author
+------
+* Avi Halachmi (:avih) - [https://github.com/avih](https://github.com/avih)
+ Contact email is available inside the patch files.
diff --git a/st.suckless.org/patches/sync/st-appsync-0.8.3.diff b/st.suckless.org/patches/sync/st-appsync-0.8.3.diff
new file mode 100644
index 00000000..ae1758c1
--- /dev/null
+++ b/st.suckless.org/patches/sync/st-appsync-0.8.3.diff
_AT_@ -0,0 +1,260 @@
+From 97bdda00d211f989ee42c02a08e96b41800544f4 Mon Sep 17 00:00:00 2001
+From: "Avi Halachmi (:avih)" <avihpit_AT_yahoo.com>
+Date: Sat, 18 Apr 2020 13:56:11 +0300
+Subject: [PATCH] application-sync: support Synchronized-Updates
+
+See https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
+
+In a nutshell: allow an application to suspend drawing until it has
+completed some output - so that the terminal will not flicker/tear by
+rendering partial content. If the end-of-suspension sequence doesn't
+arrive, the terminal bails out after a timeout (default: 200 ms).
+
+The feature is supported and pioneered by iTerm2. There are probably
+very few other terminals or applications which support this feature
+currently.
+
+One notable application which does support it is tmux (master as of
+2020-04-18) - where cursor flicker is completely avoided when a pane
+has new content. E.g. run in one pane: `while :; do cat x.c; done'
+while the cursor is at another pane.
+
+The terminfo string `Sync' added to `st.info' is also a tmux extension
+which tmux detects automatically when `st.info` is installed.
+
+Notes:
+
+- Draw-suspension begins on BSU sequence (Begin-Synchronized-Update),
+ and ends on ESU sequence (End-Synchronized-Update).
+
+- BSU, ESU are "P=1s\", "P=2s\" respectively (DCS).
+
+- SU doesn't support nesting - BSU begins or extends, ESU always ends.
+
+- ESU without BSU is ignored.
+
+- BSU after BSU extends (resets the timeout), so an application could
+ send BSU in a loop and keep drawing suspended - exactly like it can
+ not-draw anything in a loop. But as soon as it exits/aborted then
+ drawing is resumed according to the timeout even without ESU.
+
+- This implementation focuses on ESU and doesn't really care about BSU
+ in the sense that it tries hard to draw exactly once ESU arrives (if
+ it's not too soon after the last draw - according to minlatency),
+ and doesn't try to draw the content just up-to BSU. These two sides
+ complement eachother - not-drawing on BSU increases the chance that
+ ESU is not too soon after the last draw. This approach was chosen
+ because the application's main focus is that ESU indicates to the
+ terminal that the content is now ready - and that's when we try to
+ draw.
+---
+ config.def.h | 6 ++++++
+ st.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++--
+ st.info | 1 +
+ x.c | 22 +++++++++++++++++++---
+ 4 files changed, 72 insertions(+), 5 deletions(-)
+
+diff --git a/config.def.h b/config.def.h
+index fdbacfd..d44c28e 100644
+--- a/config.def.h
++++ b/config.def.h
+_AT_@ -52,6 +52,12 @@ int allowaltscreen = 1;
+ static double minlatency = 8;
+ static double maxlatency = 33;
+
++/*
++ * Synchronized-Update timeout in ms
++ * https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
++ */
++static uint su_timeout = 200;
++
+ /*
+ * blinking timeout (set to 0 to disable blinking) for the terminal blinking
+ * attribute.
+diff --git a/st.c b/st.c
+index 0ce6ac2..d53b882 100644
+--- a/st.c
++++ b/st.c
+_AT_@ -232,6 +232,33 @@ static uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
+ static Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
+ static Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
+
++#include <time.h>
++static int su = 0;
++struct timespec sutv;
++
++static void
++tsync_begin()
++{
++ clock_gettime(CLOCK_MONOTONIC, &sutv);
++ su = 1;
++}
++
++static void
++tsync_end()
++{
++ su = 0;
++}
++
++int
++tinsync(uint timeout)
++{
++ struct timespec now;
++ if (su && !clock_gettime(CLOCK_MONOTONIC, &now)
++ && TIMEDIFF(now, sutv) >= timeout)
++ su = 0;
++ return su;
++}
++
+ ssize_t
+ xwrite(int fd, const char *s, size_t len)
+ {
+_AT_@ -818,6 +845,9 @@ ttynew(char *line, char *cmd, char *out, char **args)
+ return cmdfd;
+ }
+
++static int twrite_aborted = 0;
++int ttyread_pending() { return twrite_aborted; }
++
+ size_t
+ ttyread(void)
+ {
+_AT_@ -826,7 +856,7 @@ ttyread(void)
+ int ret, written;
+
+ /* append read bytes to unprocessed bytes */
+- ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
++ ret = twrite_aborted ? 1 : read(cmdfd, buf+buflen, LEN(buf)-buflen);
+
+ switch (ret) {
+ case 0:
+_AT_@ -834,7 +864,7 @@ ttyread(void)
+ case -1:
+ die("couldn't read from shell: %s
", strerror(errno));
+ default:
+- buflen += ret;
++ buflen += twrite_aborted ? 0 : ret;
+ written = twrite(buf, buflen, 0);
+ buflen -= written;
+ /* keep any incomplete UTF-8 byte sequence for the next call */
+_AT_@ -995,6 +1025,7 @@ tsetdirtattr(int attr)
+ void
+ tfulldirt(void)
+ {
++ tsync_end();
+ tsetdirt(0, term.row-1);
+ }
+
+_AT_@ -1901,6 +1932,12 @@ strhandle(void)
+ return;
+ case 'P': /* DCS -- Device Control String */
+ term.mode |= ESC_DCS;
++ /* https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */
++ if (strstr(strescseq.buf, "=1s") == strescseq.buf)
++ tsync_begin(), term.mode &= ~ESC_DCS; /* BSU */
++ else if (strstr(strescseq.buf, "=2s") == strescseq.buf)
++ tsync_end(), term.mode &= ~ESC_DCS; /* ESU */
++ return;
+ case '_': /* APC -- Application Program Command */
+ case '^': /* PM -- Privacy Message */
+ return;
+_AT_@ -2454,6 +2491,9 @@ twrite(const char *buf, int buflen, int show_ctrl)
+ Rune u;
+ int n;
+
++ int su0 = su;
++ twrite_aborted = 0;
++
+ for (n = 0; n < buflen; n += charsize) {
+ if (IS_SET(MODE_UTF8) && !IS_SET(MODE_SIXEL)) {
+ /* process a complete utf8 char */
+_AT_@ -2464,6 +2504,10 @@ twrite(const char *buf, int buflen, int show_ctrl)
+ u = buf[n] & 0xFF;
+ charsize = 1;
+ }
++ if (su0 && !su) {
++ twrite_aborted = 1;
++ break; // ESU - allow rendering before a new BSU
++ }
+ if (show_ctrl && ISCONTROL(u)) {
+ if (u & 0x80) {
+ u &= 0x7f;
+diff --git a/st.info b/st.info
+index e2abc98..0a781db 100644
+--- a/st.info
++++ b/st.info
+_AT_@ -188,6 +188,7 @@ st-mono| simpleterm monocolor,
+ Ms=\E]52;%p1%s;%p2%s,
+ Se=\E[2 q,
+ Ss=\E[%p1%d q,
++ Sync=\EP=%p1%ds\E\,
+
+ st| simpleterm,
+ use=st-mono,
+diff --git a/x.c b/x.c
+index cbbd11f..38b08a8 100644
+--- a/x.c
++++ b/x.c
+_AT_@ -1861,6 +1861,9 @@ resize(XEvent *e)
+ cresize(e->xconfigure.width, e->xconfigure.height);
+ }
+
++int tinsync(uint);
++int ttyread_pending();
++
+ void
+ run(void)
+ {
+_AT_@ -1895,7 +1898,7 @@ run(void)
+ FD_SET(ttyfd, &rfd);
+ FD_SET(xfd, &rfd);
+
+- if (XPending(xw.dpy))
++ if (XPending(xw.dpy) || ttyread_pending())
+ timeout = 0; /* existing events might not set xfd */
+
+ seltv.tv_sec = timeout / 1E3;
+_AT_@ -1909,7 +1912,8 @@ run(void)
+ }
+ clock_gettime(CLOCK_MONOTONIC, &now);
+
+- if (FD_ISSET(ttyfd, &rfd))
++ int ttyin = FD_ISSET(ttyfd, &rfd) || ttyread_pending();
++ if (ttyin)
+ ttyread();
+
+ xev = 0;
+_AT_@ -1933,7 +1937,7 @@ run(void)
+ * 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 (ttyin || xev) {
+ if (!drawing) {
+ trigger = now;
+ drawing = 1;
+_AT_@ -1944,6 +1948,18 @@ run(void)
+ continue; /* we have time, try to find idle */
+ }
+
++ if (tinsync(su_timeout)) {
++ /*
++ * on synchronized-update draw-suspension: don't reset
++ * drawing so that we draw ASAP once we can (just after
++ * ESU). it won't be too soon because we already can
++ * draw now but we skip. we set timeout > 0 to draw on
++ * SU-timeout even without new content.
++ */
++ timeout = minlatency;
++ continue;
++ }
++
+ /* idle detected or maxlatency exhausted -> draw */
+ timeout = -1;
+ if (blinktimeout && tattrset(ATTR_BLINK)) {
+
+base-commit: 43a395ae91f7d67ce694e65edeaa7bbc720dd027
+prerequisite-patch-id: d7d5e516bc74afe094ffbfc3edb19c11d49df4e7
+--
+2.17.1
+
diff --git a/st.suckless.org/patches/sync/st-autosync-0.8.3.diff b/st.suckless.org/patches/sync/st-autosync-0.8.3.diff
new file mode 100644
index 00000000..2c02ce8b
--- /dev/null
+++ b/st.suckless.org/patches/sync/st-autosync-0.8.3.diff
_AT_@ -0,0 +1,229 @@
+From 1892290c3b0ef064083c8af4e4bec443a36ca5c8 Mon Sep 17 00:00:00 2001
+From: "Avi Halachmi (:avih)" <avihpit_AT_yahoo.com>
+Date: Tue, 26 Feb 2019 22:37:49 +0200
+Subject: [PATCH] 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
+tear, 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).
+---
+ config.def.h | 11 +++--
+ x.c | 120 ++++++++++++++++++++++++---------------------------
+ 2 files changed, 65 insertions(+), 66 deletions(-)
+
+diff --git a/config.def.h b/config.def.h
+index 0895a1f..fdbacfd 100644
+--- a/config.def.h
++++ b/config.def.h
+_AT_@ -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
+index e5f1737..cbbd11f 100644
+--- a/x.c
++++ b/x.c
+_AT_@ -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 {
+_AT_@ -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
", 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;
+ }
+ }
+
+
+base-commit: 43a395ae91f7d67ce694e65edeaa7bbc720dd027
+--
+2.17.1
+
Received on Mon Apr 27 2020 - 22:44:16 CEST

This archive was generated by hypermail 2.3.0 : Mon Apr 27 2020 - 22:48:45 CEST