sites

public wiki contents of suckless.org
git clone git://git.suckless.org/sites
Log | Files | Refs

commit d84fb45fc27cc8cc70f75e432a1e4af6c7f6e0d8
parent a4baec640d240068d6bec72c8671dedaff111cee
Author: Avi Halachmi (:avih) <avihpit@yahoo.com>
Date:   Mon, 27 Apr 2020 22:42:41 +0300

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

Diffstat:
Ast.suckless.org/patches/sync/index.md | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ast.suckless.org/patches/sync/st-appsync-0.8.3.diff | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ast.suckless.org/patches/sync/st-autosync-0.8.3.diff | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 572 insertions(+), 0 deletions(-)

diff --git a/st.suckless.org/patches/sync/index.md b/st.suckless.org/patches/sync/index.md @@ -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 @@ -0,0 +1,260 @@ +From 97bdda00d211f989ee42c02a08e96b41800544f4 Mon Sep 17 00:00:00 2001 +From: "Avi Halachmi (:avih)" <avihpit@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 "\033P=1s\033\\", "\033P=2s\033\\" 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 +@@ -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 +@@ -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) + { +@@ -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) + { +@@ -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: +@@ -834,7 +864,7 @@ ttyread(void) + case -1: + die("couldn't read from shell: %s\n", 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 */ +@@ -995,6 +1025,7 @@ tsetdirtattr(int attr) + void + tfulldirt(void) + { ++ tsync_end(); + tsetdirt(0, term.row-1); + } + +@@ -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; +@@ -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 */ +@@ -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 +@@ -188,6 +188,7 @@ st-mono| simpleterm monocolor, + Ms=\E]52;%p1%s;%p2%s\007, + 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 +@@ -1861,6 +1861,9 @@ resize(XEvent *e) + cresize(e->xconfigure.width, e->xconfigure.height); + } + ++int tinsync(uint); ++int ttyread_pending(); ++ + void + run(void) + { +@@ -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; +@@ -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; +@@ -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; +@@ -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 @@ -0,0 +1,229 @@ +From 1892290c3b0ef064083c8af4e4bec443a36ca5c8 Mon Sep 17 00:00:00 2001 +From: "Avi Halachmi (:avih)" <avihpit@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 +@@ -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 +@@ -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 { +@@ -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; + } + } + + +base-commit: 43a395ae91f7d67ce694e65edeaa7bbc720dd027 +-- +2.17.1 +