Skip to content

Drop unused raw input registration on Windows - #56

Merged
twlite merged 1 commit into
webviewjs:mainfrom
kdonev:fix/drop-unused-raw-input
Sep 23, 2026
Merged

twlite merged 1 commit into
webviewjs:mainfrom
kdonev:fix/drop-unused-raw-input

Conversation

@kdonev

@kdonev kdonev commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Environment

Windows 11 Pro 10.0.26200, WebView2 runtime 153.0.4234.48, Node 24.21.0,
@webviewjs/webview 0.4.6.

Symptom

Under sustained mouse-wheel input with Ctrl held, wheel events reached
the page progressively later - and the delay did not clear during idle time
between gestures. Plain wheel (no modifier) was unaffected under identical
conditions on the same machine.

Reproduction

Minimal reproduction, no external repo needed:

  1. In any @webviewjs/webview window with a page-side wheel listener,
    hold Ctrl and spin the mouse wheel. Events arrive progressively later,
    by up to several seconds, the longer the gesture continues.
  2. Or: hold Ctrl alone for ~5 seconds with no wheel input at all,
    release Ctrl, then immediately spin the wheel with no modifier. The
    first event afterward is late by seconds; the rest arrive normally.

A standalone repro with a visual arrival timeline (a rectangle that steps
once per wheel event, plus a 15-second arrival chart) is attached below as
webview-ctrl-wheel-repro.zip. Its README covers reproducing the bug against
the published 0.4.6 and verifying this fix against a local build of this
branch, including how to tell which binary actually loaded.
webview-ctrl-wheel-repro.zip

Isolating measurements

All of the following were measured on Windows / WebView2, in order:

1. The original symptom, via a low-level OS hook. A WH_MOUSE_LL hook
timestamping each physical wheel notch at the OS level, correlated against
performance.now() in the page at the moment its own wheel handler ran,
across six successive Ctrl+wheel gestures 2-3 seconds apart:

gesture OS-to-page-handler delay
1 17ms
2 313ms
3 1079ms
4 2135ms
5 3771ms
6 6180ms

The delay grew monotonically across the session rather than resetting
between gestures.

2. The crucial control, from the same six gestures. The delay from the
browser's own WheelEvent.timeStamp to the page's handler running was
0-1ms every time. Whatever was happening, it happened before the event
reached the browser's own event queue - nothing downstream (renderer, page
JS) was ever slow.

3. The pump loop's own health, instrumented directly. ~40 ticks/sec,
pumpEvents() under 3ms mean, identical whether the gesture was Ctrl+wheel
or plain wheel. This ruled out the JS-driven pump loop itself as the cause -
it was never slow, under either gesture.

4. Modifier breakdown, physical wheel, one gesture at a time:

  • Ctrl+wheel: lags. Plain wheel: does not.
  • Alt+wheel: does not lag either.
  • Shift+wheel produced no usable signal - Chromium turns it into horizontal
    scroll (deltaY stays 0) - inconclusive, not evidence either way.

5. Held-modifier-alone test. Holding Ctrl for 5 seconds with no wheel
input at all
, releasing, then immediately spinning plain wheel: the
first wheel event afterward lagged by seconds; the rest were immediate.
The held key alone was building a backlog, with no wheel combined with it at
all.

6. Real-browser control. The identical page, served to a plain Edge
window instead of the WebView2-hosted one: Ctrl+wheel and plain wheel
arrived at the same rate. This ruled out Chromium's own Ctrl+wheel
handling (e.g. its built-in zoom accelerator) as the cause - the divergence
is specific to this binding's host process, not to Chromium in general.

7. Two switches added to isolate the mechanism, tested one at a time:

  • Disabling WebView2's browser-accelerator-key handling
    (SetAreBrowserAcceleratorKeysEnabled(false)): lag unchanged. Rules out
    the "Ctrl is an accelerator key needing a host round trip" theory.
  • Disabling tao's raw input registration for all mice/keyboards
    (DeviceEventFilter::Always, i.e. RIDEV_REMOVE): lag gone entirely.
    Isolates the cause to raw input specifically.

8. A separate pumpEvents(timeoutMs) prototype (blocking the pump on
the OS message queue instead of a fixed clock) also removed the lag, but by
pumping fast enough to mask the backlog rather than removing its cause. Not
shipped.

Cause

