Finished the driver and installed it. Saving for archive.

This commit is contained in:
2026-07-15 11:15:33 +02:00
parent b56dd2a34e
commit e6e1e9a6d6
5 changed files with 215 additions and 32 deletions
+6
View File
@@ -24,6 +24,12 @@ Regular wheel mode:
cargo run --manifest-path renewed_driver/Cargo.toml -- --mode wheel cargo run --manifest-path renewed_driver/Cargo.toml -- --mode wheel
``` ```
Multitouch timing can be tuned without recompiling:
```bash
cargo run --manifest-path renewed_driver/Cargo.toml -- --mode multitouch --mt-idle-ms 100 --mt-idle-max-ms 1500 --mt-settle-ms 80
```
The process still needs access to the Surface Dial event device and The process still needs access to the Surface Dial event device and
`/dev/uinput`, just like the original daemon. `/dev/uinput`, just like the original daemon.
+28 -7
View File
@@ -19,14 +19,35 @@ surface-dial-renewed --mode wheel
Normal Dial rotation emits a synthetic two-finger touchpad scroll through Normal Dial rotation emits a synthetic two-finger touchpad scroll through
`Surface Dial Renewed Touchpad`. `Surface Dial Renewed Touchpad`.
The driver starts a virtual two-finger contact on the first rotation event, The driver starts a virtual two-finger contact on the first rotation event and
moves both virtual fingers vertically, and ends the touch gesture after no moves both virtual fingers vertically. Gesture release is dynamic:
rotation has arrived for 2 seconds.
The 2 second hold is intentional. Very slow physical Dial movement can emit - normal and fast rotation releases after about `100ms`
sparse `REL_DIAL` events. If the virtual fingers are lifted too quickly, libinput - sparse precision movement can stretch release up to `1500ms`
can treat those sparse movements as many tiny gestures and ignore them. Holding
the contact makes slow and precise movement accumulate into one gesture. The `100ms` minimum matches the old driver's multitouch control. The timeout is
based on the time gap between rotation events, not the raw delta size. That is
important because the Dial can emit `delta=1` even during normal fast scrolling.
If the virtual fingers are lifted too quickly during sparse movement, libinput
can treat those sparse events as many tiny gestures and ignore them. If the hold
is too long, page-edge overscroll animations can remain stretched for too long
before bouncing back.
The driver keeps the last rotation timestamp even after a virtual touch gesture
ends. This matters because sparse precision events can arrive after the 100ms
gesture has already been released. The next event still sees the previous event
gap and can stretch the new gesture timeout. A gap of `2500ms` or more is
treated as a real pause and resets back to the `100ms` minimum.
Timing is configurable:
```bash
surface-dial-renewed --mt-idle-ms 100 --mt-idle-max-ms 1500 --mt-settle-ms 80
```
`--mt-idle-ms` controls the fast-scroll minimum release timeout.
`--mt-idle-max-ms` controls the slow/precision maximum release timeout.
`--mt-settle-ms` controls the short stationary frame before the fingers lift.
## Sensitivity Selector ## Sensitivity Selector
+19
View File
@@ -52,6 +52,25 @@ The service runs:
It restarts on failure with a 2 second delay. It restarts on failure with a 2 second delay.
To tune multitouch timing in the service, edit:
```text
~/.config/systemd/user/surface-dial-renewed.service
```
Example:
```text
ExecStart=%h/.cargo/bin/surface-dial-renewed --mode multitouch --mt-idle-ms 100 --mt-idle-max-ms 1500 --mt-settle-ms 80
```
Then reload and restart:
```bash
systemctl --user daemon-reload
systemctl --user restart surface-dial-renewed.service
```
Useful commands: Useful commands:
```bash ```bash
+21
View File
@@ -56,6 +56,27 @@ systemctl --user status surface-dial-renewed.service
journalctl --user -u surface-dial-renewed.service -f journalctl --user -u surface-dial-renewed.service -f
``` ```
## Tune Multitouch Timing
Edit:
```bash
~/.config/systemd/user/surface-dial-renewed.service
```
Example:
```text
ExecStart=%h/.cargo/bin/surface-dial-renewed --mode multitouch --mt-idle-ms 100 --mt-idle-max-ms 1500 --mt-settle-ms 80
```
Apply changes:
```bash
systemctl --user daemon-reload
systemctl --user restart surface-dial-renewed.service
```
## Uninstall ## Uninstall
Automatic uninstall from the repository root: Automatic uninstall from the repository root:
+141 -25
View File
@@ -5,7 +5,7 @@ mod notify;
mod sensitivity; mod sensitivity;
mod virtual_input; mod virtual_input;
use std::time::Duration; use std::time::{Duration, Instant};
use dial::{DialEvent, DialEventKind, DialReader}; use dial::{DialEvent, DialEventKind, DialReader};
use error::Result; use error::Result;
@@ -14,8 +14,12 @@ use notify::SensitivityNotifier;
use sensitivity::Sensitivity; use sensitivity::Sensitivity;
use virtual_input::{VirtualInput, WheelDirection}; use virtual_input::{VirtualInput, WheelDirection};
const MULTITOUCH_IDLE_TIMEOUT: Duration = Duration::from_millis(2_000); const DEFAULT_MULTITOUCH_IDLE_TIMEOUT: Duration = Duration::from_millis(100);
const MULTITOUCH_SETTLE_TIMEOUT: Duration = Duration::from_millis(80); const DEFAULT_MULTITOUCH_MAX_IDLE_TIMEOUT: Duration = Duration::from_millis(1_500);
const DEFAULT_MULTITOUCH_SETTLE_TIMEOUT: Duration = Duration::from_millis(80);
const DYNAMIC_IDLE_FAST_EVENT_MS: u128 = 80;
const DYNAMIC_IDLE_SLOW_EVENT_MS: u128 = 450;
const DYNAMIC_IDLE_RESET_MS: u128 = 2_500;
const SENSITIVITY_STEP_UNITS: i32 = 48; const SENSITIVITY_STEP_UNITS: i32 = 48;
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
@@ -24,6 +28,25 @@ enum OutputMode {
Wheel, Wheel,
} }
#[derive(Debug, Clone, Copy)]
struct DriverConfig {
mode: OutputMode,
multitouch_idle_timeout: Duration,
multitouch_max_idle_timeout: Duration,
multitouch_settle_timeout: Duration,
}
impl Default for DriverConfig {
fn default() -> Self {
Self {
mode: OutputMode::Multitouch,
multitouch_idle_timeout: DEFAULT_MULTITOUCH_IDLE_TIMEOUT,
multitouch_max_idle_timeout: DEFAULT_MULTITOUCH_MAX_IDLE_TIMEOUT,
multitouch_settle_timeout: DEFAULT_MULTITOUCH_SETTLE_TIMEOUT,
}
}
}
#[derive(Debug)] #[derive(Debug)]
struct DriverState { struct DriverState {
mode: OutputMode, mode: OutputMode,
@@ -36,6 +59,8 @@ struct DriverState {
mt_active: bool, mt_active: bool,
mt_position: i32, mt_position: i32,
mt_remainder_milli: i32, mt_remainder_milli: i32,
mt_current_idle_timeout: Duration,
mt_last_rotation_at: Option<Instant>,
} }
impl DriverState { impl DriverState {
@@ -49,6 +74,8 @@ impl DriverState {
mt_active: false, mt_active: false,
mt_position: 0, mt_position: 0,
mt_remainder_milli: 0, mt_remainder_milli: 0,
mt_current_idle_timeout: DEFAULT_MULTITOUCH_IDLE_TIMEOUT,
mt_last_rotation_at: None,
} }
} }
} }
@@ -61,8 +88,8 @@ fn main() {
} }
fn run() -> Result<()> { fn run() -> Result<()> {
let mode = parse_mode(std::env::args().skip(1)); let config = parse_config(std::env::args().skip(1));
let mut state = DriverState::new(mode); let mut state = DriverState::new(config.mode);
// Input reading runs on a worker thread; output devices stay owned by the // Input reading runs on a worker thread; output devices stay owned by the
// main thread to avoid sharing uinput handles across threads. // main thread to avoid sharing uinput handles across threads.
let mut dial = DialReader::spawn(Duration::from_millis(750))?; let mut dial = DialReader::spawn(Duration::from_millis(750))?;
@@ -70,25 +97,38 @@ fn run() -> Result<()> {
let mut notifier = SensitivityNotifier::new(); let mut notifier = SensitivityNotifier::new();
let mut haptics = Haptics::new(); let mut haptics = Haptics::new();
eprintln!("surface-dial-renewed: running in {mode:?} mode"); eprintln!(
"surface-dial-renewed: running in {:?} mode, mt_idle={}ms, mt_idle_max={}ms, mt_settle={}ms",
config.mode,
config.multitouch_idle_timeout.as_millis(),
config.multitouch_max_idle_timeout.as_millis(),
config.multitouch_settle_timeout.as_millis()
);
loop { loop {
// While a synthetic two-finger gesture is active, recv with a timeout. // While a synthetic two-finger gesture is active, recv with a timeout.
// Timeout means "the user stopped rotating", so the virtual fingers can // Timeout means "the user stopped rotating", so the virtual fingers can
// be lifted. The 2s delay is deliberate: very slow Dial movement emits // be lifted. The timeout is dynamic: normal/fast scrolling uses the old
// sparse REL_DIAL events, and libinput needs one continuous gesture to // driver's 100ms release behavior, while sparse precision movement can
// recognize those as precise scroll movement. // stretch toward the configured maximum. The last rotation timestamp is
// kept across gesture endings so slow events are not all treated as
// unrelated first events.
let timeout = if state.mt_active { let timeout = if state.mt_active {
Some(MULTITOUCH_IDLE_TIMEOUT) Some(state.mt_current_idle_timeout)
} else { } else {
None None
}; };
match dial.next_event(timeout)? { match dial.next_event(timeout)? {
Some(event) => { Some(event) => handle_event(
handle_event(event, &mut state, &mut output, &mut notifier, &mut haptics)? event,
} &mut state,
None => end_multitouch_if_active(&mut state, &mut output)?, &mut output,
&mut notifier,
&mut haptics,
&config,
)?,
None => end_multitouch_if_active(&mut state, &mut output, &config)?,
} }
} }
} }
@@ -99,6 +139,7 @@ fn handle_event(
output: &mut VirtualInput, output: &mut VirtualInput,
notifier: &mut SensitivityNotifier, notifier: &mut SensitivityNotifier,
haptics: &mut Haptics, haptics: &mut Haptics,
config: &DriverConfig,
) -> Result<()> { ) -> Result<()> {
match event.kind { match event.kind {
DialEventKind::Connected => { DialEventKind::Connected => {
@@ -108,7 +149,7 @@ fn handle_event(
haptics.reconnect(); haptics.reconnect();
} }
DialEventKind::Disconnected => { DialEventKind::Disconnected => {
end_multitouch_if_active(state, output)?; end_multitouch_if_active(state, output, config)?;
haptics.disconnect(); haptics.disconnect();
eprintln!("surface-dial-renewed: dial disconnected"); eprintln!("surface-dial-renewed: dial disconnected");
} }
@@ -133,13 +174,13 @@ fn handle_event(
// Never scroll while the sensitivity selector is active. End a // Never scroll while the sensitivity selector is active. End a
// pending touchpad gesture before showing the selector so the // pending touchpad gesture before showing the selector so the
// desktop does not see overlapping scroll and adjustment input. // desktop does not see overlapping scroll and adjustment input.
end_multitouch_if_active(state, output)?; end_multitouch_if_active(state, output, config)?;
adjust_sensitivity(delta, state, notifier, haptics)?; adjust_sensitivity(delta, state, notifier, haptics)?;
return Ok(()); return Ok(());
} }
match state.mode { match state.mode {
OutputMode::Multitouch => multitouch_scroll(delta, state, output)?, OutputMode::Multitouch => multitouch_scroll(delta, state, output, config)?,
OutputMode::Wheel => wheel_scroll(delta, state, output)?, OutputMode::Wheel => wheel_scroll(delta, state, output)?,
} }
} }
@@ -178,7 +219,14 @@ fn adjust_sensitivity(
Ok(()) Ok(())
} }
fn multitouch_scroll(delta: i32, state: &mut DriverState, output: &mut VirtualInput) -> Result<()> { fn multitouch_scroll(
delta: i32,
state: &mut DriverState,
output: &mut VirtualInput,
config: &DriverConfig,
) -> Result<()> {
update_multitouch_idle_timeout(delta, state, config);
if !state.mt_active { if !state.mt_active {
state.mt_position = 0; state.mt_position = 0;
output.multitouch_start()?; output.multitouch_start()?;
@@ -216,6 +264,38 @@ fn multitouch_scroll(delta: i32, state: &mut DriverState, output: &mut VirtualIn
Ok(()) Ok(())
} }
fn update_multitouch_idle_timeout(_delta: i32, state: &mut DriverState, config: &DriverConfig) {
let now = Instant::now();
let event_gap = state
.mt_last_rotation_at
.map(|last| now.saturating_duration_since(last));
state.mt_last_rotation_at = Some(now);
let Some(event_gap) = event_gap else {
state.mt_current_idle_timeout = config.multitouch_idle_timeout;
return;
};
let gap_ms = event_gap.as_millis();
state.mt_current_idle_timeout = if gap_ms >= DYNAMIC_IDLE_RESET_MS {
// A genuine pause should not make the first new scroll linger.
config.multitouch_idle_timeout
} else if gap_ms >= DYNAMIC_IDLE_SLOW_EVENT_MS {
config.multitouch_max_idle_timeout
} else if gap_ms <= DYNAMIC_IDLE_FAST_EVENT_MS {
config.multitouch_idle_timeout
} else {
let fast = DYNAMIC_IDLE_FAST_EVENT_MS;
let slow = DYNAMIC_IDLE_SLOW_EVENT_MS;
let span = slow.saturating_sub(fast).max(1);
let progress = gap_ms.saturating_sub(fast).min(span);
let min_ms = config.multitouch_idle_timeout.as_millis();
let max_ms = config.multitouch_max_idle_timeout.as_millis().max(min_ms);
let dynamic_ms = min_ms + ((max_ms - min_ms) * progress) / span;
Duration::from_millis(dynamic_ms as u64)
};
}
fn wheel_scroll(delta: i32, state: &DriverState, output: &mut VirtualInput) -> Result<()> { fn wheel_scroll(delta: i32, state: &DriverState, output: &mut VirtualInput) -> Result<()> {
let ticks = delta.abs().saturating_mul(state.sensitivity.wheel_ticks()); let ticks = delta.abs().saturating_mul(state.sensitivity.wheel_ticks());
let direction = if delta > 0 { let direction = if delta > 0 {
@@ -231,36 +311,72 @@ fn wheel_scroll(delta: i32, state: &DriverState, output: &mut VirtualInput) -> R
Ok(()) Ok(())
} }
fn end_multitouch_if_active(state: &mut DriverState, output: &mut VirtualInput) -> Result<()> { fn end_multitouch_if_active(
state: &mut DriverState,
output: &mut VirtualInput,
config: &DriverConfig,
) -> Result<()> {
if state.mt_active { if state.mt_active {
// Emit one stationary frame and wait briefly before lifting fingers. // Emit one stationary frame and wait briefly before lifting fingers.
// This made libinput less likely to interpret the final delta as a // This made libinput less likely to interpret the final delta as a
// touchpad flick/coast. // touchpad flick/coast.
output.multitouch_settle(state.mt_position)?; output.multitouch_settle(state.mt_position)?;
std::thread::sleep(MULTITOUCH_SETTLE_TIMEOUT); std::thread::sleep(config.multitouch_settle_timeout);
output.multitouch_end()?; output.multitouch_end()?;
state.mt_active = false; state.mt_active = false;
state.mt_position = 0; state.mt_position = 0;
state.mt_remainder_milli = 0; state.mt_remainder_milli = 0;
state.mt_current_idle_timeout = config.multitouch_idle_timeout;
} }
Ok(()) Ok(())
} }
fn parse_mode(args: impl Iterator<Item = String>) -> OutputMode { fn parse_config(args: impl Iterator<Item = String>) -> DriverConfig {
let mut mode = OutputMode::Multitouch; let mut config = DriverConfig::default();
let mut previous = String::new(); let mut previous = String::new();
for arg in args { for arg in args {
if previous == "--mode" { if previous == "--mode" {
mode = parse_mode_value(&arg); config.mode = parse_mode_value(&arg);
} else if previous == "--mt-idle-ms" {
config.multitouch_idle_timeout = Duration::from_millis(parse_ms(&arg, "mt-idle-ms"));
} else if previous == "--mt-idle-max-ms" {
config.multitouch_max_idle_timeout =
Duration::from_millis(parse_ms(&arg, "mt-idle-max-ms"));
} else if previous == "--mt-settle-ms" {
config.multitouch_settle_timeout =
Duration::from_millis(parse_ms(&arg, "mt-settle-ms"));
} else if let Some(value) = arg.strip_prefix("--mode=") { } else if let Some(value) = arg.strip_prefix("--mode=") {
mode = parse_mode_value(value); config.mode = parse_mode_value(value);
} else if let Some(value) = arg.strip_prefix("--mt-idle-ms=") {
config.multitouch_idle_timeout = Duration::from_millis(parse_ms(value, "mt-idle-ms"));
} else if let Some(value) = arg.strip_prefix("--mt-idle-max-ms=") {
config.multitouch_max_idle_timeout =
Duration::from_millis(parse_ms(value, "mt-idle-max-ms"));
} else if let Some(value) = arg.strip_prefix("--mt-settle-ms=") {
config.multitouch_settle_timeout =
Duration::from_millis(parse_ms(value, "mt-settle-ms"));
} }
previous = arg; previous = arg;
} }
mode config
}
fn parse_ms(value: &str, name: &str) -> u64 {
match value.parse::<u64>() {
Ok(ms) => ms,
Err(_) => {
eprintln!("surface-dial-renewed: invalid --{name} '{value}', using default");
match name {
"mt-idle-ms" => DEFAULT_MULTITOUCH_IDLE_TIMEOUT.as_millis() as u64,
"mt-idle-max-ms" => DEFAULT_MULTITOUCH_MAX_IDLE_TIMEOUT.as_millis() as u64,
"mt-settle-ms" => DEFAULT_MULTITOUCH_SETTLE_TIMEOUT.as_millis() as u64,
_ => 0,
}
}
}
} }
fn parse_mode_value(value: &str) -> OutputMode { fn parse_mode_value(value: &str) -> OutputMode {