Desktop: Add an 'Enable V-Sync' preference on Mac (#3887)

* add vsync pref

* account for physical scale in pixel preview passthru check

* change allow to expect attr

* Update user-facing v-sync text

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Timon
2026-03-11 21:32:37 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent 6d0357bbcf
commit a18b7ff79d
9 changed files with 111 additions and 31 deletions
+11 -2
View File
@@ -21,7 +21,7 @@ use crate::persist::PersistentData;
use crate::preferences; use crate::preferences;
use crate::render::{RenderError, RenderState}; use crate::render::{RenderError, RenderState};
use crate::window::Window; 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}; use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
pub(crate) struct App { pub(crate) struct App {
@@ -46,6 +46,8 @@ pub(crate) struct App {
web_communication_initialized: bool, web_communication_initialized: bool,
web_communication_startup_buffer: Vec<Vec<u8>>, web_communication_startup_buffer: Vec<Vec<u8>>,
persistent_data: PersistentData, persistent_data: PersistentData,
#[cfg_attr(not(target_os = "macos"), expect(unused))]
preferences: Preferences,
cli: Cli, cli: Cli,
startup_time: Option<Instant>, startup_time: Option<Instant>,
exiting: Arc<AtomicBool>, exiting: Arc<AtomicBool>,
@@ -63,6 +65,7 @@ impl App {
wgpu_context: WgpuContext, wgpu_context: WgpuContext,
app_event_receiver: Receiver<AppEvent>, app_event_receiver: Receiver<AppEvent>,
app_event_scheduler: AppEventScheduler, app_event_scheduler: AppEventScheduler,
preferences: Preferences,
cli: Cli, cli: Cli,
) -> Self { ) -> Self {
let ctrlc_app_event_scheduler = app_event_scheduler.clone(); let ctrlc_app_event_scheduler = app_event_scheduler.clone();
@@ -116,6 +119,7 @@ impl App {
web_communication_initialized: false, web_communication_initialized: false,
web_communication_startup_buffer: Vec::new(), web_communication_startup_buffer: Vec::new(),
persistent_data, persistent_data,
preferences,
cli, cli,
startup_time: None, startup_time: None,
exiting, exiting,
@@ -516,7 +520,12 @@ impl ApplicationHandler for App {
let window = Window::new(event_loop, self.app_event_scheduler.clone()); let window = Window::new(event_loop, self.app_event_scheduler.clone());
self.window = Some(window); 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); self.render_state = Some(render_state);
if let Some(window) = &self.window.as_ref() { if let Some(window) = &self.window.as_ref() {
+6 -4
View File
@@ -62,6 +62,8 @@ pub fn start() {
} }
}; };
let prefs = preferences::read();
// Must be called before event loop initialization or native window integrations will break // Must be called before event loop initialization or native window integrations will break
App::init(); App::init();
@@ -73,7 +75,7 @@ pub fn start() {
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel(); 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 { if disable_ui_acceleration {
println!("UI acceleration is disabled"); 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); let exit_reason = app.run(event_loop);
@@ -111,15 +113,15 @@ pub fn start() {
drop(lock); drop(lock);
match exit_reason { match exit_reason {
#[cfg(target_os = "linux")]
app::ExitReason::Restart | app::ExitReason::UiAccelerationFailure => { app::ExitReason::Restart | app::ExitReason::UiAccelerationFailure => {
tracing::error!("Restarting application"); tracing::error!("Restarting application");
let mut command = std::process::Command::new(std::env::current_exe().unwrap()); let mut command = std::process::Command::new(std::env::current_exe().unwrap());
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
let _ = std::os::unix::process::CommandExt::exec(&mut command); let _ = std::os::unix::process::CommandExt::exec(&mut command);
#[cfg(target_family = "unix")]
tracing::error!("Failed to restart application");
#[cfg(not(target_family = "unix"))] #[cfg(not(target_family = "unix"))]
let _ = command.spawn(); let _ = command.spawn();
tracing::error!("Failed to restart application");
} }
_ => {} _ => {}
} }
+3 -5
View File
@@ -1,4 +1,5 @@
use std::borrow::Cow; use std::borrow::Cow;
use wgpu::PresentMode;
use crate::window::Window; use crate::window::Window;
use crate::wrapper::{TargetTexture, WgpuContext, WgpuExecutor}; use crate::wrapper::{TargetTexture, WgpuContext, WgpuExecutor};
@@ -27,7 +28,7 @@ pub(crate) struct RenderState {
} }
impl RenderState { impl RenderState {
pub(crate) fn new(window: &Window, context: WgpuContext) -> Self { pub(crate) fn new(window: &Window, context: WgpuContext, present_mode: Option<PresentMode>) -> Self {
let size = window.surface_size(); let size = window.surface_size();
let surface = window.create_surface(context.instance.clone()); let surface = window.create_surface(context.instance.clone());
@@ -39,10 +40,7 @@ impl RenderState {
format: surface_format, format: surface_format,
width: size.width, width: size.width,
height: size.height, height: size.height,
#[cfg(not(target_os = "macos"))] present_mode: present_mode.unwrap_or(surface_caps.present_modes[0]),
present_mode: surface_caps.present_modes[0],
#[cfg(target_os = "macos")]
present_mode: wgpu::PresentMode::Immediate,
alpha_mode: surface_caps.alpha_modes[0], alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![], view_formats: vec![],
desired_maximum_frame_latency: 1, desired_maximum_frame_latency: 1,
+2 -2
View File
@@ -77,7 +77,7 @@ fn menu_items_from_wrapper(entries: Vec<WrapperMenuItem>) -> Vec<MenuItemKind> {
} }
WrapperMenuItem::SubMenu { text: name, items, .. } => { WrapperMenuItem::SubMenu { text: name, items, .. } => {
let items = menu_items_from_wrapper(items); let items = menu_items_from_wrapper(items);
let items = items.iter().map(|item| menu_item_kind_to_dyn(item)).collect::<Vec<&dyn IsMenuItem>>(); let items = items.iter().map(menu_item_kind_to_dyn).collect::<Vec<&dyn IsMenuItem>>();
let submenu = Submenu::with_items(name, true, &items).unwrap(); let submenu = Submenu::with_items(name, true, &items).unwrap();
menu_items.push(MenuItemKind::Submenu(submenu)); menu_items.push(MenuItemKind::Submenu(submenu));
} }
@@ -106,7 +106,7 @@ fn replace_children<'a, T: Into<MenuContainer<'a>>>(menu: T, new_items: Vec<Menu
for item in items.iter() { for item in items.iter() {
menu.remove(menu_item_kind_to_dyn(item)).unwrap(); menu.remove(menu_item_kind_to_dyn(item)).unwrap();
} }
let items = new_items.iter().map(|item| menu_item_kind_to_dyn(item)).collect::<Vec<&dyn IsMenuItem>>(); let items = new_items.iter().map(menu_item_kind_to_dyn).collect::<Vec<&dyn IsMenuItem>>();
menu.append_items(items.as_ref()).unwrap(); menu.append_items(items.as_ref()).unwrap();
} }
+2 -2
View File
@@ -19,7 +19,7 @@ pub(crate) mod menu {
panic!("Menu bar layout group is supposed to be a row"); panic!("Menu bar layout group is supposed to be a row");
}; };
widgets widgets
.into_iter() .iter()
.map(|widget| { .map(|widget| {
let text_button = match widget.widget.as_ref() { let text_button = match widget.widget.as_ref() {
Widget::TextButton(text_button) => text_button, Widget::TextButton(text_button) => text_button,
@@ -79,7 +79,7 @@ pub(crate) mod menu {
let enabled = !*disabled; let enabled = !*disabled;
if !children.is_empty() { 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 }; return MenuItem::SubMenu { id, text, enabled, items };
} }
@@ -387,9 +387,44 @@ impl PreferencesDialogMessageHandler {
rows.push(ui_acceleration); rows.push(ui_acceleration);
} }
#[cfg(target_os = "macos")]
{
let vsync_description = "
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.*
"
.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 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 V-Sync")
.tooltip_label("Enable V-Sync")
.tooltip_description(vsync_description)
.for_checkbox(checkbox_id)
.widget_instance(),
];
rows.push(vsync);
}
} }
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<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) { pub fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) {
@@ -6,16 +6,38 @@ use crate::messages::prelude::*;
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum PreferencesMessage { pub enum PreferencesMessage {
// Management messages // Management messages
Load { preferences: PreferencesMessageHandler }, Load {
preferences: PreferencesMessageHandler,
},
ResetToDefaults, ResetToDefaults,
// Per-preference messages // Per-preference messages
SelectionMode { selection_mode: SelectionMode }, SelectionMode {
BrushTool { enabled: bool }, selection_mode: SelectionMode,
ModifyLayout { zoom_with_scroll: bool }, },
GraphWireStyle { style: GraphWireStyle }, BrushTool {
ViewportZoomWheelRate { rate: f64 }, enabled: bool,
UIScale { scale: f64 }, },
DisableUIAcceleration { disable_ui_acceleration: bool }, ModifyLayout {
MaxRenderRegionSize { size: u32 }, 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,
},
} }
@@ -21,15 +21,23 @@ pub struct PreferencesMessageHandler {
pub graph_wire_style: GraphWireStyle, pub graph_wire_style: GraphWireStyle,
pub viewport_zoom_wheel_rate: f64, pub viewport_zoom_wheel_rate: f64,
pub ui_scale: f64, pub ui_scale: f64,
pub disable_ui_acceleration: bool,
pub max_render_region_size: u32, pub max_render_region_size: u32,
pub disable_ui_acceleration: bool,
#[cfg(target_os = "macos")]
pub vsync: bool,
} }
impl PreferencesMessageHandler { impl PreferencesMessageHandler {
#[cfg(not(target_os = "macos"))]
pub fn needs_restart(&self, other: &Self) -> bool { pub fn needs_restart(&self, other: &Self) -> bool {
self.disable_ui_acceleration != other.disable_ui_acceleration 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 { pub fn get_selection_mode(&self) -> SelectionMode {
self.selection_mode self.selection_mode
} }
@@ -54,8 +62,10 @@ impl Default for PreferencesMessageHandler {
graph_wire_style: GraphWireStyle::default(), graph_wire_style: GraphWireStyle::default(),
viewport_zoom_wheel_rate: VIEWPORT_ZOOM_WHEEL_RATE, viewport_zoom_wheel_rate: VIEWPORT_ZOOM_WHEEL_RATE,
ui_scale: UI_SCALE_DEFAULT, ui_scale: UI_SCALE_DEFAULT,
disable_ui_acceleration: false,
max_render_region_size: EditorPreferences::default().max_render_region_size, 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<PreferencesMessage, PreferencesMessageContext<'_>> for Prefe
self.ui_scale = scale; self.ui_scale = scale;
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale }); responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
} }
PreferencesMessage::DisableUIAcceleration { disable_ui_acceleration } => {
self.disable_ui_acceleration = disable_ui_acceleration;
}
PreferencesMessage::MaxRenderRegionSize { size } => { PreferencesMessage::MaxRenderRegionSize { size } => {
self.max_render_region_size = size; self.max_render_region_size = size;
responses.add(PortfolioMessage::EditorPreferences); responses.add(PortfolioMessage::EditorPreferences);
responses.add(NodeGraphMessage::RunDocumentGraph); 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() }); responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() });
+1 -1
View File
@@ -22,7 +22,7 @@ pub async fn pixel_preview<'a: 'n>(
let physical_scale = render_params.scale; let physical_scale = render_params.scale;
let footprint = *ctx.footprint(); 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. { 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(); let context = OwnedContextImpl::from(ctx).into_context();