By default (DeviceEventFilter::Unfocused, i.e. RIDEV_DEVNOTIFY) tao
registers raw input for all mice and keyboards, so while this window has
focus every keystroke - a held key's autorepeat included, roughly 30/sec -
posts WM_INPUT to this thread.

This binding never reads a tao DeviceEvent anywhere: the pump closures in
app.rs (both the Windows/Linux run_return path and the macOS
pump_events path) match only Event::WindowEvent, Event::MainEventsCleared
and Event::NewEvents, dropping everything else - including every
DeviceEvent - through a catch-all _ => {}.

So the registration was pure cost: on Windows, the resulting WM_INPUT
traffic - amplified by a held key's autorepeat - was backing up on this
thread's message queue, delaying whatever else was waiting on it, wheel
input included.

Fix

One call, right after the event loop is created in src/app.rs:

event_loop.set_device_event_filter(DeviceEventFilter::Always);

DeviceEventFilter::Always means "always filter device events" - tao's
Windows implementation responds by issuing RIDEV_REMOVE, so mice and
keyboards are never registered for raw input on this thread in the first
place.

Why this is safe

  • No code path in this crate, on any platform, ever reads a DeviceEvent.
    There is no behaviour to lose.
  • set_device_event_filter is tao's own public, documented API and already
    a no-op on non-Windows targets: its body is gated
    #[cfg(target_os = "windows")] internally
    (tao/src/event_loop.rs:285-288) - this change has zero effect on macOS,
    Linux, Android, or the FreeBSD stub build. It only stops asking for input
    that was never consumed, on the one platform where that registration was
    actually happening.
  • Minimal diff: one call plus an import, no public API change, no behaviour
    change to anything this binding actually uses.

Verified

  • cargo fmt -- --check, cargo clippy -- -D warnings, cargo check: clean.
  • Release build succeeds.
  • node --test __test__/*.test.*: 43/47 pass - the same 4 pre-existing
    failures as an untouched build of the commit this branches from, confirmed
    by building that commit standalone and running the identical suite; no new
    failures from this change.
  • Confirmed with a physical wheel on this exact build: Ctrl+wheel keeps pace
    with plain wheel, and holding Ctrl alone no longer delays the next wheel
    event. (It was first confirmed during isolation testing with a build that
    made the identical call behind a diagnostic switch - measurement 7.)

Not yet explained

Why Ctrl specifically shows the symptom and Alt does not is still
open - tao's raw keyboard path has nothing Ctrl-specific in it, and this
asymmetry is unexplained. It doesn't change whether this fix is correct or
sufficient for the reported symptom, but it may be worth a note to tao as
well.

🤖 Generated with Claude Code

Symptom: Ctrl+wheel input reaching a WebView2 child window lagged by up
to several seconds while Ctrl was held (growing the longer it was
held); plain wheel and Alt+wheel were unaffected.

Cause: by default (DeviceEventFilter::Unfocused, i.e. RIDEV_DEVNOTIFY)
tao registers raw input for all mice and keyboards, so while this
window has focus every keystroke - a held key's autorepeat included,
~30/sec - posts WM_INPUT to this thread. This binding never consumes a
tao DeviceEvent anywhere: the pump closures in app.rs match only
WindowEvent, MainEventsCleared and NewEvents, dropping everything else
via `_ => {}`. So that raw input registration was pure cost, and on
Windows the resulting WM_INPUT traffic backing up on this thread
delayed whatever else was waiting on the same queue, including input
headed for the WebView2 child.

Fix: call event_loop.set_device_event_filter(DeviceEventFilter::Always)
once, right after the event loop is created, so raw input is never
registered for this thread in the first place (tao issues RIDEV_REMOVE
for mice and keyboards on Windows).

Safe because the binding has no code path, on any platform, that reads
a DeviceEvent - there is no behaviour to lose. set_device_event_filter
is already a documented no-op on non-Windows targets (tao gates its own
body internally), so this changes behaviour only on Windows, and only
by no longer asking for input this binding never reads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@twlite twlite left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you

@twlite
twlite merged commit 40e459f into webviewjs:main Sep 23, 2026
13 checks passed
kdonev added a commit to kdonev/termscape that referenced this pull request Sep 27, 2026
0.4.7 carries webviewjs/webview#56, which unregisters the unused raw input
that made Ctrl+wheel zoom lag. The pump went back to its default.

Closes #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants