diff --git a/desktop/src/app.rs b/desktop/src/app.rs index a7a5e0bc7d..a0714c28c7 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -43,7 +43,7 @@ pub(crate) struct App { web_communication_startup_buffer: Vec>, persistent_data: PersistentData, cli: Cli, - startup_time: Option, + compatibility_mode: bool, exit_reason: ExitReason, } @@ -59,6 +59,7 @@ impl App { app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, cli: Cli, + compatibility_mode: bool, ) -> Self { let ctrlc_app_event_scheduler = app_event_scheduler.clone(); ctrlc::set_handler(move || { @@ -81,7 +82,7 @@ impl App { let mut persistent_data = PersistentData::default(); persistent_data.load_from_disk(); - let desktop_wrapper = DesktopWrapper::new(rand::rng().random()); + let desktop_wrapper = DesktopWrapper::new(rand::rng().random(), compatibility_mode); Self { render_state: None, @@ -106,8 +107,8 @@ impl App { web_communication_startup_buffer: Vec::new(), persistent_data, cli, + compatibility_mode, exit_reason: ExitReason::Shutdown, - startup_time: None, } } @@ -398,6 +399,9 @@ impl App { window.show_all(); } } + DesktopFrontendMessage::RelaunchWithUiAcceleration => { + self.exit(Some(ExitReason::RelaunchWithUiAcceleration)); + } } } @@ -457,6 +461,12 @@ impl App { self.cef_init_successful = true; } } + AppEvent::StartupTextureWatchdogTimeout => { + if !self.cef_init_successful && !self.compatibility_mode { + tracing::error!("No CEF texture received within the first second, restarting in compatibility mode"); + self.exit(Some(ExitReason::UiAccelerationFailure)); + } + } AppEvent::ScheduleBrowserWork(instant) => { if instant <= Instant::now() { self.cef_context.work(); @@ -496,7 +506,13 @@ impl ApplicationHandler for App { self.desktop_wrapper.init(self.wgpu_context.clone()); - self.startup_time = Some(Instant::now()); + if !self.compatibility_mode { + let app_event_scheduler = self.app_event_scheduler.clone(); + let _ = thread::spawn(move || { + thread::sleep(Duration::from_secs(1)); + app_event_scheduler.schedule(AppEvent::StartupTextureWatchdogTimeout); + }); + } } fn proxy_wake_up(&mut self, event_loop: &dyn ActiveEventLoop) { @@ -561,16 +577,6 @@ impl ApplicationHandler for App { } let _ = self.start_render_sender.try_send(()); } - - if !self.cef_init_successful - && !self.cli.disable_ui_acceleration - && self.web_communication_initialized - && let Some(startup_time) = self.startup_time - && startup_time.elapsed() > Duration::from_secs(3) - { - tracing::error!("UI acceleration not working, exiting."); - self.exit(Some(ExitReason::UiAccelerationFailure)); - } } WindowEvent::DragDropped { paths, .. } => { for path in paths { @@ -664,4 +670,5 @@ impl ApplicationHandler for App { pub(crate) enum ExitReason { Shutdown, UiAccelerationFailure, + RelaunchWithUiAcceleration, } diff --git a/desktop/src/consts.rs b/desktop/src/consts.rs index d51594ea16..4577e5a41f 100644 --- a/desktop/src/consts.rs +++ b/desktop/src/consts.rs @@ -9,6 +9,7 @@ pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite"; pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock"; pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron"; pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron"; +pub(crate) const APP_STARTUP_SETTINGS_FILE_NAME: &str = "startup-settings.ron"; pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents"; // CEF configuration constants diff --git a/desktop/src/event.rs b/desktop/src/event.rs index 9e8bb2bf4e..3b3731852f 100644 --- a/desktop/src/event.rs +++ b/desktop/src/event.rs @@ -3,6 +3,7 @@ use crate::wrapper::messages::DesktopWrapperMessage; pub(crate) enum AppEvent { UiUpdate(wgpu::Texture), + StartupTextureWatchdogTimeout, CursorChange(crate::window::Cursor), ScheduleBrowserWork(std::time::Instant), WebCommunicationInitialized, diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index f113376323..f28fbb1250 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -1,4 +1,5 @@ use clap::Parser; +use std::ffi::OsStr; use std::io::Write; use std::process::exit; use tracing_subscriber::EnvFilter; @@ -40,6 +41,8 @@ pub fn start() { } let cli = Cli::parse(); + let mut startup_settings = persist::StartupSettings::load_from_disk(); + let compatibility_mode = cli.disable_ui_acceleration || startup_settings.ui_compatibility_mode; let Ok(lock_file) = std::fs::OpenOptions::new() .read(true) @@ -77,12 +80,12 @@ pub fn start() { let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel(); - if cli.disable_ui_acceleration { + if compatibility_mode { println!("UI acceleration is disabled"); } let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver); - let cef_context = match cef_context_builder.initialize(cef_handler, cli.disable_ui_acceleration) { + let cef_context = match cef_context_builder.initialize(cef_handler, compatibility_mode) { Ok(context) => { tracing::info!("CEF initialized successfully"); context @@ -105,7 +108,15 @@ 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, + cli, + compatibility_mode, + ); let exit_reason = app.run(event_loop); @@ -113,15 +124,23 @@ pub fn start() { drop(lock); match exit_reason { - #[cfg(target_os = "linux")] app::ExitReason::UiAccelerationFailure => { - use std::os::unix::process::CommandExt; - tracing::error!("Restarting application without UI acceleration"); - let _ = std::process::Command::new(std::env::current_exe().unwrap()).arg("--disable-ui-acceleration").exec(); - tracing::error!("Failed to restart application"); + startup_settings.ui_compatibility_mode = true; + startup_settings.save_to_disk(); + if let Err(error) = restart_application(true) { + tracing::error!("Failed to restart application: {error}"); + } + } + app::ExitReason::RelaunchWithUiAcceleration => { + tracing::info!("Restarting application with UI acceleration"); + startup_settings.ui_compatibility_mode = false; + startup_settings.save_to_disk(); + if let Err(error) = restart_application(false) { + tracing::error!("Failed to restart application: {error}"); + } } - _ => {} + app::ExitReason::Shutdown => {} } // Workaround for a Windows-specific exception that occurs when `app` is dropped. @@ -133,6 +152,21 @@ pub fn start() { exit(0); } +fn restart_application(disable_ui_acceleration: bool) -> std::io::Result<()> { + let current_exe = std::env::current_exe()?; + let args = restart_arguments(disable_ui_acceleration); + std::process::Command::new(current_exe).args(args).spawn().map(|_| ()) +} + +fn restart_arguments(disable_ui_acceleration: bool) -> Vec { + let disable_flag = OsStr::new("--disable-ui-acceleration"); + let mut args = std::env::args_os().skip(1).filter(|arg| arg != disable_flag).collect::>(); + if disable_ui_acceleration { + args.push(disable_flag.into()); + } + args +} + pub fn start_helper() { let cef_context_builder = cef::CefContextBuilder::::new_helper(); assert!(cef_context_builder.is_sub_process()); diff --git a/desktop/src/persist.rs b/desktop/src/persist.rs index 97d329f66e..87bd64da93 100644 --- a/desktop/src/persist.rs +++ b/desktop/src/persist.rs @@ -1,5 +1,52 @@ use crate::wrapper::messages::{Document, DocumentId, Preferences}; +#[derive(Default, serde::Serialize, serde::Deserialize)] +pub(crate) struct StartupSettings { + #[serde(default)] + pub(crate) ui_compatibility_mode: bool, +} + +impl StartupSettings { + pub(crate) fn load_from_disk() -> Self { + let path = Self::file_path(); + let data = match std::fs::read_to_string(&path) { + Ok(d) => d, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Self::default(), + Err(e) => { + tracing::error!("Failed to read startup settings from disk: {e}"); + return Self::default(); + } + }; + + match ron::from_str(&data) { + Ok(settings) => settings, + Err(e) => { + tracing::error!("Failed to deserialize startup settings: {e}"); + Self::default() + } + } + } + + pub(crate) fn save_to_disk(&self) { + let data = match ron::ser::to_string_pretty(self, Default::default()) { + Ok(d) => d, + Err(e) => { + tracing::error!("Failed to serialize startup settings: {e}"); + return; + } + }; + if let Err(e) = std::fs::write(Self::file_path(), data) { + tracing::error!("Failed to write startup settings to disk: {e}"); + } + } + + fn file_path() -> std::path::PathBuf { + let mut path = crate::dirs::app_data_dir(); + path.push(crate::consts::APP_STARTUP_SETTINGS_FILE_NAME); + path + } +} + #[derive(Default, serde::Serialize, serde::Deserialize)] pub(crate) struct PersistentData { documents: DocumentStore, diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index e4ab2aeb2b..7f0a8b6f27 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -151,6 +151,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD FrontendMessage::WindowShowAll => { dispatcher.respond(DesktopFrontendMessage::WindowShowAll); } + FrontendMessage::WindowRelaunchWithUiAcceleration => { + dispatcher.respond(DesktopFrontendMessage::RelaunchWithUiAcceleration); + } m => return Some(m), } None diff --git a/desktop/wrapper/src/lib.rs b/desktop/wrapper/src/lib.rs index 2ef3bbb60c..7db96237df 100644 --- a/desktop/wrapper/src/lib.rs +++ b/desktop/wrapper/src/lib.rs @@ -27,14 +27,18 @@ pub struct DesktopWrapper { } impl DesktopWrapper { - pub fn new(uuid_random_seed: u64) -> Self { + pub fn new(uuid_random_seed: u64, compatibility_mode: bool) -> Self { #[cfg(target_os = "windows")] let host = Host::Windows; #[cfg(target_os = "macos")] let host = Host::Mac; #[cfg(target_os = "linux")] let host = Host::Linux; - let env = Environment { platform: Platform::Desktop, host }; + let env = Environment { + platform: Platform::Desktop, + host, + compatibility_mode, + }; Self { editor: Editor::new(env, uuid_random_seed), diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index 84aab2b2aa..82428f27b3 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -74,6 +74,7 @@ pub enum DesktopFrontendMessage { WindowHide, WindowHideOthers, WindowShowAll, + RelaunchWithUiAcceleration, } pub enum DesktopWrapperMessage { diff --git a/editor/src/application.rs b/editor/src/application.rs index cc54caa8e3..0fa5a7c1d0 100644 --- a/editor/src/application.rs +++ b/editor/src/application.rs @@ -51,6 +51,7 @@ impl Editor { &Environment { platform: Platform::Desktop, host: Host::Linux, + compatibility_mode: false, } } } @@ -59,6 +60,7 @@ impl Editor { pub struct Environment { pub platform: Platform, pub host: Host, + pub compatibility_mode: bool, } #[derive(Clone, Copy, Debug)] pub enum Platform { @@ -87,6 +89,9 @@ impl Environment { pub fn is_linux(&self) -> bool { matches!(self.host, Host::Linux) } + pub fn is_compatibility_mode(&self) -> bool { + self.compatibility_mode + } } pub const GRAPHITE_RELEASE_SERIES: &str = env!("GRAPHITE_RELEASE_SERIES"); diff --git a/editor/src/messages/app_window/app_window_message.rs b/editor/src/messages/app_window/app_window_message.rs index c02b99b5fd..800fdf71e6 100644 --- a/editor/src/messages/app_window/app_window_message.rs +++ b/editor/src/messages/app_window/app_window_message.rs @@ -13,4 +13,5 @@ pub enum AppWindowMessage { Hide, HideOthers, ShowAll, + RelaunchWithUiAcceleration, } diff --git a/editor/src/messages/app_window/app_window_message_handler.rs b/editor/src/messages/app_window/app_window_message_handler.rs index 21b6e9b5e9..19ebafbb53 100644 --- a/editor/src/messages/app_window/app_window_message_handler.rs +++ b/editor/src/messages/app_window/app_window_message_handler.rs @@ -40,6 +40,9 @@ impl MessageHandler for AppWindowMessageHandler { AppWindowMessage::ShowAll => { responses.add(FrontendMessage::WindowShowAll); } + AppWindowMessage::RelaunchWithUiAcceleration => { + responses.add(FrontendMessage::WindowRelaunchWithUiAcceleration); + } } } advertise_actions!(AppWindowMessageDiscriminant; diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 0a03f676c5..06c96ab423 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -376,4 +376,5 @@ pub enum FrontendMessage { WindowHide, WindowHideOthers, WindowShowAll, + WindowRelaunchWithUiAcceleration, } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 78cce0bfad..368081428a 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1050,7 +1050,20 @@ impl MessageHandler> for Portfolio } PortfolioMessage::RequestStatusBarInfoLayout => { #[cfg(not(target_family = "wasm"))] - let widgets = vec![TextLabel::new("Graphite 1.0.0-RC3").disabled(true).widget_instance()]; // TODO: After the RCs, call this "Graphite (beta) x.y.z" + let widgets = { + let mut widgets = Vec::new(); + if Editor::environment().is_compatibility_mode() { + widgets.push( + IconButton::new("Warning", 12) + .tooltip_label("Compatibility mode") + .tooltip_description("Running without hardware acceleration. Click to relaunch with acceleration enabled.") + .on_update(|_| AppWindowMessage::RelaunchWithUiAcceleration.into()) + .widget_instance(), + ); + } + widgets.push(TextLabel::new("Graphite 1.0.0-RC3").disabled(true).widget_instance()); // TODO: After the RCs, call this "Graphite (beta) x.y.z" + widgets + }; #[cfg(target_family = "wasm")] let widgets = vec![]; diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index aaa17a1d22..9da82682e4 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -93,6 +93,7 @@ impl EditorHandle { "Windows" => Host::Windows, _ => unreachable!(), }, + compatibility_mode: false, }, uuid_random_seed, );