Startup hardening: safe mode, a software-renderer retry, and bad settings files set aside (#1138)
Startup no longer stops at a bad settings file or a missing GPU: a settings file that fails to load is set aside, a failed GPU start retries with the software renderer, and a safe mode starts with defaults. Adds a troubleshooting page to the guide.
This commit is contained in:
1 parent
8f48544021
commit
2c754c430b
10 files changed
+445
-13
No files matched your search
@@ -184,6 +184,19 @@ pub fn control(app: &mut SolveApp, req: crate::control::ControlRequest) -> Optio
|
||||
None
|
||||
}
|
||||
|
||||
/// An autosave that failed to load goes back in the recovery list, so it can be discarded (or
|
||||
/// left for later) instead of coming back unasked.
|
||||
fn reoffer(app: &mut SolveApp, id: &str, dir: Option<&str>) {
|
||||
let dir = dir.map(std::path::PathBuf::from).or_else(|| app.autosave.as_ref().map(|a| a.dir.clone()));
|
||||
let Some(dir) = dir else { return };
|
||||
if app.recoverable.iter().any(|e| e.id == id) {
|
||||
return;
|
||||
}
|
||||
if let Some(e) = solidcraft_engine::recovery::orphans(&dir).into_iter().find(|e| e.id == id) {
|
||||
app.recoverable.insert(0, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// A recovered design is ours now: autosave it under this app's entry before the old one goes.
|
||||
fn recovered(app: &mut SolveApp, request: &Request) {
|
||||
let Kind::Recover { id, dir } = &request.kind else { return };
|
||||
@@ -448,7 +461,12 @@ pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
Err(e) => {
|
||||
request.answer(Err(e.clone()));
|
||||
app.set_status(e, true);
|
||||
if let Kind::Recover { id, dir } = &request.kind {
|
||||
reoffer(app, id, dir.as_deref());
|
||||
app.set_status(format!("could not recover {}: {e}. Discard it or choose Later", request.path), true);
|
||||
} else {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,9 +757,10 @@ fn tessellation(adapter: &wgpu::Adapter, device: &wgpu::Device, queue: &wgpu::Qu
|
||||
}
|
||||
|
||||
/// Create the pipelines. Call once with the app's render state and its depth/stencil/MSAA
|
||||
/// settings. Without a stencil the section cap does not hide the inside of cut bodies.
|
||||
pub fn install(rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32) -> GpuTarget {
|
||||
let tess = tessellation(&rs.adapter, &rs.device, &rs.queue, solidcraft_gpu::SHADER);
|
||||
/// settings. Without a stencil the section cap does not hide the inside of cut bodies. Without
|
||||
/// `gpu_meshing` (safe mode) bodies are meshed on the CPU.
|
||||
pub fn install(rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32, gpu_meshing: bool) -> GpuTarget {
|
||||
let tess = if gpu_meshing { tessellation(&rs.adapter, &rs.device, &rs.queue, solidcraft_gpu::SHADER) } else { None };
|
||||
let t = GpuTarget { format: rs.target_format, depth: egui_wgpu::depth_format_from_bits(depth_bits, stencil_bits), samples, tess: tess.is_some() };
|
||||
let res = Resources::new(&rs.device, t, tess);
|
||||
rs.renderer.write().callback_resources.insert(res);
|
||||
|
||||
@@ -290,6 +290,9 @@ pub struct SolveApp {
|
||||
pub custom_titlebar: bool,
|
||||
/// macOS: the traffic lights sit over the application bar's left end.
|
||||
pub integrated_titlebar: bool,
|
||||
/// Build bodies' display meshes on the CPU even where the GPU could (safe mode). Read by
|
||||
/// [`SolveApp::set_wgpu`].
|
||||
pub cpu_meshing: bool,
|
||||
pub frame_ms: f64,
|
||||
pub synthetic: Vec<egui::Event>,
|
||||
/// The modifiers of the synthetic press in progress (a Ctrl-drag keeps Ctrl while it moves).
|
||||
@@ -357,6 +360,7 @@ impl SolveApp {
|
||||
autosave_minutes: 5.0,
|
||||
custom_titlebar: false,
|
||||
integrated_titlebar: false,
|
||||
cpu_meshing: false,
|
||||
frame_ms: 0.0,
|
||||
synthetic: Vec::new(),
|
||||
synthetic_mods: egui::Modifiers::default(),
|
||||
@@ -389,7 +393,7 @@ impl SolveApp {
|
||||
/// Draw the viewport on the GPU (eframe's wgpu render state, with the app's depth and
|
||||
/// stencil buffer bits and MSAA sample count). Without it the viewport renders on the CPU.
|
||||
pub fn set_wgpu(&mut self, rs: &egui_wgpu::RenderState, depth_bits: u8, stencil_bits: u8, samples: u32) {
|
||||
self.viewport.gpu = Some(gpu::install(rs, depth_bits, stencil_bits, samples));
|
||||
self.viewport.gpu = Some(gpu::install(rs, depth_bits, stencil_bits, samples, !self.cpu_meshing));
|
||||
self.msaa_supported = rs.msaa_supported.clone();
|
||||
let info = rs.adapter.get_info();
|
||||
self.help.gpu = Some(format!("{} ({:?}, {:?})", info.name.trim(), info.backend, info.device_type));
|
||||
@@ -468,7 +472,7 @@ impl SolveApp {
|
||||
if let Some(t) = v.get("toolbar") {
|
||||
self.toolbar_custom.load(t);
|
||||
}
|
||||
if let Some(mut p) = v.get("preferences").and_then(|p| serde_json::from_value::<prefs::Prefs>(p.clone()).ok()) {
|
||||
if let Some(mut p) = v.get("preferences").filter(|p| p.is_object()).map(prefs::lenient) {
|
||||
if !prefs::MSAA.contains(&p.msaa) {
|
||||
p.msaa = prefs::Prefs::default().msaa;
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ pub const MSAA: [u32; 4] = [1, 2, 4, 8];
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NavigatorStyle {
|
||||
/// Also what a navigator this version doesn't know reads as (`lenient`).
|
||||
#[default]
|
||||
Axes,
|
||||
Cube,
|
||||
@@ -1101,6 +1102,28 @@ pub fn control(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Saved preferences, keeping every one that still reads. A value this version doesn't
|
||||
/// understand (a setting a newer or older release saved differently, a hand edit) falls back to
|
||||
/// its default on its own instead of discarding all of them.
|
||||
pub fn lenient(saved: &serde_json::Value) -> Prefs {
|
||||
if let Ok(p) = serde_json::from_value::<Prefs>(saved.clone()) {
|
||||
return p;
|
||||
}
|
||||
let Ok(mut cur) = serde_json::to_value(Prefs::default()) else { return Prefs::default() };
|
||||
if let (Some(o), Some(src)) = (cur.as_object_mut(), saved.as_object()) {
|
||||
for (k, v) in src.iter().take(1000) {
|
||||
// Keys this version doesn't have are ignored anyway.
|
||||
let Some(old) = o.get(k).cloned() else { continue };
|
||||
o.insert(k.clone(), v.clone());
|
||||
if serde_json::from_value::<Prefs>(serde_json::Value::Object(o.clone())).is_err() {
|
||||
log::warn!("preferences: `{k}` is not understood by this version; using its default");
|
||||
o.insert(k.clone(), old);
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::from_value(cur).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Set preferences from JSON (control channel); returns them all.
|
||||
pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let mut cur = serde_json::to_value(&app.preferences).map_err(|e| e.to_string())?;
|
||||
@@ -1151,6 +1174,26 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn a_value_this_version_does_not_understand_keeps_the_other_preferences() {
|
||||
use serde_json::json;
|
||||
// A navigator, a field of the wrong type and a key from another release.
|
||||
let saved = json!({"nav": "solidworks", "navigator_style": "hologram", "ui_scale": "big", "zoom_reverse": true, "from_the_future": 3});
|
||||
let p = super::lenient(&saved);
|
||||
assert_eq!(p.nav, "solidworks");
|
||||
assert!(p.zoom_reverse);
|
||||
assert_eq!(p.navigator_style, super::NavigatorStyle::Axes);
|
||||
assert_eq!(p.ui_scale, super::Prefs::default().ui_scale);
|
||||
assert_eq!(super::lenient(&json!("garbage")), super::Prefs::default());
|
||||
// Whole-app load: the other saved state is kept too, and nothing panics.
|
||||
let mut app = crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
app.load_prefs(&json!({"grid": false, "preferences": saved}).to_string());
|
||||
assert!(!app.ui.show_grid && app.preferences.nav == "solidworks" && app.preferences.zoom_reverse);
|
||||
for junk in ["", "{", "[]", "null", "{\"preferences\": 7, \"recent\": {}, \"shortcuts\": 1, \"toolbar\": \"x\"}"] {
|
||||
crate::SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default()).load_prefs(junk);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_prefs_fall_back_to_the_legacy_key() {
|
||||
let only_old = |k: &str| (k == super::LEGACY_STORAGE_KEY).then(|| "old".to_string());
|
||||
|
||||
Reference in new issue
Block a user