From 653f83b8cb21f8ce2cdc873ca712a58c73628352 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Fri, 16 Oct 2026 20:46:40 -0600 Subject: [PATCH] Startup: keep the backend a start was on, whichever it is, and cover it Builds on the fix in this branch, which spotted that a failed OpenGL was being forgotten because `failed` is filtered to this platform's candidates and OpenGL is no candidate off Windows. Rather than spare OpenGL from the filter, run the filter before the previous start's backend is added. A stored entry that is no candidate here came from another machine and still says nothing, so it is dropped as before; the backend a start was actually running on is added afterwards and stays, whichever backend it is. That keeps the same crash out of the next instance without naming OpenGL or the platform, and without holding a failure recorded on someone else's machine against this one. Adds the regression test: on macOS, where Metal is the only candidate, a start left on OpenGL has to keep it out of the instance, while a stored OpenGL entry that never ran here is dropped. --- apps/designcraft/src/gpu/backend.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/apps/designcraft/src/gpu/backend.rs b/apps/designcraft/src/gpu/backend.rs index 3cac6089..0a3cda61 100644 --- a/apps/designcraft/src/gpu/backend.rs +++ b/apps/designcraft/src/gpu/backend.rs @@ -138,17 +138,18 @@ pub fn choose(candidates: &[Backend], previous: &Record, version: &str, skipped: let first = *candidates.first()?; let same_version = previous.version == version; let mut failed: Vec = if same_version { previous.failed.clone() } else { Vec::new() }; + // A stored entry that is no candidate here came from another machine and says nothing. The + // backend the previous start was still on is added after this and stays, candidate or not: + // the instance can load a backend that is no candidate — everywhere but Windows OpenGL rides + // along beside the chosen one (`instance_backends`) — and a start that one took down must + // keep it out of the next instance. + failed.retain(|b| candidates.contains(b)); let unfinished = previous.trying.filter(|_| same_version); if let Some(b) = unfinished && !failed.contains(&b) { failed.push(b); } - // A stale entry for a backend this platform cannot use is dropped, but one that can still - // be reached stays: OpenGL is the in-process fallback off Windows (`instance_backends`) - // without being a candidate there, so forgetting it would offer it again after it failed - // to present — on macOS, where Metal is the only candidate, it never got left out at all. - failed.retain(|b| candidates.contains(b) || (!cfg!(windows) && *b == Backend::Gl)); let pick = |failed: &[Backend]| candidates.iter().copied().find(|b| !failed.contains(b) && !skipped.contains(b)); let backend = match pick(&failed) { Some(b) => b, @@ -391,6 +392,23 @@ mod tests { assert_eq!(c.record.failed, &[Backend::Vulkan]); } + /// The backend the previous start was still on stays in `failed` even where it is no + /// candidate: on macOS Metal is the only candidate, but the instance loads OpenGL beside it, + /// so a start OpenGL took down has to keep it out of the next one. Only the stored list is + /// filtered to this platform's candidates — those entries came from another machine. + #[test] + fn the_backend_a_start_was_on_counts_as_failed_even_where_it_is_no_candidate() { + const MAC: &[Backend] = &[Backend::Metal]; + let c = choose(MAC, &record(Some(Backend::Gl), &[]), V, &[]).unwrap(); + assert_eq!(c.backend, Backend::Metal); + assert_eq!(c.record.failed, &[Backend::Gl], "OpenGL ran here, so it failed here"); + assert_eq!(c.switched_from, Some(Backend::Gl)); + assert!(!instance_backends(c.backend, &c.record.failed).contains(wgpu::Backends::GL)); + // Stored on another machine, never run here: dropped, and OpenGL may ride along again. + let c = choose(MAC, &record(None, &[Backend::Gl]), V, &[]).unwrap(); + assert_eq!(c.record.failed, &[]); + } + /// A restart that found no adapter of DirectX 12 names the backend in its skip list: that start /// takes the next candidate, with or without a record, and the record doesn't keep it as failed /// (a later launch tries it again).