Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,8 @@ size = 3.0

[performance]
buffer_count = 3
enable_vsync = true
max_fps_no_vsync = 60
enable_vsync = false
max_fps_no_vsync = 120
ui_animation_fps = 30

[ui]
Expand Down Expand Up @@ -909,13 +909,20 @@ See `docs/CONFIG.md` for the full list.

### Performance tuning

Default behavior prioritizes lower drawing latency by disabling vsync and capping no-vsync rendering:

```toml
[performance]
buffer_count = 2
enable_vsync = true
buffer_count = 3
enable_vsync = false
max_fps_no_vsync = 120
ui_animation_fps = 30
```

Use `120` as a strong low-latency cap for common systems. Try `144`, `165`, `240`, or higher if it matches your display and the machine handles the extra rendering work. Use `max_fps_no_vsync = 0` only for profiling because uncapped rendering can spin CPU/GPU hard.

Set `enable_vsync = true` when tear-free presentation, lower power use, or quieter behavior matters more than input latency. Vsync usually adds a frame-cadence floor, especially on 60 Hz displays; disabling it improves input latency but may allow tearing and higher CPU/GPU usage.

---

## Contributing
Expand Down
8 changes: 4 additions & 4 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -964,12 +964,12 @@ head_at_end = false
buffer_count = 3

# Enable vsync frame synchronization
# Prevents tearing and limits rendering to display refresh rate
enable_vsync = true
# false lowers drawing latency; true prevents tearing and limits rendering to display refresh rate
enable_vsync = false

# Max FPS when VSync is disabled (0 = unlimited)
# Prevents CPU spinning at very high FPS; set to match your monitor (60/120/144/240)
max_fps_no_vsync = 60
# 120 keeps pen latency low without uncapped CPU usage; set to 0 only for profiling
max_fps_no_vsync = 120

# UI animation frame rate (0 = unlimited)
# Higher values smooth UI effects at the cost of more redraws
Expand Down
4 changes: 2 additions & 2 deletions configurator/src/app/view/performance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ impl ConfiguratorApp {
&self.draft.performance_max_fps_no_vsync,
&self.defaults.performance_max_fps_no_vsync,
TextField::PerformanceMaxFpsNoVsync,
Some("0 = unlimited, or match your monitor (60/120/144/240)"),
Some("Default 120; try 144/240 on high-refresh displays; 0 = unlimited"),
validate_u32_range(&self.draft.performance_max_fps_no_vsync, 0, 1000),
))
.push(text("Caps frame rate when VSync is disabled. Prevents CPU spinning at 500+ FPS. Set to your monitor's refresh rate for best results, or 0 for unlimited (requires strong CPU).").size(12));
.push(text("Caps frame rate when VSync is disabled. 120 FPS keeps drawing latency low without uncapped CPU/GPU usage; use 0 only for profiling.").size(12));
}

