mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Lost window device: close only once every unsaved document is saved for recovery, retrying every 2 s; also check the device on the screen-picker frame (#6464 review)
This commit is contained in:
@@ -43,7 +43,9 @@ pub fn check(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
/// with shapes panics creating its staging buffers (#1215), and one without leaves a frozen window
|
||||
/// whose close button opens an unsaved-changes prompt nobody can see. So this frame and every later
|
||||
/// one draw nothing; the first writes every unsaved document to crash recovery, waits for the
|
||||
/// writes, and closes the app without asking. The next launch reopens the documents.
|
||||
/// writes, and closes the app without asking once all of them are saved. The next launch reopens
|
||||
/// the documents. If a write fails (a full disk, say) the app stays open with the documents in
|
||||
/// memory and tries again every [`RECOVERY_RETRY_SECS`]: closing would lose them.
|
||||
pub fn check_window(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
let Some((device, health)) = app.window_device.as_ref() else { return };
|
||||
// A loss while the app sat idle or the PC slept is noticed here, before the next frame.
|
||||
@@ -52,36 +54,78 @@ pub fn check_window(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
return;
|
||||
}
|
||||
ctx.graphics_mut(|g| *g = Default::default());
|
||||
if app.window_lost {
|
||||
return;
|
||||
let now = ctx.input(|i| i.time);
|
||||
if !app.window_lost {
|
||||
app.window_lost = true;
|
||||
let reason = health.fault().map_or_else(|| "GPU device lost".to_string(), |f| f.to_string());
|
||||
log::error!("{reason}: the window can't be drawn any more; saving unsaved documents for recovery and closing");
|
||||
} else {
|
||||
match app.window_recovery_retry {
|
||||
// Saved, and the close is on its way.
|
||||
None => return,
|
||||
Some(at) if now < at => {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64((at - now).max(0.0)));
|
||||
return;
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
match save_for_recovery(app) {
|
||||
Ok(()) => {
|
||||
app.window_recovery_retry = None;
|
||||
app.allow_close = true;
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{e}; keeping the documents open and trying again in {RECOVERY_RETRY_SECS} s");
|
||||
app.window_recovery_retry = Some(now + RECOVERY_RETRY_SECS);
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(RECOVERY_RETRY_SECS));
|
||||
}
|
||||
}
|
||||
app.window_lost = true;
|
||||
let reason = health.fault().map_or_else(|| "GPU device lost".to_string(), |f| f.to_string());
|
||||
log::error!("{reason}: the window can't be drawn any more; saving unsaved documents for recovery and closing");
|
||||
save_for_recovery(app);
|
||||
app.allow_close = true;
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
|
||||
/// Queue a crash-recovery autosave of every document with unsaved changes, whatever the autosave
|
||||
/// timer and preference, and wait until the writes finished (see `Services::flush_autosaves`).
|
||||
fn save_for_recovery(app: &mut PhotocraftApp) {
|
||||
/// Seconds between attempts to save unsaved work for recovery after the window's device was lost.
|
||||
pub const RECOVERY_RETRY_SECS: f64 = 2.0;
|
||||
|
||||
/// Write every document with unsaved changes to crash recovery, whatever the autosave timer and
|
||||
/// preference, and wait until the writes finished (see `Services::flush_autosaves`). `Ok` only when
|
||||
/// each of them is known to be on disk.
|
||||
fn save_for_recovery(app: &mut PhotocraftApp) -> Result<(), String> {
|
||||
let jobs: Vec<_> = app.session.documents().iter().filter(|d| d.is_dirty()).map(|d| (d.doc.clone(), d.revision, d.path.clone())).collect();
|
||||
if let Some(save) = app.services.autosave.as_mut() {
|
||||
for (doc, revision, path) in &jobs {
|
||||
if let Err(e) = save(doc, *revision, path.as_deref()) {
|
||||
log::error!("couldn't save {} for recovery: {e}", doc.name);
|
||||
}
|
||||
if jobs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// (document id, why) for each document not known to be saved.
|
||||
let mut failed: Vec<(u64, String)> = Vec::new();
|
||||
let Some(save) = app.services.autosave.as_mut() else { return Err("no crash-recovery autosave".into()) };
|
||||
for (doc, revision, path) in &jobs {
|
||||
if let Err(e) = save(doc, *revision, path.as_deref()) {
|
||||
failed.push((doc.id.0, format!("{}: {e}", doc.name)));
|
||||
}
|
||||
}
|
||||
if let Some(flush) = app.services.flush_autosaves.as_mut() {
|
||||
for (id, revision, outcome) in flush() {
|
||||
match outcome {
|
||||
Ok(()) => log::info!("document {id} (revision {revision}) saved for recovery"),
|
||||
Err(e) => log::error!("couldn't save document {id} for recovery: {e}"),
|
||||
}
|
||||
// Without a flush, only a service whose `autosave` reports the finished write can confirm it.
|
||||
let outcomes = match app.services.flush_autosaves.as_mut() {
|
||||
Some(flush) => flush(),
|
||||
None if app.services.autosave_results.is_none() => Vec::new(),
|
||||
None => return Err("crash-recovery autosaves can't be waited for".into()),
|
||||
};
|
||||
let confirm = app.services.flush_autosaves.is_some();
|
||||
for (doc, revision, _) in &jobs {
|
||||
if failed.iter().any(|(id, _)| *id == doc.id.0) {
|
||||
continue;
|
||||
}
|
||||
// The writer skips to the newest queued snapshot, so a later revision confirms this one.
|
||||
let mine: Vec<_> = outcomes.iter().filter(|(id, written, _)| *id == doc.id.0 && written >= revision).collect();
|
||||
if let Some((.., Err(e))) = mine.iter().find(|(.., outcome)| outcome.is_err()) {
|
||||
failed.push((doc.id.0, format!("{}: {e}", doc.name)));
|
||||
} else if confirm && mine.is_empty() {
|
||||
failed.push((doc.id.0, format!("{}: the write wasn't confirmed", doc.name)));
|
||||
} else {
|
||||
log::info!("{} (revision {revision}) saved for recovery", doc.name);
|
||||
}
|
||||
}
|
||||
let failed: Vec<_> = failed.into_iter().map(|(_, why)| why).collect();
|
||||
if failed.is_empty() { Ok(()) } else { Err(format!("couldn't save for recovery: {}", failed.join("; "))) }
|
||||
}
|
||||
|
||||
/// Drop the GPU canvas after `fault`: free its resources, send every view through the CPU
|
||||
|
||||
Reference in New Issue
Block a user