From 470b50bf4f013bc04ff0642117437a1d7a29a3c9 Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 5 Mar 2026 22:33:52 +0000 Subject: [PATCH 1/4] add vsync pref --- desktop/src/app.rs | 13 +++++- desktop/src/lib.rs | 10 +++-- desktop/src/render/state.rs | 8 ++-- desktop/src/window/mac/menu.rs | 4 +- desktop/wrapper/src/utils.rs | 4 +- .../preferences_dialog_message_handler.rs | 33 +++++++++++++++ .../preferences/preferences_message.rs | 40 ++++++++++++++----- .../preferences_message_handler.rs | 24 ++++++++--- 8 files changed, 107 insertions(+), 29 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 414e0a40c5..d92b0dbf62 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -21,7 +21,7 @@ use crate::persist::PersistentData; use crate::preferences; use crate::render::{RenderError, RenderState}; use crate::window::Window; -use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState}; +use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences}; use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; pub(crate) struct App { @@ -46,6 +46,8 @@ pub(crate) struct App { web_communication_initialized: bool, web_communication_startup_buffer: Vec>, persistent_data: PersistentData, + #[allow(unused)] + preferences: Preferences, cli: Cli, startup_time: Option, exiting: Arc, @@ -63,6 +65,7 @@ impl App { wgpu_context: WgpuContext, app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, + preferences: Preferences, cli: Cli, ) -> Self { let ctrlc_app_event_scheduler = app_event_scheduler.clone(); @@ -116,6 +119,7 @@ impl App { web_communication_initialized: false, web_communication_startup_buffer: Vec::new(), persistent_data, + preferences, cli, startup_time: None, exiting, @@ -516,7 +520,12 @@ impl ApplicationHandler for App { let window = Window::new(event_loop, self.app_event_scheduler.clone()); self.window = Some(window); - let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone()); + #[cfg(not(target_os = "macos"))] + let present_mode = None; + #[cfg(target_os = "macos")] + let present_mode = if !self.preferences.vsync { Some(wgpu::PresentMode::Immediate) } else { None }; + + let render_state = RenderState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone(), present_mode); self.render_state = Some(render_state); if let Some(window) = &self.window.as_ref() { diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index bc29f729cb..e3b9f3274d 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -62,6 +62,8 @@ pub fn start() { } }; + let prefs = preferences::read(); + // Must be called before event loop initialization or native window integrations will break App::init(); @@ -73,7 +75,7 @@ pub fn start() { let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel(); - let disable_ui_acceleration = preferences::read().disable_ui_acceleration || cli.disable_ui_acceleration; + let disable_ui_acceleration = prefs.disable_ui_acceleration || cli.disable_ui_acceleration; if disable_ui_acceleration { println!("UI acceleration is disabled"); } @@ -95,7 +97,7 @@ pub fn start() { } }; - let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, cli); + let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli); let exit_reason = app.run(event_loop); @@ -111,15 +113,15 @@ pub fn start() { drop(lock); match exit_reason { - #[cfg(target_os = "linux")] app::ExitReason::Restart | app::ExitReason::UiAccelerationFailure => { tracing::error!("Restarting application"); let mut command = std::process::Command::new(std::env::current_exe().unwrap()); #[cfg(target_family = "unix")] let _ = std::os::unix::process::CommandExt::exec(&mut command); + #[cfg(target_family = "unix")] + tracing::error!("Failed to restart application"); #[cfg(not(target_family = "unix"))] let _ = command.spawn(); - tracing::error!("Failed to restart application"); } _ => {} } diff --git a/desktop/src/render/state.rs b/desktop/src/render/state.rs index 4db481d4bc..0c8c80c068 100644 --- a/desktop/src/render/state.rs +++ b/desktop/src/render/state.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use wgpu::PresentMode; use crate::window::Window; use crate::wrapper::{TargetTexture, WgpuContext, WgpuExecutor}; @@ -27,7 +28,7 @@ pub(crate) struct RenderState { } impl RenderState { - pub(crate) fn new(window: &Window, context: WgpuContext) -> Self { + pub(crate) fn new(window: &Window, context: WgpuContext, present_mode: Option) -> Self { let size = window.surface_size(); let surface = window.create_surface(context.instance.clone()); @@ -39,10 +40,7 @@ impl RenderState { format: surface_format, width: size.width, height: size.height, - #[cfg(not(target_os = "macos"))] - present_mode: surface_caps.present_modes[0], - #[cfg(target_os = "macos")] - present_mode: wgpu::PresentMode::Immediate, + present_mode: present_mode.unwrap_or(surface_caps.present_modes[0]), alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![], desired_maximum_frame_latency: 1, diff --git a/desktop/src/window/mac/menu.rs b/desktop/src/window/mac/menu.rs index 41358b46b1..cfdb13a74c 100644 --- a/desktop/src/window/mac/menu.rs +++ b/desktop/src/window/mac/menu.rs @@ -77,7 +77,7 @@ fn menu_items_from_wrapper(entries: Vec) -> Vec { } WrapperMenuItem::SubMenu { text: name, items, .. } => { let items = menu_items_from_wrapper(items); - let items = items.iter().map(|item| menu_item_kind_to_dyn(item)).collect::>(); + let items = items.iter().map(menu_item_kind_to_dyn).collect::>(); let submenu = Submenu::with_items(name, true, &items).unwrap(); menu_items.push(MenuItemKind::Submenu(submenu)); } @@ -106,7 +106,7 @@ fn replace_children<'a, T: Into>>(menu: T, new_items: Vec>(); + let items = new_items.iter().map(menu_item_kind_to_dyn).collect::>(); menu.append_items(items.as_ref()).unwrap(); } diff --git a/desktop/wrapper/src/utils.rs b/desktop/wrapper/src/utils.rs index 0d24ecb929..5b60cceeaf 100644 --- a/desktop/wrapper/src/utils.rs +++ b/desktop/wrapper/src/utils.rs @@ -19,7 +19,7 @@ pub(crate) mod menu { panic!("Menu bar layout group is supposed to be a row"); }; widgets - .into_iter() + .iter() .map(|widget| { let text_button = match widget.widget.as_ref() { Widget::TextButton(text_button) => text_button, @@ -79,7 +79,7 @@ pub(crate) mod menu { let enabled = !*disabled; if !children.is_empty() { - let items = convert_menu_bar_entry_children_to_menu_items(&children, root_widget_id, path.clone()); + let items = convert_menu_bar_entry_children_to_menu_items(children, root_widget_id, path.clone()); return MenuItem::SubMenu { id, text, enabled, items }; } diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index e4d50a86d3..7f2abbdea9 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -387,6 +387,39 @@ impl PreferencesDialogMessageHandler { rows.push(ui_acceleration); } + + #[cfg(target_os = "macos")] + { + let vsync_description = " + Enable vertical synchronization (VSync), which synchronizes the frame rate to the display's refresh rate. This can reduce screen tearing but may add input latency.\n\ + \n\ + *Default: Off.* + " + .trim(); + + let checkbox_id = CheckboxId::new(); + let vsync_checked = preferences.vsync; + + let vsync = vec![ + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + CheckboxInput::new(vsync_checked) + .tooltip_label("Enable VSync") + .tooltip_description(vsync_description) + .on_update(|checkbox_input: &CheckboxInput| Message::Batched { + messages: Box::new([PreferencesDialogMessage::MayRequireRestart.into(), PreferencesMessage::VSync { vsync: checkbox_input.checked }.into()]), + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Enable VSync") + .tooltip_label("Enable VSync") + .tooltip_description(vsync_description) + .for_checkbox(checkbox_id) + .widget_instance(), + ]; + + rows.push(vsync); + } } Layout(rows.into_iter().map(|r| LayoutGroup::row(r)).collect()) diff --git a/editor/src/messages/preferences/preferences_message.rs b/editor/src/messages/preferences/preferences_message.rs index ff9803c0c3..8b6b64caa3 100644 --- a/editor/src/messages/preferences/preferences_message.rs +++ b/editor/src/messages/preferences/preferences_message.rs @@ -6,16 +6,38 @@ use crate::messages::prelude::*; #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum PreferencesMessage { // Management messages - Load { preferences: PreferencesMessageHandler }, + Load { + preferences: PreferencesMessageHandler, + }, ResetToDefaults, // Per-preference messages - SelectionMode { selection_mode: SelectionMode }, - BrushTool { enabled: bool }, - ModifyLayout { zoom_with_scroll: bool }, - GraphWireStyle { style: GraphWireStyle }, - ViewportZoomWheelRate { rate: f64 }, - UIScale { scale: f64 }, - DisableUIAcceleration { disable_ui_acceleration: bool }, - MaxRenderRegionSize { size: u32 }, + SelectionMode { + selection_mode: SelectionMode, + }, + BrushTool { + enabled: bool, + }, + ModifyLayout { + zoom_with_scroll: bool, + }, + GraphWireStyle { + style: GraphWireStyle, + }, + ViewportZoomWheelRate { + rate: f64, + }, + UIScale { + scale: f64, + }, + MaxRenderRegionSize { + size: u32, + }, + DisableUIAcceleration { + disable_ui_acceleration: bool, + }, + #[cfg(target_os = "macos")] + VSync { + vsync: bool, + }, } diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 8431934b4e..523515debc 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -21,15 +21,23 @@ pub struct PreferencesMessageHandler { pub graph_wire_style: GraphWireStyle, pub viewport_zoom_wheel_rate: f64, pub ui_scale: f64, - pub disable_ui_acceleration: bool, pub max_render_region_size: u32, + pub disable_ui_acceleration: bool, + #[cfg(target_os = "macos")] + pub vsync: bool, } impl PreferencesMessageHandler { + #[cfg(not(target_os = "macos"))] pub fn needs_restart(&self, other: &Self) -> bool { self.disable_ui_acceleration != other.disable_ui_acceleration } + #[cfg(target_os = "macos")] + pub fn needs_restart(&self, other: &Self) -> bool { + self.disable_ui_acceleration != other.disable_ui_acceleration || self.vsync != other.vsync + } + pub fn get_selection_mode(&self) -> SelectionMode { self.selection_mode } @@ -54,8 +62,10 @@ impl Default for PreferencesMessageHandler { graph_wire_style: GraphWireStyle::default(), viewport_zoom_wheel_rate: VIEWPORT_ZOOM_WHEEL_RATE, ui_scale: UI_SCALE_DEFAULT, - disable_ui_acceleration: false, max_render_region_size: EditorPreferences::default().max_render_region_size, + disable_ui_acceleration: false, + #[cfg(target_os = "macos")] + vsync: false, } } } @@ -112,14 +122,18 @@ impl MessageHandler> for Prefe self.ui_scale = scale; responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale }); } - PreferencesMessage::DisableUIAcceleration { disable_ui_acceleration } => { - self.disable_ui_acceleration = disable_ui_acceleration; - } PreferencesMessage::MaxRenderRegionSize { size } => { self.max_render_region_size = size; responses.add(PortfolioMessage::EditorPreferences); responses.add(NodeGraphMessage::RunDocumentGraph); } + PreferencesMessage::DisableUIAcceleration { disable_ui_acceleration } => { + self.disable_ui_acceleration = disable_ui_acceleration; + } + #[cfg(target_os = "macos")] + PreferencesMessage::VSync { vsync } => { + self.vsync = vsync; + } } responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() }); From 4bda0d6988f595fb8f3e2d5f9a9f04522d2389bd Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 11 Mar 2026 19:03:56 +0000 Subject: [PATCH 2/4] account for physical scale in pixel preview passthru check --- node-graph/nodes/gstd/src/pixel_preview.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-graph/nodes/gstd/src/pixel_preview.rs b/node-graph/nodes/gstd/src/pixel_preview.rs index d5e15de2ac..5628477586 100644 --- a/node-graph/nodes/gstd/src/pixel_preview.rs +++ b/node-graph/nodes/gstd/src/pixel_preview.rs @@ -22,7 +22,7 @@ pub async fn pixel_preview<'a: 'n>( let physical_scale = render_params.scale; let footprint = *ctx.footprint(); - let viewport_zoom = footprint.decompose_scale().x; + let viewport_zoom = footprint.decompose_scale().x * physical_scale; if render_params.render_mode != RenderMode::PixelPreview || !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || viewport_zoom <= 1. { let context = OwnedContextImpl::from(ctx).into_context(); From 55e964e54a5ec369921e7eff15a2dd284e4c6282 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 11 Mar 2026 19:04:04 +0000 Subject: [PATCH 3/4] change allow to expect attr --- desktop/src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index d92b0dbf62..c413e6ad9a 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -46,7 +46,7 @@ pub(crate) struct App { web_communication_initialized: bool, web_communication_startup_buffer: Vec>, persistent_data: PersistentData, - #[allow(unused)] + #[cfg_attr(not(target_os = "macos"), expect(unused))] preferences: Preferences, cli: Cli, startup_time: Option, From 318fa0f8feb01da6e61f757ab5f26bdf15d79270 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 11 Mar 2026 14:26:20 -0700 Subject: [PATCH 4/4] Update user-facing v-sync text --- .../preferences_dialog_message_handler.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index 7f2abbdea9..d2850fbfd2 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -391,7 +391,9 @@ impl PreferencesDialogMessageHandler { #[cfg(target_os = "macos")] { let vsync_description = " - Enable vertical synchronization (VSync), which synchronizes the frame rate to the display's refresh rate. This can reduce screen tearing but may add input latency.\n\ + Render frames with vertical synchronization (v-sync) to prevent visual tearing within Graphite and the operating system compositor. This introduces increased input latency which is more noticeable on lower refresh rate displays. Future versions of Graphite will aim to reduce the macOS-specific latency without tearing artifacts.\n\ + \n\ + The application will restart for this change to take effect.\n\ \n\ *Default: Off.* " @@ -404,15 +406,15 @@ impl PreferencesDialogMessageHandler { Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), CheckboxInput::new(vsync_checked) - .tooltip_label("Enable VSync") + .tooltip_label("Enable V-Sync") .tooltip_description(vsync_description) .on_update(|checkbox_input: &CheckboxInput| Message::Batched { messages: Box::new([PreferencesDialogMessage::MayRequireRestart.into(), PreferencesMessage::VSync { vsync: checkbox_input.checked }.into()]), }) .for_label(checkbox_id) .widget_instance(), - TextLabel::new("Enable VSync") - .tooltip_label("Enable VSync") + TextLabel::new("Enable V-Sync") + .tooltip_label("Enable V-Sync") .tooltip_description(vsync_description) .for_checkbox(checkbox_id) .widget_instance(), @@ -422,7 +424,7 @@ impl PreferencesDialogMessageHandler { } } - Layout(rows.into_iter().map(|r| LayoutGroup::row(r)).collect()) + Layout(rows.into_iter().map(LayoutGroup::row).collect()) } pub fn send_layout(&self, responses: &mut VecDeque, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) {