8000 fix(terminal): smooth touchpad scrolling + repaint after WebGL context loss by kiromoussa · Pull Request #218 · amirlehmam/wmux · GitHub
[go: up one dir, main page]

Skip to content

fix(terminal): smooth touchpad scrolling + repaint after WebGL context loss - #218

Merged
amirlehmam merged 2 commits into
amirlehmam:masterfrom
kiromoussa:fix/terminal-feel
Sep 5, 2026
Merged

amirlehmam merged 2 commits into
amirlehmam:masterfrom
kiromoussa:fix/terminal-feel

Conversation

@kiromoussa
Copy link
Copy Markdown
Contributor

Two independent terminal-feel fixes, one commit each:

1. Touchpad scrolling is jerky (fractional-line accumulation)

Pixel-precision devices fire many wheel events per second with tiny sub-line deltaY values, and wheelDeltaToLines's Math.max(1, round) forced every micro-event to a whole line — a gentle two-finger drag scrolled fast and jerky, line-at-a-time. Fix: a per-surface fractional-line accumulator carries the sub-line remainder between events and emits only whole lines, measured against the real cell height (host rect / rows) instead of a fixed 17px so the mapping tracks the user's font size. Notched wheels (DOM_DELTA_LINE/PAGE) keep the at-least-one-line-per-detent snap. The accumulator is cleaned up on surface reset alongside surfaceMouseModes.

2. Panes go blank after WebGL context loss

Chromium force-loses the oldest WebGL context past its ~16 cap — which happens exactly when panes are split/resized and several terminals flip visible at once. The existing onContextLoss handler correctly downgrades to the DOM renderer, but nothing repainted the buffer into it: the canvas the lost context left behind stayed blank until the next PTY write. This is the "text disappears when I resize/split panes" symptom. Fix: force a full terminal.refresh(0, rows-1) on the next frame once the DOM renderer is live.

Testing

  • npm run typecheck clean; npm test 2708 passed. (The one failure on my machine is the pre-existing resolveExistingShellPath pwsh test — no PowerShell 7 installed here; it fails identically on a clean master checkout.)
  • Tested live: touchpad scrolling in a terminal pane, and split/resize churn past the WebGL context budget.

Independent of #217 (both apply cleanly to master in either order).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cfhwse76gqxU7voD3iGCvt

kiromoussa and others added 2 commits September 4, 2026 21:11
…tion

Pixel-precision devices (touchpads, high-resolution mice) fire many
wheel events per second with sub-line deltaY values, and the old
Math.max(1, round) forced EVERY micro-event to a whole line — a gentle
two-finger drag became a fast, jerky line-at-a-time jump. Carry the
sub-line remainder between events in a per-surface accumulator and emit
only whole lines, measured against the REAL cell height rather than a
fixed 17px so the mapping tracks the user's font size. Notched wheels
(line/page delta modes) keep the at-least-one-line-per-detent snap.
The accumulator is dropped on surface reset alongside surfaceMouseModes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cfhwse76gqxU7voD3iGCvt
Chromium force-loses the oldest WebGL context past its ~16 cap, which
happens exactly when panes are split/resized and several terminals flip
visible at once. Disposing the WebGL addon reactivates xterm's DOM
renderer, but nothing repainted the existing buffer into it — the
canvas the lost context left behind stayed blank until the next PTY
write, which is the "text disappears when I resize/split panes" bug.
Force a full terminal.refresh once the DOM renderer is live (next
frame, so its layout has settled).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cfhwse76gqxU7voD3iGCvt
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@amirlehmam
Copy link
Copy Markdown
Owner

Merged — shipping in v2.9.3. Two genuinely independent fixes in one PR, one commit each, both correct; that's the right way to send this.

Scrolling. Math.max(1, round) forcing every sub-line micro-event to a whole line is a good catch, and the fix is better than just removing the clamp: keeping the snap for DOM_DELTA_LINE/PAGE preserves the detent feel on a notched wheel while the accumulator smooths pixel-precision devices. Measuring the cell height from the host rect instead of the fixed 17 is the part that will quietly help the most — that constant was wrong for every font size but the default.

WebGL context loss. This is the better of the two. The existing onContextLoss handler looked complete — it downgrades to the DOM renderer, which is the hard part — and the missing repaint is invisible until you notice the pane only comes back on the next PTY write. On an idle shell that's never, which is why this reads to users as "text disappears when I split panes" rather than as anything renderer-shaped. Pinning it to the ~16-context cap being hit exactly during split/resize churn is the detail that makes it diagnosable next time.

One thing I changed on merge: wheelLineAccum.delete() now also runs on the PTY-exit path, not only on wmux:reset-terminal. It's keyed by surfaceId and sits next to surfaceMouseModes, whose delete on that path carries a comment specifically about the map growing one entry per surface for the life of the process — same map shape, same problem. It's also right on its own terms: a fraction of a line owed to a gesture aimed at a now-dead process shouldn't be paid out to whatever opens on that surface next. Tiny, and entirely in the spirit of what you wrote.

For context: npm test here is 2716 passed including resolveExistingShellPath — that box just needs PowerShell 7, nothing was wrong with your run.

amirlehmam added a commit that referenced this pull request Sep 5, 2026
#219 promoted auto-open from a module-level runtime value to a persisted
pref — the right move, and the Settings toggle plus the config.toml
write-through land as merged. Only the default changes here.

Defaulting it OFF is a defensible policy for a NEW feature and the wrong
one for this one, because the behaviour predates the pref. Prefs persist
as whole blocks and a new field is filled from DEFAULTS
({...DEFAULTS, ...stored}), so `false` does not offer anybody a choice:
it silently switches auto-open off for every existing install on upgrade,
with no note attached. `openLinksExternally` two fields up already
documents exactly this argument. A pref meant to give the people it
bothers a way out should not take it from the people it doesn't.

The Settings hint and en.ts string are corrected to match, and a test
pins the default plus the merge that depends on it — the value is one
word that reads like an oversight, which is the shape of thing a later
tidy-up flips back without noticing.

Also drops wheelLineAccum on the PTY-exit path (#218 follow-up). It was
cleared only on `wmux:reset-terminal`, but it is keyed by surfaceId and
sits beside surfaceMouseModes, whose delete on that same path carries a
comment about the map growing one entry per surface for the life of the
process. Correct on its own terms too: a fraction of a line owed to a
gesture aimed at a dead process should not be paid to whatever opens
on that surface next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mu39594ynryhibh6AfixjX
amirlehmam added a commit that referenced this pull request Sep 11, 2026
…ater (#229)

After ~2.5 days the reporter's window kept its last frame while every PTY
lived on: Responding=True, 0% CPU everywhere, renderer paged down to 3 MB.
Killing ONLY the GPU process restored rendering with the same renderer PID
and nothing lost. The wedge is Chromium's display compositor on a hybrid
NVIDIA/Intel + DisplayLink machine, with no repro; what wmux can own is the
remedy.

Detector (renderer/utils/gpu-watchdog.ts): requestAnimationFrame runs on
BeginFrames from the GPU process, so when it wedges rAF stops while
setTimeout keeps going. Three unanswered requests 5 s apart while the
document is visible is a report. Misses while hidden are discarded.

Decision (main/gpu-watchdog.ts): the renderer never acts. Main restarts the
GPU process only when the window is FOCUSED, the OS reports input within
30 s (powerMonitor.getSystemIdleTime), and no restart happened in the last
5 min - Chromium counts each killed GPU process as a crash and drops to
software rendering after a few. process.kill on the GPU entry of
app.getAppMetrics(); Chromium relaunches it, and the #218 context-loss path
repaints every pane. gpu-stall / gpu-restart / gpu-gone go to main.log.
Off switch: gpuWatchdog: false in wmux-workspace-prefs.

The update prompt's default button is now 'Later': it opens over a terminal
the user is typing into, and Enter there installed the update and quit
every live session.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtqBese2n4qh7cZpVx8H5A
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.

2 participants

0