Summary
With Touchscreen.DisableOnStylus = true, one physical multi-touch
gesture can be split into multiple independent touch sequences shortly
after using the stylus.
This was most visible in Krita: one intentional two-finger Undo gesture
occasionally triggered Undo three times. Raw evdev and internal iptsd
instrumentation showed that Krita correctly received three separate
two-contact sequences. The gesture was already fragmented before it
reached Krita.
Environment
- Device: Microsoft Surface Pro 7
- Kernel: Linux 6.19.8
- Distribution: NixOS 26.05.20260722.b3fe958
- iptsd: 3.1.0
- Application used for reproduction: Krita 6.0.2.1
- Stylus: Microsoft Surface Pen, Model 1776
Relevant configuration:
[Touchscreen]
DisableOnPalm = true
DisableOnStylus = true
My build also contains a local 300 ms post-stylus touch holdoff. That
change only delays touch re-enabling; it does not alter the unconditional
m_touch->disable() call described below.
Steps to reproduce
- Enable
Touchscreen.DisableOnStylus.
- Draw a few strokes with the stylus.
- Move the stylus out of proximity.
- Perform one two-finger tap on the touchscreen.
- Observe the touch events with evdev, or bind a two-finger tap to Undo
in Krita.
The problem is timing-dependent, so it may take several attempts.
Expected behavior
One physical two-finger tap should be emitted as one continuous touch
sequence and trigger one Undo.
Actual behavior
One physical two-finger tap can be emitted as several independent
two-contact sequences, triggering Undo multiple times.
In one raw evdev capture, a single physical tap was emitted as:
two contacts down 11.157549
two contacts up 11.160090
two contacts down 11.166247
two contacts up 11.166679
two contacts down 11.188842
two contacts up 11.194638
Krita received three corresponding TouchBegin / TouchEnd pairs.
I also added temporary instrumentation after search_lifted(). During
another reproduction it reported:
16:03:18.942 contacts=2 blocked=false current=2 last=0
16:03:18.966 contacts=2 blocked=false current=2 last=0
16:03:18.972 contacts=2 blocked=false current=2 last=0
Both contacts were valid=1 and stable=1. There were no zero-contact
frames between these entries, and palm blocking was never active.
Nevertheless, the previous-contact state was cleared between frames.
Cause
Daemon::on_stylus() disables the touch device for every stylus sample,
without checking stylus.proximity:
https://github.com/linux-surface/iptsd/blob/v3.1.0/src/apps/daemon/daemon.hpp#L79-L90
TouchDevice::disable() lifts all active contacts and clears all contact
tracking state:
https://github.com/linux-surface/iptsd/blob/v3.1.0/src/apps/daemon/touch.hpp#L166-L179
Therefore, a non-proximity stylus sample arriving during a touch gesture
produces an artificial touch release. The following touch frame sees the
stylus as inactive, re-enables the touchscreen, and emits the same
physical fingers as a new gesture.
This behavior appears to have been introduced by the 2023 daemon
refactoring:
cff1189
Before that change, the heatmap handler checked whether the stylus was
actually active before disabling touch.
Minimal fix tested
I tested limiting disable() to the current proximity state or the
transition out of the previous proximity state:
if (m_config.touchscreen_disable_on_stylus && m_touch.has_value()) {
if ((stylus.proximity || m_stylus->active()) &&
m_touch->enabled())
m_touch->disable();
}
At this point m_stylus->active() still contains the previous sample
because m_stylus->update(stylus) occurs afterward.
This preserves the intended behavior:
- current sample is in proximity: disable touch immediately;
- previous sample was in proximity: remain disabled through the
proximity-out transition;
- current and previous samples are both non-proximity: do not repeatedly
disable and clear an active touch gesture.
After applying this condition, the same reproduction consistently
produced one Undo for one two-finger tap. Stylus-based touch suppression
and the post-stylus holdoff continued to work.
Related reports
Summary
With
Touchscreen.DisableOnStylus = true, one physical multi-touchgesture can be split into multiple independent touch sequences shortly
after using the stylus.
This was most visible in Krita: one intentional two-finger Undo gesture
occasionally triggered Undo three times. Raw evdev and internal iptsd
instrumentation showed that Krita correctly received three separate
two-contact sequences. The gesture was already fragmented before it
reached Krita.
Environment
Relevant configuration:
My build also contains a local 300 ms post-stylus touch holdoff. That
change only delays touch re-enabling; it does not alter the unconditional
m_touch->disable()call described below.Steps to reproduce
Touchscreen.DisableOnStylus.in Krita.
The problem is timing-dependent, so it may take several attempts.
Expected behavior
One physical two-finger tap should be emitted as one continuous touch
sequence and trigger one Undo.
Actual behavior
One physical two-finger tap can be emitted as several independent
two-contact sequences, triggering Undo multiple times.
In one raw evdev capture, a single physical tap was emitted as:
Krita received three corresponding
TouchBegin/TouchEndpairs.I also added temporary instrumentation after
search_lifted(). Duringanother reproduction it reported:
Both contacts were
valid=1andstable=1. There were no zero-contactframes between these entries, and palm blocking was never active.
Nevertheless, the previous-contact state was cleared between frames.
Cause
Daemon::on_stylus()disables the touch device for every stylus sample,without checking
stylus.proximity:https://github.com/linux-surface/iptsd/blob/v3.1.0/src/apps/daemon/daemon.hpp#L79-L90
TouchDevice::disable()lifts all active contacts and clears all contacttracking state:
https://github.com/linux-surface/iptsd/blob/v3.1.0/src/apps/daemon/touch.hpp#L166-L179
Therefore, a non-proximity stylus sample arriving during a touch gesture
produces an artificial touch release. The following touch frame sees the
stylus as inactive, re-enables the touchscreen, and emits the same
physical fingers as a new gesture.
This behavior appears to have been introduced by the 2023 daemon
refactoring:
cff1189
Before that change, the heatmap handler checked whether the stylus was
actually active before disabling touch.
Minimal fix tested
I tested limiting
disable()to the current proximity state or thetransition out of the previous proximity state:
At this point
m_stylus->active()still contains the previous samplebecause
m_stylus->update(stylus)occurs afterward.This preserves the intended behavior:
proximity-out transition;
disable and clear an active touch gesture.
After applying this condition, the same reproduction consistently
produced one Undo for one two-finger tap. Stylus-based touch suppression
and the post-stylus holdoff continued to work.
Related reports
DisableOnStylus.but this report specifically covers gesture fragmentation caused by
repeated non-proximity stylus samples.