Skip to content

fix: stop ~0.6 s terminal echo lag on Linux WebKitGTK (#224) - #313

Merged
kipavy merged 1 commit into
devfrom
fix/224-linux-input-latency
Sep 18, 2026
Merged

kipavy merged 1 commit into
devfrom
fix/224-linux-input-latency

Conversation

@kipavy

@kipavy kipavy commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #224.

What

On Linux, the xterm WebGL addon is now created with preserveDrawingBuffer: true. This goes through a small createWebglAddon() helper used by both terminal views (useTerminal.ts and MultiplayerTerminalView.tsx). Other platforms keep xterm's default.

Why

With WEBKIT_DISABLE_DMABUF_RENDERER=1, WebKitGTK shows a WebGL canvas one draw behind when the canvas has antialias: false (as xterm's does) and preserveDrawingBuffer: false. A typed character was only drawn to the screen on xterm's next redraw, which with cursor blink on is the 600 ms blink tick. NVIDIA users always end up on this path, because the DMABUF renderer fails on NVIDIA and linux_gfx.rs turns it off. Upstream bug: https://bugs.webkit.org/show_bug.cgi?id=324549

Keydown → IPC → PTY → xterm render takes 5–16 ms. All of the delay came after xterm had drawn.

Testing

Tested on an RTX 5070 with driver 610.57.04, KDE 6.7.4 and webkit2gtk-4.1 2.52.6. Latency was measured by injecting keys and polling screen pixels.

  • Echo latency before: 528–652 ms. After: 14–37 ms.
  • With cursor blink off (the worst case), each key used to reveal the previous key's echo. Now each key shows its own.
  • Typing on native Wayland with blink on felt instant, with no rendering artifacts.
  • Heavy output (seq 1 1000000): 1.99 s with the fix vs 2.09 s without, so no slowdown.
  • pnpm run build passes; its one chunk-size warning is also present on dev.
  • No Rust changes.

🤖 Generated with Claude Code

WebKitGTK's non-DMABUF path (forced on NVIDIA) presents a WebGL canvas
one draw behind, so terminal echo only showed on the next cursor-blink
redraw (~600 ms). Creating the xterm WebGL addon with
preserveDrawingBuffer on Linux presents each frame immediately.
@kipavy

kipavy commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Investigation report for #224 (supporting PR #313)

TL;DR

  • Where the delay is: after xterm has already drawn. It's in how WebKitGTK puts the WebGL canvas on screen. Input delivery, Tauri IPC, the PTY, and xterm parsing/rendering take 5–16 ms per key combined.
  • Root cause (evidence below): webkit2gtk-4.1 (2.52.6) with the DMABUF renderer disabled (WEBKIT_DISABLE_DMABUF_RENDERER=1) presents a WebGL canvas one draw behind when it has antialias: false (as xterm's does) and preserveDrawingBuffer: false (xterm's default). With the default antialias: true it displays on time. The echo therefore only appears when xterm next redraws the canvas. With cursor blink on, that is the 600 ms blink tick (BLINK_INTERVAL = 600 in addon-webgl), hence the steady ~0.6 s. It is not NVIDIA-specific: it reproduces with Mesa (no NVIDIA libraries loaded) when DMABUF is off. NVIDIA users hit it because the DMABUF renderer fails on NVIDIA (blank window / Wayland protocol error), which forces the fallback. src-tauri/src/linux_gfx.rs sets that fallback automatically on NVIDIA.
  • Fix: create xterm's WebglAddon with preserveDrawingBuffer = true on Linux. Measured echo latency drops from ~630 ms to 14–37 ms. A human tester confirmed it feels instant on native Wayland with blink on. Heavy-output throughput is unchanged.

Environment

OS CachyOS live USB, kernel 7.1.6-1-cachyos
Desktop KDE Plasma 6.7.4 / KWin 6.7.4, Wayland session (Xwayland on :0)
GPU / driver NVIDIA GeForce RTX 5070, 610.57.04 (nvidia-open, nvidia-utils 610.57.04-1), egl-wayland 1.1.21, egl-wayland2 1.0.1
Monitor 1× HDMI 1920×1080
WebKit webkit2gtk-4.1 2.52.6-1 (same version as the reporter), gtk3 3.24.52, mesa 26.1.6
Voltius AppImage v0.39.1 (sha256 verified); source dev @ c7aad8e (0.39.1), debug build
Toolchain rustc 1.98.1, node 26.9.0, pnpm 11.26

Method

  • In-app probes (temporary, removed): epoch-µs timestamps at DOM keydown, xterm onData, invoke start/resolve, Rust command received, PTY write, PTY read, emit, JS listen callback, term.write callback, next rAF, xterm onRender. Also an rAF-per-second counter and a main-thread stall detector. All printed to stdout via a temporary lat_log command.
  • Screen probe: a small X11 tool (XTest key injection plus XGetImage polling about every 1 ms). It logs when window pixels change after an injected key and correlates that with the in-app timestamps. Requires GDK_BACKEND=x11, where the reporters also see the latency.
  • Standalone WebKit repro, no Voltius: a page in webkit2gtk-4.1's MiniBrowser with a WebGL canvas and a DOM box set to the same color each second. A sampler compares the two on screen; a mismatch means the canvas is stale. Each variant is a separate HTML file, because MiniBrowser drops #hash and ?query from command-line file:// URLs (see the correction below).

Test table

# Test Result
1 AppImage v0.39.1, Wayland, defaults (linux_gfx sets DMABUF off on NVIDIA) Reproduces: tester rated local terminal echo at ~0.5 s. Settings/OmniSearch text input: instant. UI animations: smooth.
2 Dev build + probes, Wayland (DMABUF off auto) Tester confirmed ~0.5 s. rAF 62 fps steady. Per key (15 keys): keydown→…→xterm render = 5–16 ms; invoke round trip ~1 ms. Main-thread stalls (150–320 ms) only at startup, none while typing.
3 Screen probe, X11, WebGL renderer, blink on key→JS keydown 1–4 ms; xterm render 5–16 ms; pixels change +528…652 ms (Enter: 634/646/652).
4 Same, xterm DOM renderer (WebGL addon skipped) Pixels change +12…30 ms (8/8 keys).
5 Same as 3, re-check 631–646 ms, near-constant. That looks like a timeout or next-redraw effect, not throttling.
6 Same as 3, cursor blink off Each key reveals the previous key's echo (+12…26 ms); the first key shows nothing. The canvas is one draw behind.
7 MiniBrowser, NVIDIA, DMABUF on, X11 Blank page, Failed to create GBM buffer … Invalid argument
8 MiniBrowser, NVIDIA, DMABUF off, key-driven WebGL page Color readout: each key shows the previous color (8/8). Initial draw never visible. Reproduces without Voltius.
9 Auto page, NVIDIA, DMABUF off: env discriminators (120 samples each) Plain 100% stale. WEBKIT_DISABLE_COMPOSITING_MODE=1 100% · __GL_SYNC_TO_VBLANK=0 100% · __GL_THREADED_OPTIMIZATIONS=0 100% · both GL vars 100% · WEBKIT_SKIA_ENABLE_CPU_RENDERING=1 100%
10 Auto page, NVIDIA, DMABUF off: page variants preserveDrawingBuffer:true 0% · redraw again next rAF 2% (~1 frame) · redraw after 50/200 ms 5%/19% (≈ the delay) · canvas opacity nudge same/next frame 100% · 2D canvas (accel on/off) 0%
11 Tester's visual check of the auto page, X11 and native Wayland "Left (WebGL) lags right (DOM)" on both, which confirms the sampler
12 Fix in Voltius, X11, blink off (worst case), screen probe Each key shows its own echo at +14…37 ms (8/8). Before the fix, the echo only appeared on the next key.
13 EGL vendor check via /proc/<WebProcess>/maps With __EGL_VENDOR_LIBRARY_FILENAMES=…/50_mesa.json, only libEGL_mesa + libgallium were mapped (no NVIDIA). Mesa + DMABUF off: 100% stale. Mesa + DMABUF on: 0%. Mesa + DMABUF off + preserveDrawingBuffer: 0%.
14 MiniBrowser, NVIDIA, DMABUF on, native Wayland Exits: Gdk-Message: Error 71 (Protocol error) dispatching to Wayland display
15 Fix, native Wayland, cursor blink on, tester typing "Instant", rendering "looks normal"
16 Heavy output seq 1 1000000, Wayland, 1 run each With fix: last write→render 1.99 s, fps 40/25/54. Without: 2.09 s, fps 49/15/45. No measurable regression.
— KWin tearing / compositor latency; Plasma X11 session; nouveau reboot Not run. The stale frame already exists inside the web process: it reproduces under Xwayland and native Wayland, with Mesa, and the app hands KWin the old content promptly. Compositor settings can't change which buffer WebKit presents, and test 13 already rules out the NVIDIA driver, which is what the nouveau reboot was meant to decide.

Correction recorded during the investigation: a first round of page-variant runs used #hash URLs. MiniBrowser drops the fragment (and any query string) from command-line file:// URLs, so those runs actually loaded the plain page. They were discarded and redone with one file per variant (rows 10 and 13). The env-var rows (9) were plain-page tests anyway, so they remain valid.

Which hop carries the latency

After xterm's render call returns. The in-app pipeline takes 5–16 ms per key; the remaining ~600 ms is WebKit presenting the WebGL canvas's previous buffer until the next canvas draw.

hop time
key → DOM keydown 1–4 ms
keydown → Rust command → PTY write → PTY read → emit → JS listener ~1–2 ms
term.write → xterm WebGL render 3–15 ms
xterm render → pixels on screen ~600 ms (waits for the 600 ms blink redraw); 10–20 ms with the fix

Root cause

webkit2gtk-4.1 2.52.6, when the DMABUF renderer is disabled, presents a WebGL canvas created with preserveDrawingBuffer: false one draw behind (tests 6, 8, 10, 13). A 2D canvas and normal DOM content aren't affected (tests 1, 4, 10). That explains why text inputs feel instant and why "the terminal lags but the UI doesn't".

  • It is driver-independent (test 13, Mesa). NVIDIA users hit it because the DMABUF path is broken for them (tests 7, 14), so linux_gfx.rs or the user sets WEBKIT_DISABLE_DMABUF_RENDERER=1.
  • That also explains why none of the reporters' env-var experiments moved the input latency: every working configuration on NVIDIA uses the DMABUF-off path.
  • Exactly why WebKit's non-DMABUF path swaps late is unknown: I did not trace WebKit internals. The behavior is established empirically; the internal mechanism is not.

Notes on the issue thread:

  • The reporter's "removing @xterm/addon-webgl: no change" does not match this machine: the DOM renderer measured 12–30 ms (test 4). I can't explain the discrepancy from here.
  • The separate "whole UI low FPS" symptom did not reproduce here: the UI was smooth with DMABUF off, which Voltius applies automatically on NVIDIA. It was not investigated further.

Fix

On branch fix/224-linux-input-latency (committed locally, not pushed).

  • New src/utils/webglAddon.ts: createWebglAddon() returns new WebglAddon(preserveDrawingBuffer), with the flag on when the UA is Linux and not Android.
    • The check is a sync UA test because the addon is loaded synchronously and getPlatform() is async.
    • WebKitGTK reports X11; Linux x86_64 on Wayland too (verified).
  • useTerminal.ts and MultiplayerTerminalView.tsx now use the helper. These are the only two WebGL sites.
  • It also helps on DMABUF-on (non-NVIDIA) Linux, where it's harmless (test 13: 0% stale either way). macOS, Windows and Android keep xterm's default.
  • Checks: tsc --noEmit passes. vitest run src/hooks src/components/terminal gives 114 failed / 200 passed on both dev and the fix: an identical failure set, pre-existing, from localStorage being undefined in this machine's Node 26 test env.
diff --git a/src/components/terminal/MultiplayerTerminalView.tsx b/src/components/terminal/MultiplayerTerminalView.tsx
--- a/src/components/terminal/MultiplayerTerminalView.tsx
+++ b/src/components/terminal/MultiplayerTerminalView.tsx
@@ -1,7 +1,7 @@
-import { WebglAddon } from "@xterm/addon-webgl";
+import { createWebglAddon } from "@/utils/webglAddon";
@@ -47,7 +47,7 @@
-        term.loadAddon(new WebglAddon());
+        term.loadAddon(createWebglAddon());
diff --git a/src/hooks/useTerminal.ts b/src/hooks/useTerminal.ts
--- a/src/hooks/useTerminal.ts
+++ b/src/hooks/useTerminal.ts
@@ -2,7 +2,7 @@
-import { WebglAddon } from "@xterm/addon-webgl";
+import { createWebglAddon } from "@/utils/webglAddon";
@@ -1046,7 +1046,7 @@
-        term.loadAddon(new WebglAddon());
+        term.loadAddon(createWebglAddon());
diff --git a/src/utils/webglAddon.ts b/src/utils/webglAddon.ts
new file mode 100644
--- /dev/null
+++ b/src/utils/webglAddon.ts
@@ -0,0 +1,10 @@
+import { WebglAddon } from "@xterm/addon-webgl";
+
+// WebKitGTK presents a WebGL canvas one draw behind unless the buffer is preserved,
+// so terminal echo only appears on the next redraw (cursor blink, ~600 ms). #224
+const PRESERVE_DRAWING_BUFFER =
+  typeof navigator !== "undefined" && /Linux/.test(navigator.userAgent) && !/Android/i.test(navigator.userAgent);
+
+export function createWebglAddon(): WebglAddon {
+  return new WebglAddon(PRESERVE_DRAWING_BUFFER);
+}

Upstream

Filed as https://bugs.webkit.org/show_bug.cgi?id=324549 (repro page attached there).

Standalone repro page

Save the page below as a.html and run WEBKIT_DISABLE_DMABUF_RENDERER=1 /usr/lib/webkit2gtk-4.1/MiniBrowser -p file:///path/a.html. The left (WebGL) square shows the previous color of the right (DOM) square. With preserveDrawingBuffer:true, the two match.

<!doctype html><title>webgl-auto</title>
<style>body{margin:0;background:#123456}#c,#s{position:absolute;top:20px;width:100px;height:100px}#c{left:20px}#s{left:140px}</style>
<canvas id=c width=100 height=100></canvas><div id=s></div>
<script>
const gl=c.getContext('webgl2',{antialias:false,preserveDrawingBuffer:false});
const cols=['ff0000','00ff00','0000ff','ffff00','00ffff','ff00ff','ffffff','000000'];let i=0;
function step(){const h=cols[i++%8];requestAnimationFrame(()=>{s.style.background='#'+h;
 const v=[0,2,4].map(k=>parseInt(h.substr(k,2),16)/255);gl.clearColor(v[0],v[1],v[2],1);gl.clear(gl.COLOR_BUFFER_BIT);});}
step();setInterval(step,1000);
</script>

Follow-up finding

The DMABUF renderer's failure on NVIDIA + Wayland is an explicit-sync protocol error (wp_linux_drm_syncobj_surface_v1 error 4, "no acquire point"). With __NV_DISABLE_EXPLICIT_SYNC=1 the DMABUF renderer works in MiniBrowser and WebGL is presented on time. Reported upstream separately. Keeping DMABUF on for Wayland + NVIDIA may be a better long-term fix; that is not yet tested in Voltius.

@kipavy

kipavy commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Related: #315 keeps the DMABUF renderer on native Wayland + NVIDIA by setting __NV_DISABLE_EXPLICIT_SYNC=1. On that path WebGL frames arrive on time, so this workaround isn't needed there.

This PR is still needed wherever DMABUF stays off:

  • X11 + NVIDIA. DMABUF gives a blank page there. Measured echo: 559–640 ms without this PR, 22–34 ms with it.
  • Every AppImage user, since the AppImage always runs on X11.
  • Anyone who sets WEBKIT_DISABLE_DMABUF_RENDERER=1 themselves.

The two PRs are independent and can merge in either order.

@kipavy
kipavy merged commit 0ebafc0 into dev Sep 18, 2026
4 checks passed
@kipavy
kipavy deleted the fix/224-linux-input-latency branch September 18, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant