diff --git a/Cargo.lock b/Cargo.lock index 75cda7d1..75d8421e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3457,8 +3457,10 @@ dependencies = [ "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", + "pollster 0.4.0", "quick-xml", "rfd", + "ron", "rustls", "serde_json", "solidcraft-engine", diff --git a/apps/solidcraft/Cargo.toml b/apps/solidcraft/Cargo.toml index ff332a5a..45c60cba 100644 --- a/apps/solidcraft/Cargo.toml +++ b/apps/solidcraft/Cargo.toml @@ -27,6 +27,8 @@ egui = { workspace = true } serde_json = { workspace = true } log = { workspace = true } rfd = { workspace = true } +# Checks eframe's settings file (app.ron) before eframe reads it (src/startup.rs). +ron = "0.12" getrandom = "0.3" # The update check (docs/updates.md): HTTPS with rustls, certificates from the operating system. ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "platform-verifier"] } @@ -48,5 +50,8 @@ block2 = "0.6" [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] winit = { version = "0.30", default-features = false, features = ["x11"] } +[dev-dependencies] +pollster = { workspace = true } + [build-dependencies] embed-resource = "3.0.11" diff --git a/apps/solidcraft/src/main.rs b/apps/solidcraft/src/main.rs index e2557f28..1d15a9ba 100644 --- a/apps/solidcraft/src/main.rs +++ b/apps/solidcraft/src/main.rs @@ -1,6 +1,9 @@ //! SolidCraft desktop app. //! -//! Usage: `solidcraft [--control ] [--sample] [--crash-log] [design.solidcraft | part.step]` +//! Usage: `solidcraft [--control ] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]` +//! +//! `--safe-mode` (or `SOLIDCRAFT_SAFE_MODE=1`) starts with default settings, no recovery offer and +//! the software renderer; see `startup` and docs/book/src/guide/troubleshooting.md. //! //! A `.step`/`.stp` file opens as a new design holding the file's bodies (an Import feature); a //! `.3mf`/`.stl` file as a new design holding mesh bodies. @@ -20,18 +23,24 @@ mod control_server; mod open_documents; #[cfg(any(target_os = "windows", target_os = "linux"))] mod space_mouse; +mod startup; use solidcraft_engine::{Session, io}; use solidcraft_ui_egui::{Services, SolveApp}; struct App { solve: SolveApp, + /// The settings folder whose start marker the first frame clears (`startup::started`). + starting: Option, #[cfg(any(target_os = "windows", target_os = "linux"))] space_mouse: space_mouse::SpaceMouse, } impl eframe::App for App { fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + if let Some(dir) = self.starting.take() { + startup::started(&dir); + } // The design on screen is the one whose commands a crash report replays. self.solve.session.crash_journal = solidcraft_engine::crash::enabled(); // The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved @@ -306,7 +315,7 @@ const APP_ID: &str = solidcraft_engine::io::APP_ID; fn main() -> eframe::Result { adopt_legacy_settings(); - let samples = msaa(); + let mut safe_mode = startup::env_on(std::env::var(startup::SAFE_MODE_ENV).ok().as_deref()); let mut control_port: Option = solidcraft_engine::geom::env::var("CONTROL_PORT").and_then(|p| p.parse().ok()); let mut files = Vec::new(); let mut sample = false; @@ -315,6 +324,7 @@ fn main() -> eframe::Result { match a.as_str() { "--control" => control_port = args.next().and_then(|p| p.parse().ok()), "--sample" => sample = true, + "--safe-mode" => safe_mode = true, "--version" | "-V" => { println!("solidcraft {}", env!("CARGO_PKG_VERSION")); return Ok(()); @@ -333,12 +343,30 @@ fn main() -> eframe::Result { return Ok(()); } "--help" | "-h" => { - println!("usage: solidcraft [--control PORT] [--sample] [--crash-log] [design.solidcraft | part.step]"); + println!("usage: solidcraft [--control PORT] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]"); return Ok(()); } _ => files.push(a), } } + // Settings that don't read are moved aside; launches that never got a window are counted, + // and after a few in a row this one starts in safe mode (docs/book/src/guide/troubleshooting.md). + let settings = settings_dir(); + let mut notes: Vec = Vec::new(); + let mut failed_starts = 0; + if let Some(dir) = &settings { + notes.extend(startup::check_settings(dir, &[solidcraft_ui_egui::prefs::STORAGE_KEY, solidcraft_ui_egui::prefs::LEGACY_STORAGE_KEY])); + failed_starts = startup::begin(dir); + } + if failed_starts >= startup::FAILED_STARTS_FOR_SAFE_MODE && !safe_mode { + safe_mode = true; + notes.push(format!("The last {failed_starts} launches stopped before the window opened, so this one is in safe mode.")); + } + let cpu_renderer = safe_mode || startup::cpu_renderer_env(std::env::var(startup::RENDERER_ENV).ok().as_deref()); + if safe_mode { + notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into()); + } + let samples = if safe_mode { MSAA } else { msaa() }; // Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md). let crashed = solidcraft_engine::crash::default_dir() .map(|dir| { @@ -355,8 +383,17 @@ fn main() -> eframe::Result { depth_buffer: DEPTH_BITS, stencil_buffer: STENCIL_BITS, multisampling: samples, - // Kept with the other settings, not in a folder named after the app id. - persistence_path: settings_dir().map(|d| d.join("app.ron")), + // Kept with the other settings, not in a folder named after the app id. Safe mode reads + // and writes a scratch copy, leaving the user's settings as they were. + persistence_path: if safe_mode { + let scratch = std::env::temp_dir().join(format!("solidcraft-safe-mode-{}.ron", std::process::id())); + let _ = std::fs::remove_file(&scratch); + Some(scratch) + } else { + settings.as_ref().map(|d| d.join("app.ron")) + }, + persist_window: !safe_mode, + wgpu_options: wgpu_options(cpu_renderer), ..Default::default() }; #[cfg(all(unix, not(target_os = "macos")))] @@ -376,15 +413,20 @@ fn main() -> eframe::Result { // Files opened from Finder and the Dock arrive as events, not arguments. #[cfg(target_os = "macos")] open_documents::install(); + let created = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let created_in_app = created.clone(); + let starting = settings.clone(); let result = eframe::run_native( "SolidCraft", options, Box::new(move |cc| { + created_in_app.store(true, std::sync::atomic::Ordering::SeqCst); solidcraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx); let mut app = SolveApp::new(Session::default(), services()); app.custom_titlebar = CUSTOM_TITLEBAR; app.integrated_titlebar = cfg!(target_os = "macos"); - if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) { + app.cpu_meshing = safe_mode; + if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) { app.load_prefs(&p); } if let Some(rs) = &cc.wgpu_render_state { @@ -429,20 +471,63 @@ fn main() -> eframe::Result { if let Some(dir) = solidcraft_engine::recovery::default_dir() { let every = if app.autosave_minutes > 0.0 { app.autosave_minutes * 60.0 } else { f64::MAX / 4.0 }; solidcraft_ui_egui::recovery_ui::start(&mut app, &dir, std::time::Duration::from_secs_f64(every.min(1e9))); + // Safe mode keeps autosaving but offers nothing: the entries wait for a normal start. + if safe_mode { + app.recoverable.clear(); + } } // Tell the user about crashes since the last start (after the recovery offer is set up). solidcraft_ui_egui::crash_ui::announce(&mut app, &crashed); + if !notes.is_empty() { + app.set_status(notes.join(" "), true); + } #[cfg(any(target_os = "windows", target_os = "linux"))] let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx); Ok(Box::new(App { solve: app, + starting, #[cfg(any(target_os = "windows", target_os = "linux"))] space_mouse, })) }), ); control_server::remove_token_file(); - result + match result { + Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, settings.as_deref()), + r => r, + } +} + +/// The renderer settings: with `cpu`, the software adapter (WARP, lavapipe, llvmpipe). +fn wgpu_options(cpu: bool) -> eframe::egui_wgpu::WgpuConfiguration { + let mut options = eframe::egui_wgpu::WgpuConfiguration::default(); + if cpu && let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup { + setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter)); + } + options +} + +/// The window never opened (no graphics adapter, a device that wouldn't start): once more on +/// the software renderer, else a message box saying what failed instead of a silent exit. +fn no_window(error: eframe::Error, tried_cpu: bool, settings: Option<&std::path::Path>) -> eframe::Result { + log::error!("the window could not open: {error}"); + eprintln!("SolidCraft: the window could not open: {error}"); + if !tried_cpu && let Ok(exe) = std::env::current_exe() { + eprintln!("SolidCraft: trying again with the software renderer ({}=cpu)", startup::RENDERER_ENV); + match std::process::Command::new(exe).args(std::env::args_os().skip(1)).env(startup::RENDERER_ENV, "cpu").status() { + Ok(status) if status.success() => return Ok(()), + // The second try showed its own message. + Ok(_) => return Err(error), + Err(e) => log::error!("could not start again with the software renderer: {e}"), + } + } + let _ = rfd::MessageDialog::new() + .set_level(rfd::MessageLevel::Error) + .set_title("SolidCraft could not start") + .set_description(startup::no_window_message(&error.to_string(), settings)) + .set_buttons(rfd::MessageButtons::Ok) + .show(); + Err(error) } /// Whether to run through XWayland rather than native Wayland. winit has no file drag-and-drop diff --git a/apps/solidcraft/src/startup.rs b/apps/solidcraft/src/startup.rs new file mode 100644 index 00000000..d24fd52a --- /dev/null +++ b/apps/solidcraft/src/startup.rs @@ -0,0 +1,204 @@ +//! Getting the window open, whatever state the settings folder or the graphics device is in +//! (docs/book/src/guide/troubleshooting.md). +//! +//! - **Settings that don't read** (`app.ron`, `graphics.json`): moved aside as +//! `.bad-