if show_animation {
Expand Down
43 changes: 30 additions & 13 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,12 @@ Controls rendering performance and smoothness.
buffer_count = 3

# Enable vsync frame synchronization
# Prevents tearing and limits rendering to display refresh rate
enable_vsync = true
# false lowers drawing latency; true prevents tearing and limits rendering to display refresh rate
enable_vsync = false

# Max FPS when VSync is disabled (0 = unlimited)
# Prevents CPU spinning at very high FPS; set to match your monitor
max_fps_no_vsync = 60
# 120 keeps pen latency low without uncapped CPU usage; set to 0 only for profiling
max_fps_no_vsync = 120

# UI animation frame rate (0 = unlimited)
# Higher values smooth UI effects at the cost of more redraws
Expand All @@ -262,13 +262,14 @@ ui_animation_fps = 30
- **4**: Quad buffering - for high-refresh displays (144Hz+), ultra-smooth

**VSync:**
- **true** (default): Synchronizes with display refresh rate, no tearing
- **false**: Capped by `max_fps_no_vsync` (set to 0 for uncapped); may cause tearing but lower latency
- **false** (default): Capped by `max_fps_no_vsync`; lower drawing latency, with possible tearing
- **true**: Synchronizes with display refresh rate, no tearing, but input-to-commit latency is bounded by refresh cadence

**Max FPS (VSync off):**
- **60** (default): Suitable for most displays
- **0**: Unlimited (uncapped; higher CPU usage)
- Set to your monitor refresh (60/120/144/240) for best balance
- **120** (default): Low-latency drawing without uncapped redraw loops
- **60**: Lower CPU/GPU use, but latency may feel closer to one 60 Hz frame interval
- **144/165/240+**: Use when it matches your display and the machine handles the extra rendering work
- **0**: Unlimited; mostly for profiling because it can spin CPU/GPU hard

**UI Animation FPS:**
- **30** (default): Smooth enough for most effects
Expand All @@ -277,10 +278,24 @@ ui_animation_fps = 30

**Defaults:**
- Buffer count: 3 (triple buffering)
- VSync: true
- Max FPS (VSync off): 60
- VSync: false
- Max FPS (VSync off): 120
- UI animation FPS: 30

**Tradeoff:**
Disabling vsync improves input latency but may allow tearing and higher CPU/GPU usage. On weaker
PCs, laptops, or battery-sensitive setups, restore `enable_vsync = true` or lower
`max_fps_no_vsync` if you notice heat, fan noise, battery drain, or compositor smoothness issues.

**Measurement note:**
With `WAYSCRIBER_PERF_LOG=1`, the `perf.input_to_paint_latency proxy=input_to_wayland_commit`
line reports an input-to-Wayland-commit proxy metric. It measures from input sample receipt inside
the app to Wayland surface commit. It is not photons-on-screen display latency; compositor
scheduling, display scanout, and hardware can add more latency outside Wayscriber.

In local continuous-drawing measurements, 120 FPS low-latency mode held p95 around 8-9 ms and
p99 around 8-9 ms for this proxy metric. Isolated max spikes existed, but p99 stayed under 16 ms.

### `[ui]` - User Interface

Controls visual indicators, overlays, and UI styling.
Expand Down Expand Up @@ -1392,7 +1407,8 @@ default_font_size = 24.0

[performance]
buffer_count = 4
enable_vsync = true
enable_vsync = false
max_fps_no_vsync = 120
ui_animation_fps = 30

[ui]
Expand All @@ -1416,7 +1432,8 @@ status_bar_position = "top-right"
```toml
[performance]
buffer_count = 4
enable_vsync = true
enable_vsync = false
max_fps_no_vsync = 144
ui_animation_fps = 120
```

Expand Down
1 change: 1 addition & 0 deletions src/backend/wayland/backend/event_loop/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub(super) fn maybe_render(
state.surface.frame_callback_pending()
);
let render_start = Instant::now();
state.begin_perf_render(render_start);
match state.render(qh) {
Ok(keep_rendering) => {
let render_duration = render_start.elapsed();
Expand Down
9 changes: 9 additions & 0 deletions src/backend/wayland/handlers/pointer/motion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use log::debug;
use smithay_client_toolkit::seat::pointer::PointerEvent;
use wayland_client::Connection;

use crate::backend::wayland::state::PerfInputSource;
use crate::backend::wayland::state::drag_log;
use crate::backend::wayland::toolbar_intent::intent_to_event;

Expand Down Expand Up @@ -167,5 +168,13 @@ impl WaylandState {
wx,
wy,
);
self.record_perf_input_sample(
PerfInputSource::Pointer,
event.position.0.round() as i32,
event.position.1.round() as i32,
wx,
wy,
false,
);
}
}
20 changes: 14 additions & 6 deletions src/backend/wayland/handlers/tablet/frame.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use log::{debug, info};

use crate::backend::wayland::state::WaylandState;
use crate::backend::wayland::state::{PerfInputSource, WaylandState};
use crate::input::MouseButton;

/// Linux input event code for the primary stylus barrel button.
Expand Down Expand Up @@ -49,7 +49,7 @@ impl WaylandState {
}

if let Some((x, y)) = pending.motion {
self.commit_stylus_motion_sample(x, y);
self.commit_stylus_motion_sample(x, y, pending.pressure.is_some());
}

if pending.down {
Expand All @@ -61,7 +61,7 @@ impl WaylandState {
&& !pending.down
&& self.stylus_tip_down
{
self.commit_stylus_motion_sample_at_current_position();
self.commit_stylus_motion_sample_at_current_position(true);
}

if pending.up {
Expand Down Expand Up @@ -107,23 +107,31 @@ impl WaylandState {
self.record_stylus_peak(self.input_state.current_thickness);
}

fn commit_stylus_motion_sample(&mut self, x: f64, y: f64) {
fn commit_stylus_motion_sample(&mut self, x: f64, y: f64, pressure_sample: bool) {
let previous_hover_cursor_pos = self.stylus_hover_cursor_position();
self.set_current_mouse(x as i32, y as i32);
self.stylus_last_pos = Some((x, y));
let (wx, wy) = self.zoomed_world_coords(x, y);
self.input_state
.on_mouse_motion_with_canvas(x.round() as i32, y.round() as i32, wx, wy);
self.record_perf_input_sample(
PerfInputSource::Stylus,
x.round() as i32,
y.round() as i32,
wx,
wy,
pressure_sample,
);
let next_hover_cursor_pos = self.stylus_hover_cursor_position();
self.mark_stylus_hover_cursor_dirty(previous_hover_cursor_pos, next_hover_cursor_pos);
if self.stylus_tip_down {
self.record_stylus_motion_thickness();
}
}

fn commit_stylus_motion_sample_at_current_position(&mut self) {
fn commit_stylus_motion_sample_at_current_position(&mut self, pressure_sample: bool) {
let (x, y) = self.current_stylus_position();
self.commit_stylus_motion_sample(x, y);
self.commit_stylus_motion_sample(x, y, pressure_sample);
}

fn commit_stylus_down(&mut self) {
Expand Down
3 changes: 2 additions & 1 deletion src/backend/wayland/handlers/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use wayland_client::{
};

use crate::backend::wayland::state::{
TouchTarget, WaylandState, debug_toolbar_drag_logging_enabled,
PerfInputSource, TouchTarget, WaylandState, debug_toolbar_drag_logging_enabled,
};
use crate::backend::wayland::toolbar_intent::intent_to_event;
use crate::input::MouseButton;
Expand Down Expand Up @@ -325,6 +325,7 @@ impl WaylandState {
.update_pointer_positions(screen_x, screen_y, wx, wy);
self.input_state
.on_mouse_motion_with_canvas(screen_x, screen_y, wx, wy);
self.record_perf_input_sample(PerfInputSource::Touch, screen_x, screen_y, wx, wy, false);
}

fn handle_touch_up(
Expand Down
3 changes: 3 additions & 0 deletions src/backend/wayland/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ mod helpers;
mod input_actions;
mod onboarding;
mod pdf_export;
mod perf;
mod render;
mod toolbar;
mod zoom;
Expand All @@ -108,6 +109,7 @@ pub(super) use helpers::{
scale_damage_regions, surface_id, toolbar_drag_preview_enabled, toolbar_drag_throttle_interval,
toolbar_pointer_lock_enabled,
};
pub(in crate::backend::wayland) use perf::PerfInputSource;

pub(in crate::backend::wayland) struct WaylandGlobals {
pub registry_state: RegistryState,
Expand Down Expand Up @@ -181,6 +183,7 @@ pub(super) struct WaylandState {
pub(super) capture: CaptureState,
pub(super) frozen: FrozenState,
pub(super) zoom: ZoomState,
perf: perf::PerfMetrics,

// Overlay behavior
pub(super) exit_after_capture_mode: ExitAfterCaptureMode,
Expand Down
1 change: 1 addition & 0 deletions src/backend/wayland/state/core/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ impl WaylandState {
capture: CaptureState::new(capture_manager),
frozen: FrozenState::new(screencopy_manager),
zoom: ZoomState::new(zoom_manager),
perf: perf::PerfMetrics::from_env(),
exit_after_capture_mode,
themed_pointer: None,
touch: None,
Expand Down
Loading
Loading