From ee5b0938aaa0e6aad3322399943c7ac0a2ab69df Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 15 Oct 2026 13:30:01 -0700 Subject: [PATCH] Windows: after a graphics driver crash, the next start leaves that adapter out When a graphics driver crashed the process while the window started, as Intel's Direct3D 12 driver (igd12umd64.dll) did on an Intel UHD Graphics 605 (#2243), the next start picked the same adapter again. The restart without a failing adapter and the first-frame watch run inside the process, so the crash ended them along with it. Each start now writes graphics-start.json in the log folder before the graphics start, adds the adapter once one is picked, and clears both when the first frame reaches the screen or the app ends without crashing. A start that finds an earlier start still recorded there leaves that adapter out, as a restart does, logs it and shows a note in the status bar. When the crash came while the adapters were being listed (wgpu's Direct3D 12 backend creates a device on each GPU to list it), the next starts list OpenGL's adapters alone, except on macOS. The adapters stay left out until the app version or the Graphics Processor preference changes, a start shows its first frame on one, or no adapter is left. WGPU_BACKEND and WGPU_ADAPTER_NAME choose regardless of the record. Docs: docs/development.md, docs/target-app-parity.md. --- apps/vectorcraft/src/gpu.rs | 550 +++++++++++++++++++++++++++++- apps/vectorcraft/src/main.rs | 24 +- crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + docs/development.md | 3 +- docs/target-app-parity.md | 2 +- 18 files changed, 591 insertions(+), 16 deletions(-) diff --git a/apps/vectorcraft/src/gpu.rs b/apps/vectorcraft/src/gpu.rs index 6819476da..93ca92068 100644 --- a/apps/vectorcraft/src/gpu.rs +++ b/apps/vectorcraft/src/gpu.rs @@ -37,9 +37,23 @@ //! and the app stayed in the background without a window (#964). [`watch_first_frame`] gives the //! first frame [`FIRST_FRAME_TIMEOUT`] once the app is created and, when it doesn't come, starts //! the app again without that adapter in the same way. +//! +//! A driver can also crash the whole process, and then no code in it runs to start it again: +//! Intel's Direct3D 12 driver (`igd12umd64.dll`) ended 0.9.0 before its window appeared (#2243). +//! So each start writes [`START_FILE`] in the log folder before the graphics start, adds the +//! adapter once one is picked, and clears both once the first frame reaches the screen or the app +//! ends without crashing ([`Startup::track`]). The next start that finds a start still recorded +//! there leaves that adapter out, as a restart does, and keeps leaving it out; when the crash came +//! while the adapters were being listed, before one was picked, it lists OpenGL's adapters alone +//! (except on macOS, where wgpu has none). A start that `WGPU_BACKEND` or `WGPU_ADAPTER_NAME` +//! chooses leaves nothing out, and the crashes are forgotten when the app version or +//! Preferences › Performance › Graphics Processor changes, when a start shows its first frame on +//! the adapter that crashed, or when no adapter is left to show the window. +use std::io::{Read as _, Seek as _, Write as _}; +use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, OnceLock}; +use std::sync::{Arc, Mutex, OnceLock, PoisonError}; use std::time::{Duration, Instant}; use eframe::egui_wgpu::NativeAdapterSelectorMethod; @@ -50,6 +64,14 @@ use eframe::wgpu::{self, Backend, Backends, DeviceType, PowerPreference}; /// again. pub const SKIP_ENV: &str = "VECTORCRAFT_GPU_SKIP"; +/// The file in the log folder that records each start of the window until its first frame +/// reaches the screen, and the adapters earlier starts crashed on ([`Startup::track`]). +pub const START_FILE: &str = "graphics-start.json"; + +/// The most bytes of [`START_FILE`] read, and the most crashed adapters it keeps. +const START_FILE_LIMIT: u64 = 64 * 1024; +const CRASHED_LIMIT: usize = 16; + /// wgpu's variable for choosing an adapter by (part of) its name, any case. const NAME_ENV: &str = "WGPU_ADAPTER_NAME"; @@ -348,6 +370,226 @@ fn parse_skip(v: &str) -> Vec { v.split(',').map(str::trim).filter(|k| !k.is_empty()).map(String::from).collect() } +/// Whether `WGPU_BACKEND` or `WGPU_ADAPTER_NAME` is set: then a start uses the backends and the +/// adapter they name, and leaves out none that earlier starts crashed on. +pub fn explicit_choice() -> bool { + std::env::var_os("WGPU_BACKEND").is_some() || std::env::var_os(NAME_ENV).is_some() +} + +/// How the last start ended when it ended before the window's first frame reached the screen, +/// with no error or panic to report: a crash in the graphics driver (#2243), or the process was +/// ended from outside. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Crash { + /// While the adapters were being listed, before one was picked. + Listing, + /// On the adapter with this [`key`] and name. + Adapter { key: String, name: String }, +} + +/// What a start leaves out because earlier starts crashed ([`Startup::track`]). +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LeftOut { + /// The adapters ([`key`]s) that crashed a start. + pub adapters: Vec, + /// A start crashed while listing the adapters: list OpenGL's alone. Never on macOS, where wgpu + /// has no OpenGL adapters to fall back to. + pub opengl_only: bool, + /// The crash the last start ended in, when this start leaves something out for it. + pub crash: Option, +} + +impl LeftOut { + /// `backends` to list: OpenGL alone after a crash while listing, when it is among them; + /// otherwise `backends`. + pub fn backends(&self, backends: Backends) -> Backends { + if self.opengl_only && backends.contains(Backends::GL) { Backends::GL } else { backends } + } +} + +/// An adapter in [`START_FILE`]: its [`key`] and name. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct Recorded { + key: String, + name: String, +} + +/// The start in progress, in [`START_FILE`]. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct Starting { + /// The adapter picked; `None` while the adapters are being listed. + adapter: Option, + /// The listing found an adapter of a backend other than OpenGL. + native: bool, +} + +/// What [`START_FILE`] holds. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct StartRecord { + /// The app version and the Graphics Processor preference (`gpuPreference`) it was written + /// under: the crashes count only for the same ones. + version: String, + preference: String, + /// The start in progress, from before the graphics start until its first frame reaches the + /// screen or the app ends without crashing. + starting: Option, + /// The adapters that crashed a start, oldest first. + crashed: Vec, + /// A start crashed while listing the adapters. + listing_crashed: bool, +} + +impl StartRecord { + /// This start's record, made from the one the last start left (`previous`; `None` when there + /// is none or it can't be read), and the crash that start ended in, if it did. A record + /// written by another version or under another `preference` is dropped. + fn begin(previous: Option, version: &str, preference: &str) -> (Self, Option) { + let mut record = previous.filter(|r| r.version == version && r.preference == preference).unwrap_or_default(); + let crash = record.starting.take().map(|s| match s.adapter { + Some(Recorded { key, name }) => Crash::Adapter { key, name }, + None => Crash::Listing, + }); + match &crash { + Some(Crash::Adapter { key, name }) if !record.crashed.iter().any(|c| &c.key == key) => { + record.crashed.push(Recorded { key: key.clone(), name: name.clone() }); + if record.crashed.len() > CRASHED_LIMIT { + record.crashed.remove(0); + } + } + Some(Crash::Listing) => record.listing_crashed = true, + _ => {} + } + record.version = version.to_string(); + record.preference = preference.to_string(); + record.starting = Some(Starting::default()); + (record, crash) + } + + /// What this start leaves out: nothing when `explicit` ([`explicit_choice`]). `crash` is the + /// one [`begin`](Self::begin) found. + fn left_out(&self, crash: Option, explicit: bool) -> LeftOut { + if explicit { + return LeftOut::default(); + } + let opengl_only = self.listing_crashed && !cfg!(target_os = "macos"); + let crash = crash.filter(|c| matches!(c, Crash::Adapter { .. }) || opengl_only); + LeftOut { adapters: self.crashed.iter().map(|c| c.key.clone()).collect(), opengl_only, crash } + } + + /// The adapter this start picked, after a listing that found one of a backend other than + /// OpenGL when `native`. + fn picked(&mut self, key: &str, name: &str, native: bool) { + self.starting = Some(Starting { adapter: Some(Recorded { key: key.to_string(), name: name.trim().to_string() }), native }); + } + + /// The listing found no adapter that can show the window: nothing is left to leave out, so the + /// crashes are forgotten and the next start tries every adapter again. + fn none_left(&mut self) { + self.crashed.clear(); + self.listing_crashed = false; + } + + /// The window's first frame reached the screen: the adapter it was drawn on, and the listing + /// when it found an adapter of a backend other than OpenGL, no longer count as crashed. + fn presented(&mut self) { + let Some(starting) = self.starting.take() else { return }; + if let Some(adapter) = starting.adapter { + self.crashed.retain(|c| c.key != adapter.key); + } + if starting.native { + self.listing_crashed = false; + } + } + + /// The start ended without crashing: a normal exit, an error, or a restart on another adapter. + fn ended(&mut self) { + self.starting = None; + } + + fn to_json(&self) -> String { + let recorded = |r: &Recorded| serde_json::json!({ "key": r.key, "name": r.name }); + let mut v = serde_json::json!({ + "version": self.version, + "gpuPreference": self.preference, + "crashed": self.crashed.iter().map(recorded).collect::>(), + "listingCrashed": self.listing_crashed, + }); + if let (Some(s), Some(map)) = (&self.starting, v.as_object_mut()) { + map.insert("starting".into(), serde_json::json!({ "adapter": s.adapter.as_ref().map(recorded), "native": s.native })); + } + v.to_string() + } + + /// The record in `text`; `None` when it isn't one. + fn parse(text: &str) -> Option { + let v: serde_json::Value = serde_json::from_str(text).ok()?; + let recorded = |v: &serde_json::Value| -> Option { + Some(Recorded { key: v.get("key")?.as_str()?.to_string(), name: v.get("name")?.as_str()?.to_string() }) + }; + let starting = match v.get("starting") { + None => None, + Some(s) => Some(Starting { + adapter: match s.get("adapter") { + None | Some(serde_json::Value::Null) => None, + Some(a) => Some(recorded(a)?), + }, + native: s.get("native")?.as_bool()?, + }), + }; + let crashed = v.get("crashed")?.as_array()?; + Some(Self { + version: v.get("version")?.as_str()?.to_string(), + preference: v.get("gpuPreference")?.as_str()?.to_string(), + starting, + crashed: crashed.iter().take(CRASHED_LIMIT).map(recorded).collect::>()?, + listing_crashed: v.get("listingCrashed")?.as_bool()?, + }) + } +} + +/// [`START_FILE`], open and locked while the start runs, so that a second copy of the app starting +/// at the same time doesn't take it for the record of a start that crashed. +struct StartFile { + file: std::fs::File, + record: StartRecord, +} + +impl StartFile { + /// Open `dir`'s [`START_FILE`], with the record the last start left in it (`None` when there is + /// none or it can't be read). `Ok(None)` when another copy of the app holds it. + fn open(dir: &Path) -> std::io::Result)>> { + std::fs::create_dir_all(dir)?; + let file = std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(dir.join(START_FILE))?; + match file.try_lock() { + Ok(()) => {} + Err(std::fs::TryLockError::WouldBlock) => return Ok(None), + Err(std::fs::TryLockError::Error(e)) => return Err(e), + } + let mut text = String::new(); + // Text that isn't UTF-8, or is longer than any record, is ignored like a damaged record. + let previous = (&file).take(START_FILE_LIMIT).read_to_string(&mut text).ok().and_then(|_| StartRecord::parse(&text)); + Ok(Some((file, previous))) + } + + fn write(&mut self) { + let text = self.record.to_json(); + let written = self.file.set_len(0).and_then(|()| self.file.rewind()).and_then(|()| self.file.write_all(text.as_bytes())); + if let Err(e) = written { + log::warn!("{START_FILE}: {e}"); + } + } +} + +/// The value of `gpuPreference` the crashes are recorded under: any value this version doesn't +/// know reads as `automatic`, as in [`power_preference`]. +fn preference_name(pref: Option<&str>) -> &'static str { + match pref { + Some("highPerformance") => "highPerformance", + Some("lowPower") => "lowPower", + _ => "automatic", + } +} + /// What the window's start-up leaves for [`finish`]. #[derive(Default)] pub struct Startup { @@ -357,6 +599,8 @@ pub struct Startup { ui: OnceLock, /// When the app was created, the files named at the start opened: the first frame is due. ready: OnceLock, + /// [`START_FILE`] while the start runs ([`Self::track`]). + start: Mutex>, } impl Startup { @@ -371,6 +615,82 @@ impl Startup { // Set once, like `created`. let _ = self.ready.set(Instant::now()); } + + /// Record this start in `dir`'s [`START_FILE`] until the window's first frame reaches the + /// screen or the app ends without crashing, and say what it leaves out for the starts that + /// crashed before (see the module docs). `preference` is Preferences › Performance › Graphics + /// Processor, and `explicit` is [`explicit_choice`]. When the file can't be used (it can't be + /// written, or another copy of the app is starting), nothing is recorded or left out. + pub fn track(&self, dir: &Path, preference: Option<&str>, explicit: bool) -> LeftOut { + let (file, previous) = match StartFile::open(dir) { + Ok(Some(opened)) => opened, + Ok(None) => { + log::info!("{START_FILE}: another VectorCraft is starting, so this start isn't recorded"); + return LeftOut::default(); + } + Err(e) => { + log::warn!("{START_FILE}: {e}"); + return LeftOut::default(); + } + }; + let (record, crash) = StartRecord::begin(previous, env!("CARGO_PKG_VERSION"), preference_name(preference)); + let left_out = record.left_out(crash.clone(), explicit); + match &crash { + Some(Crash::Adapter { key, name }) => { + log::warn!("the last start ended on graphics adapter {name} ({key}) before the window's first frame reached the screen") + } + Some(Crash::Listing) => log::warn!("the last start ended while it listed the graphics adapters"), + None => {} + } + if explicit && (!record.crashed.is_empty() || record.listing_crashed) { + log::info!("WGPU_BACKEND or WGPU_ADAPTER_NAME is set: no adapter is left out for the starts that crashed"); + } + if !left_out.adapters.is_empty() { + log::warn!("leaving out the graphics adapters earlier starts crashed on: {}", left_out.adapters.join(", ")); + } + if left_out.opengl_only { + log::warn!("an earlier start crashed while it listed the graphics adapters: listing OpenGL's alone"); + } + let mut file = StartFile { file, record }; + file.write(); + *self.start.lock().unwrap_or_else(PoisonError::into_inner) = Some(file); + left_out + } + + /// [`selector`] picked the adapter with this [`key`] and name; `native` when the listing found + /// an adapter of a backend other than OpenGL. + fn picked(&self, key: &str, name: &str, native: bool) { + // Set once; eframe picks the adapter once. + let _ = self.adapter.set(key.to_string()); + self.update(|r| r.picked(key, name, native), false); + } + + /// [`selector`] found no adapter that can show the window. + fn none_left(&self) { + self.update(StartRecord::none_left, false); + } + + /// The window's first frame reached the screen, or the window started minimized. + fn presented(&self) { + self.update(StartRecord::presented, true); + } + + /// The app ends, or starts again, without crashing. + fn ended(&self) { + self.update(StartRecord::ended, true); + } + + /// Change the record and write it; `last` releases the file for the next start. + fn update(&self, change: impl FnOnce(&mut StartRecord), last: bool) { + let mut start = self.start.lock().unwrap_or_else(PoisonError::into_inner); + if let Some(file) = start.as_mut() { + change(&mut file.record); + file.write(); + } + if last { + *start = None; + } + } } /// What [`watch_first_frame`] makes of the window's start. @@ -396,10 +716,10 @@ fn first_frame(frames: u64, since: Duration, minimized: bool) -> FirstFrame { } } -/// Watch the window's first frame from another thread: when it hasn't reached the screen -/// [`FIRST_FRAME_TIMEOUT`] after the app was created ([`Startup::ready`]), start the app again -/// without the adapter, as [`finish`] does for one that failed, and end this process, whose UI -/// thread is stuck in the graphics driver. +/// Watch the window's first frame from another thread: once it reaches the screen, clear the start +/// from [`START_FILE`]; when it hasn't reached the screen [`FIRST_FRAME_TIMEOUT`] after the app was +/// created ([`Startup::ready`]), start the app again without the adapter, as [`finish`] does for +/// one that failed, and end this process, whose UI thread is stuck in the graphics driver. pub fn watch_first_frame(startup: Arc) { let watch = move || loop { std::thread::sleep(Duration::from_millis(250)); @@ -409,7 +729,10 @@ pub fn watch_first_frame(startup: Arc) { let minimized = ui.and_then(|c| c.input(|i| i.viewport().minimized)).unwrap_or(false); match first_frame(frames, due.elapsed(), minimized) { FirstFrame::Wait => continue, - FirstFrame::Shown => return, + FirstFrame::Shown => { + startup.presented(); + return; + } FirstFrame::Hung => {} } let mut skip = skipped(); @@ -418,6 +741,8 @@ pub fn watch_first_frame(startup: Arc) { "the window's first frame didn't reach the screen in {}s on graphics adapter {failed}: starting again without it", FIRST_FRAME_TIMEOUT.as_secs() ); + // The new app records its own start. + startup.ended(); skip.push(failed.to_string()); match restart(&skip.join(",")) { Ok(()) => std::process::exit(0), @@ -433,20 +758,22 @@ pub fn watch_first_frame(startup: Arc) { } /// eframe's adapter choice: the first of [`adapter_order`] for `power`, the GPU the `compositor` -/// draws with, the GPUs that drive a `displays`, `WGPU_ADAPTER_NAME` and the adapters a restart -/// left out. Every adapter, the displays and the choice are logged: which GPU draws is the first -/// question in every black-window report. +/// draws with, the GPUs that drive a `displays`, `WGPU_ADAPTER_NAME`, the adapters a restart +/// left out and those `crashed` ([`LeftOut::adapters`]). Every adapter, the displays and the +/// choice are logged: which GPU draws is the first question in every black-window report. pub fn selector( power: PowerPreference, displays: Vec, compositor: Option<(u32, u32)>, + crashed: Vec, startup: Arc, ) -> NativeAdapterSelectorMethod { let named = std::env::var(NAME_ENV).ok(); let skip = skipped(); + let left_out: Vec = skip.iter().chain(&crashed).cloned().collect(); Arc::new(move |adapters, surface| { let candidates: Vec = adapters.iter().map(|a| Candidate::new(a, surface)).collect(); - let order = adapter_order(&candidates, power, &displays, compositor, named.as_deref(), &skip); + let order = adapter_order(&candidates, power, &displays, compositor, named.as_deref(), &left_out); let listed: Vec = candidates .iter() .map(|c| { @@ -455,6 +782,8 @@ pub fn selector( ", can't show the window" } else if skip.contains(&c.key) { ", failed before" + } else if crashed.contains(&c.key) { + ", crashed an earlier start" } else if c.composites(compositor) { ", the compositor draws with it" } else if drives_primary { @@ -470,6 +799,7 @@ pub fn selector( log::info!("graphics adapters: {}", listed.join("; ")); let first = order.first().and_then(|&i| adapters.get(i).zip(candidates.get(i))); let Some((adapter, chosen)) = first else { + startup.none_left(); return Err(format!("no graphics adapter can show the window (power preference {power:?}, {} adapters)", adapters.len())); }; let why = match chosen.drives(&displays) { @@ -480,8 +810,7 @@ pub fn selector( (false, false) => format!("no adapter matches a display GPU, so power preference {power:?}"), }; log::info!("drawing on {} ({:?}, {:?}): {why}", chosen.name.trim(), chosen.backend, chosen.device_type); - // Set once; eframe picks the adapter once. - let _ = startup.adapter.set(chosen.key.clone()); + startup.picked(&chosen.key, &chosen.name, candidates.iter().any(|c| c.backend != Backend::Gl)); Ok(adapter.clone()) }) } @@ -514,6 +843,8 @@ fn restart_without<'a>(graphics_failed: bool, frames: u64, picked: Option<&'a st /// docs), otherwise why it failed. `outcome` is eframe's result, or the message of a panic that /// ended it. pub fn finish(outcome: Result, startup: &Startup) -> Result<(), String> { + // Whatever follows, this start didn't crash; a new app records its own start. + startup.ended(); let (why, graphics_failed) = match outcome { Ok(Ok(())) => return Ok(()), Ok(Err(e @ eframe::Error::Wgpu(_))) => (e.to_string(), true), @@ -1101,4 +1432,199 @@ mod tests { GRAPHICS_PANIC.store(false, Ordering::Relaxed); assert_eq!(finish(Err("a bug".into()), &startup), Err("a bug".into())); } + + /// A log folder of its own for each test. + fn start_dir(tag: &str) -> std::path::PathBuf { + let d = std::env::temp_dir().join(format!("vectorcraft-gpu-start-{tag}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + d + } + + /// What a start in `dir` leaves out; it then ends without crashing. + fn next_start(dir: &std::path::Path, explicit: bool) -> LeftOut { + let s = Startup::default(); + let left_out = s.track(dir, None, explicit); + s.ended(); + left_out + } + + /// The machine of #2243: one Intel UHD Graphics 605, listed through DX12 and OpenGL, and + /// Windows' software adapter. + fn issue_2243() -> Vec { + vec![ + pci_gpu("Intel(R) UHD Graphics 605", Backend::Dx12, DeviceType::IntegratedGpu, (0x8086, 0x3184)), + pci_gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu, (0x1414, 0x008c)), + pci_gpu("Intel(R) UHD Graphics 605", Backend::Gl, DeviceType::Other, (0x8086, 0)), + ] + } + + /// #2243: the driver ends the process after the adapter was picked, before the first frame. + /// The next start leaves that adapter out and draws through OpenGL, and so do the starts after + /// it, without a status-bar note. + #[test] + fn a_start_that_never_showed_a_frame_leaves_its_adapter_out_of_the_next() { + let dir = start_dir("crash"); + let c = issue_2243(); + let first = Startup::default(); + assert_eq!(first.track(&dir, None, false), LeftOut::default()); + first.picked(&c[0].key, &c[0].name, true); + // The process ends here: its file is closed and unlocked, and the start is still recorded. + drop(first); + + let second = Startup::default(); + let left_out = second.track(&dir, Some("automatic"), false); + assert_eq!(left_out.adapters, ["Dx12:8086:3184"]); + assert_eq!(left_out.crash, Some(Crash::Adapter { key: "Dx12:8086:3184".into(), name: "Intel(R) UHD Graphics 605".into() })); + assert!(!left_out.opengl_only); + assert_eq!(left_out.backends(Backends::DX12 | Backends::GL), Backends::DX12 | Backends::GL); + let order = adapter_order(&c, AUTOMATIC, &[], None, None, &left_out.adapters); + assert_eq!(order, [2, 1], "OpenGL on the same GPU before the software adapter"); + second.picked(&c[2].key, &c[2].name, true); + second.presented(); + + let third = Startup::default(); + let left_out = third.track(&dir, None, false); + assert_eq!(left_out.adapters, ["Dx12:8086:3184"], "kept"); + assert_eq!(left_out.crash, None, "the note is shown once"); + third.ended(); + let _ = std::fs::remove_dir_all(&dir); + } + + /// A start whose first frame reached the screen, or that ended without crashing (a normal exit, + /// an error, a restart on another adapter), leaves nothing out of the next. + #[test] + fn a_start_that_showed_its_first_frame_or_ended_is_cleared() { + let dir = start_dir("shown"); + let c = issue_2243(); + for end in [Startup::presented as fn(&Startup), Startup::ended] { + let first = Startup::default(); + first.track(&dir, None, false); + first.picked(&c[0].key, &c[0].name, true); + end(&first); + drop(first); + assert_eq!(next_start(&dir, false), LeftOut::default()); + } + // So does a start that ended before it picked an adapter. + let first = Startup::default(); + first.track(&dir, None, false); + assert!(finish(Ok(Ok(())), &first).is_ok()); + drop(first); + assert_eq!(next_start(&dir, false), LeftOut::default()); + let _ = std::fs::remove_dir_all(&dir); + } + + /// The process ended while the adapters were being listed (wgpu's DX12 backend creates a + /// device on each GPU to list it): the next starts list OpenGL's adapters alone, except on + /// macOS, where wgpu has none. + #[test] + fn a_crash_while_listing_lists_opengl_alone() { + let dir = start_dir("listing"); + let first = Startup::default(); + first.track(&dir, None, false); + drop(first); + let second = Startup::default(); + let left_out = second.track(&dir, None, false); + assert!(left_out.adapters.is_empty()); + if cfg!(target_os = "macos") { + assert_eq!(left_out, LeftOut::default()); + } else { + assert_eq!(left_out.crash, Some(Crash::Listing)); + assert_eq!(left_out.backends(Backends::DX12 | Backends::GL), Backends::GL); + assert_eq!(left_out.backends(Backends::PRIMARY | Backends::GL), Backends::GL); + assert_eq!(left_out.backends(Backends::METAL), Backends::METAL, "no OpenGL to fall back to"); + } + // Listing OpenGL alone found nothing that can show the window: the next start lists every + // backend again. + second.none_left(); + second.ended(); + assert_eq!(next_start(&dir, false), LeftOut::default()); + let _ = std::fs::remove_dir_all(&dir); + } + + /// `WGPU_BACKEND` or `WGPU_ADAPTER_NAME` choose for a start whatever crashed before; when that + /// start shows its first frame on the adapter that crashed, the adapter counts again. + #[test] + fn wgpu_backend_or_adapter_name_ignores_the_crashes() { + let dir = start_dir("explicit"); + let c = issue_2243(); + let first = Startup::default(); + first.track(&dir, None, false); + first.picked(&c[0].key, &c[0].name, true); + drop(first); + assert_eq!(next_start(&dir, true), LeftOut::default()); + assert_eq!(next_start(&dir, false).adapters, ["Dx12:8086:3184"], "still left out without them"); + + let explicit = Startup::default(); + explicit.track(&dir, None, true); + explicit.picked(&c[0].key, &c[0].name, true); + explicit.presented(); + assert_eq!(next_start(&dir, false), LeftOut::default(), "a new driver shows the window"); + let _ = std::fs::remove_dir_all(&dir); + } + + /// A damaged record, a file that can't be opened and one another starting copy of the app holds + /// are ignored: the start goes on with nothing left out. + #[test] + fn a_damaged_or_unreadable_start_file_is_ignored() { + let dir = start_dir("damaged"); + std::fs::create_dir_all(&dir).unwrap(); + let damaged: [&[u8]; 9] = [ + b"", + b"{", + b"[]", + b"null", + b"{\"version\":1}", + b"{\"starting\":{}}", + b"\xef\xbb\xbf\0", + b"\xff\xfe{", + &[b' '; START_FILE_LIMIT as usize + 1], + ]; + for text in damaged { + std::fs::write(dir.join(START_FILE), text).unwrap(); + assert_eq!(next_start(&dir, false), LeftOut::default(), "{:?}", String::from_utf8_lossy(&text[..text.len().min(20)])); + } + + // Another copy of the app is starting: this start neither reads nor writes the file. + let c = issue_2243(); + let starting = Startup::default(); + starting.track(&dir, None, false); + starting.picked(&c[0].key, &c[0].name, true); + assert_eq!(next_start(&dir, false), LeftOut::default()); + drop(starting); + assert_eq!(next_start(&dir, false).adapters, ["Dx12:8086:3184"]); + + // A folder in the file's place. + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(dir.join(START_FILE)).unwrap(); + assert_eq!(next_start(&dir, false), LeftOut::default()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn crashes_are_forgotten_in_another_version_or_under_another_preference() { + let starting = |key: &str| Starting { adapter: Some(Recorded { key: key.into(), name: "GPU".into() }), native: true }; + let (mut record, crash) = StartRecord::begin(None, "0.9.0", "automatic"); + assert_eq!(crash, None); + record.starting = Some(starting("Dx12:8086:3184")); + let (record, crash) = StartRecord::begin(Some(record), "0.9.0", "automatic"); + assert_eq!(crash, Some(Crash::Adapter { key: "Dx12:8086:3184".into(), name: "GPU".into() })); + assert_eq!(record.left_out(None, false).adapters, ["Dx12:8086:3184"]); + // Written and read back. + assert_eq!(StartRecord::parse(&record.to_json()), Some(record.clone())); + assert!(StartRecord::begin(Some(record.clone()), "0.9.1", "automatic").0.crashed.is_empty()); + assert!(StartRecord::begin(Some(record.clone()), "0.9.0", "highPerformance").0.crashed.is_empty()); + assert_eq!(preference_name(None), "automatic"); + assert_eq!(preference_name(Some("powerSaving")), "automatic"); + assert_eq!(preference_name(Some("lowPower")), "lowPower"); + // An adapter is listed once, and the list is bounded. + let mut record = record; + for n in 0..CRASHED_LIMIT + 4 { + for _ in 0..2 { + record.starting = Some(starting(&format!("Dx12:10de:{n:04x}"))); + record = StartRecord::begin(Some(record), "0.9.0", "automatic").0; + } + } + assert_eq!(record.crashed.len(), CRASHED_LIMIT); + assert_eq!(record.crashed.last().map(|c| c.key.as_str()), Some("Dx12:10de:0013")); + } } diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 5517e9ecc..bfa0f3a49 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -761,6 +761,16 @@ fn main() -> std::process::ExitCode { } #[cfg(feature = "wgpu")] let startup = std::sync::Arc::new(gpu::Startup::default()); + // A driver that crashes the process while the window starts leaves this start recorded in the + // log folder; the next start leaves that adapter out (#2243). Runs without preferences, which + // write no log file, don't record their starts. + #[cfg(feature = "wgpu")] + let left_out = match log_dir().filter(|_| prefs_enabled()) { + Some(dir) => startup.track(&dir, gpu_pref, gpu::explicit_choice()), + None => gpu::LeftOut::default(), + }; + #[cfg(feature = "wgpu")] + let crash = left_out.crash.clone(); let custom_titlebar = custom_titlebar(saved.as_ref()); let options = native_options(custom_titlebar); // winit picks Wayland whenever `WAYLAND_DISPLAY` is set; Preferences › Performance › Linux @@ -790,13 +800,14 @@ fn main() -> std::process::ExitCode { options.wgpu_options.surface = eframe::egui_wgpu::SurfaceConfig::LOW_LATENCY; // Only adapters that can show the window, in the order `gpu` gives (#306, #502). if let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup { - create.native_adapter_selector = Some(gpu::selector(power, displays, compositor, startup.clone())); + create.native_adapter_selector = Some(gpu::selector(power, displays, compositor, left_out.adapters.clone(), startup.clone())); // Nothing draws or dispatches indirectly, so wgpu's check of indirect arguments only // costs a compute shader at start-up, one some drivers can't compile (OCLP-patched // Metal on an Iris Pro, #651). `WGPU_VALIDATION_INDIRECT_CALL=1` turns it back on. create.instance_descriptor.flags = (eframe::wgpu::InstanceFlags::from_build_config() - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL).with_env(); - create.instance_descriptor.backends = gpu::backends(create.instance_descriptor.backends, std::env::var_os("WGPU_BACKEND").is_some()); + create.instance_descriptor.backends = + left_out.backends(gpu::backends(create.instance_descriptor.backends, std::env::var_os("WGPU_BACKEND").is_some())); // DX12 shaders compile with FXC, part of Windows: wgpu's default loads whichever // dxcompiler.dll the search path finds, and one without its dxil.dll fails every shader, // losing the device before the first frame (#1194). `WGPU_DX12_COMPILER` picks another. @@ -871,6 +882,15 @@ fn main() -> std::process::ExitCode { app.status(format!( "The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary}{hint}" )); + } else if let Some(crash) = &crash { + app.status(match crash { + gpu::Crash::Adapter { name, .. } => format!( + "The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set" + ), + gpu::Crash::Listing => format!( + "The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set" + ), + }); } app.graphics_adapter = Some(summary); let (loss, ctx) = (graphics_loss.clone(), cc.egui_ctx.clone()); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index dacc7a6f8..7f103971b 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3999,6 +3999,8 @@ axis Both И двете @msg {_1} files {_1} файла @msg The graphics were lost and have been restarted Графиката беше загубена и рестартирана @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Първият опит на графичния процесор да покаже прозореца не успя, затова VectorCraft стартира отново на {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Последното стартиране на VectorCraft прекъсна на {name}, преди прозорецът да се появи, затова VectorCraft стартира на {summary}. За да опитате отново с този графичен процесор, променете Настройки › Производителност › Графичен процесор или стартирайте със зададена променлива WGPU_BACKEND +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Последното стартиране на VectorCraft прекъсна, докато се изброяваха графичните процесори, затова VectorCraft стартира на {summary} само с OpenGL. За да опитате отново другите графични интерфейси, променете Настройки › Производителност › Графичен процесор или стартирайте със зададена променлива WGPU_BACKEND @msg Loading VectorCraft… Зареждане на VectorCraft… @msg VectorCraft failed to start: {e} VectorCraft не успя да стартира: {e} @msg A browser with WebGPU or WebGL2 is required. Изисква се браузър с поддръжка на WebGPU или WebGL2. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 5a2aea292..b018d77df 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4366,6 +4366,8 @@ imageTrace Outline Obrys @msg {_1} files Soubory {_1} @msg The graphics were lost and have been restarted Grafika byla ztracena a byla restartována @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Grafický procesor zkoušený jako první nedokázal zobrazit okno, proto se VectorCraft znovu spustil na {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Poslední spuštění aplikace VectorCraft skončilo na {name} dřív, než se zobrazilo okno, proto se spustila na {summary}. Chcete-li tento grafický procesor zkusit znovu, změňte Předvolby › Výkon › Grafický procesor nebo ji spusťte s nastavenou proměnnou WGPU_BACKEND +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Poslední spuštění aplikace VectorCraft skončilo během zjišťování grafických procesorů, proto se spustila na {summary} jen s OpenGL. Chcete-li znovu zkusit ostatní grafická rozhraní, změňte Předvolby › Výkon › Grafický procesor nebo ji spusťte s nastavenou proměnnou WGPU_BACKEND @msg Loading VectorCraft… Načítání VectorCraftu… @msg VectorCraft failed to start: {e} VectorCraft se nepodařilo spustit: {e} @msg A browser with WebGPU or WebGL2 is required. Je potřeba prohlížeč s WebGPU nebo WebGL2. diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 862c70ecb..c9b1cd693 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3975,6 +3975,8 @@ theme Appearance Erscheinungsbild @msg {_1} files {_1}-Dateien @msg The graphics were lost and have been restarted Die Grafikausgabe ging verloren und wurde neu gestartet @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Der zuerst verwendete Grafikprozessor konnte das Fenster nicht anzeigen, daher wurde VectorCraft auf {summary} neu gestartet +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Der letzte Start von VectorCraft endete auf {name}, bevor das Fenster erschien, daher wurde VectorCraft auf {summary} gestartet. Um diesen Grafikprozessor erneut zu versuchen, ändern Sie Voreinstellungen › Leistung › Grafikprozessor oder starten Sie mit gesetzter Variable WGPU_BACKEND +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Der letzte Start von VectorCraft endete beim Auflisten der Grafikprozessoren, daher wurde VectorCraft auf {summary} nur mit OpenGL gestartet. Um die anderen Grafikschnittstellen erneut zu versuchen, ändern Sie Voreinstellungen › Leistung › Grafikprozessor oder starten Sie mit gesetzter Variable WGPU_BACKEND @msg Loading VectorCraft… VectorCraft wird geladen… @msg VectorCraft failed to start: {e} VectorCraft konnte nicht gestartet werden: {e} @msg A browser with WebGPU or WebGL2 is required. Ein Browser mit WebGPU oder WebGL2 ist erforderlich. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 8c71e88ad..6c56ef4ec 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4072,6 +4072,8 @@ axis Both Ambos @msg {_1} files {_1} archivos @msg The graphics were lost and have been restarted Se perdieron los gráficos y se reiniciaron @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} El procesador gráfico probado primero no pudo mostrar la ventana, así que VectorCraft se inició de nuevo con {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set El último inicio de VectorCraft terminó en {name} antes de que apareciera la ventana, así que se inició con {summary}. Para volver a probar ese procesador gráfico, cambia Preferencias › Rendimiento › Procesador gráfico o inicia con la variable WGPU_BACKEND definida +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set El último inicio de VectorCraft terminó mientras enumeraba los procesadores gráficos, así que se inició solo con OpenGL, en {summary}. Para volver a probar las demás interfaces gráficas, cambia Preferencias › Rendimiento › Procesador gráfico o inicia con la variable WGPU_BACKEND definida @msg Loading VectorCraft… Cargando VectorCraft… @msg VectorCraft failed to start: {e} VectorCraft no pudo iniciarse: {e} @msg A browser with WebGPU or WebGL2 is required. Se necesita un navegador con WebGPU o WebGL2. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a71873605..24b272423 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4069,6 +4069,8 @@ axis Both Les deux @msg {_1} files Fichiers {_1} @msg The graphics were lost and have been restarted Le contexte graphique a été perdu puis redémarré @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Le processeur graphique essayé en premier n’a pas pu afficher la fenêtre : VectorCraft a redémarré sur {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Le dernier démarrage de VectorCraft s’est arrêté sur {name} avant l’affichage de la fenêtre, donc VectorCraft a démarré sur {summary}. Pour réessayer ce processeur graphique, modifiez Préférences › Performances › Processeur graphique ou démarrez avec la variable WGPU_BACKEND définie +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Le dernier démarrage de VectorCraft s’est arrêté pendant l’énumération des processeurs graphiques, donc VectorCraft a démarré sur {summary} avec OpenGL seul. Pour réessayer les autres interfaces graphiques, modifiez Préférences › Performances › Processeur graphique ou démarrez avec la variable WGPU_BACKEND définie @msg Loading VectorCraft… Chargement de VectorCraft… @msg VectorCraft failed to start: {e} Le démarrage de VectorCraft a échoué : {e} @msg A browser with WebGPU or WebGL2 is required. Un navigateur prenant en charge WebGPU ou WebGL2 est nécessaire. diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 66ced89bf..fb7c8c894 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4618,6 +4618,8 @@ imageTrace Outline Körvonal @msg the file's preview header points outside the file a fájl előnézeti fejléce a fájlon kívülre mutat @msg the files come to more than 1 GB: add fewer at a time a fájlok együtt nagyobbak 1 GB-nál: egyszerre kevesebbet adj hozzá @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Az először kipróbált grafikus processzor nem tudta megjeleníteni az ablakot, ezért a VectorCraft újraindult ezen: {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set A VectorCraft legutóbbi indítása még az ablak megjelenése előtt véget ért ezen: {name}, ezért most ezen indult el: {summary}. Ha újra ki szeretnéd próbálni ezt a grafikus processzort, módosítsd a Beállítások › Teljesítmény › Grafikus processzor értékét, vagy indítsd a WGPU_BACKEND változó beállításával +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set A VectorCraft legutóbbi indítása a grafikus processzorok listázása közben ért véget, ezért most csak OpenGL-lel indult el ezen: {summary}. Ha újra ki szeretnéd próbálni a többi grafikus felületet, módosítsd a Beállítások › Teljesítmény › Grafikus processzor értékét, vagy indítsd a WGPU_BACKEND változó beállításával @msg The graphics were lost and couldn't be restarted. A grafikus megjelenítés megszakadt, és nem sikerült újraindítani. @msg The graphics were lost and have been restarted A grafikus megjelenítés megszakadt, és újraindult @msg the guide is on a hidden or locked layer a segédvonal rejtett vagy zárolt rétegen van diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 093747349..d0cb1a6d1 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4075,6 +4075,8 @@ axis Both Entrambi @msg {_1} files {_1} file @msg The graphics were lost and have been restarted Il contesto grafico è stato perso ed è stato riavviato @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Il processore grafico provato per primo non è riuscito a mostrare la finestra, quindi VectorCraft è stato riavviato con {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set L'ultimo avvio di VectorCraft si è interrotto su {name} prima che comparisse la finestra, quindi VectorCraft è stato avviato con {summary}. Per riprovare quel processore grafico, cambia Preferenze › Prestazioni › Processore grafico o avvia con la variabile WGPU_BACKEND impostata +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set L'ultimo avvio di VectorCraft si è interrotto mentre elencava i processori grafici, quindi VectorCraft è stato avviato con {summary} usando solo OpenGL. Per riprovare le altre interfacce grafiche, cambia Preferenze › Prestazioni › Processore grafico o avvia con la variabile WGPU_BACKEND impostata @msg Loading VectorCraft… Caricamento di VectorCraft… @msg VectorCraft failed to start: {e} Impossibile avviare VectorCraft: {e} @msg A browser with WebGPU or WebGL2 is required. È necessario un browser con WebGPU o WebGL2. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 0c5f7f462..c1173274f 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3555,6 +3555,8 @@ theme Appearance 外観 @msg VectorCraft failed to start: {e} VectorCraft を起動できませんでした: {e} @msg Your unsaved changes are kept in this browser: reload the page to get them back. 未保存の変更はこのブラウザーに残っています: 復元するにはページを再読み込みしてください。 @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} 最初に試したグラフィックスプロセッサーではウィンドウを表示できなかったため、{summary} で VectorCraft を再起動しました +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set 前回の VectorCraft の起動はウィンドウが表示される前に {name} で終了したため、{summary} で起動しました。このグラフィックプロセッサーをもう一度試すには、「環境設定 › パフォーマンス › グラフィックプロセッサー」を変更するか、WGPU_BACKEND を設定して起動してください +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set 前回の VectorCraft の起動はグラフィックプロセッサーの一覧を取得している間に終了したため、OpenGL のみを使って {summary} で起動しました。ほかのグラフィックバックエンドをもう一度試すには、「環境設定 › パフォーマンス › グラフィックプロセッサー」を変更するか、WGPU_BACKEND を設定して起動してください @msg not a DXF drawing DXF 図面ではありません @msg scale must be a number from {_1} to {_2}, not {s} 縮尺は {_1} から {_2} までの数値にしてください ({s} は指定できません) @msg the artboard to fit must be a positive size up to {_1} pt 合わせる先のアートボードのサイズは 0 より大きく {_1} pt 以下にしてください diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index ea67dc650..272e295d4 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -3533,6 +3533,8 @@ theme Appearance 외관 @msg VectorCraft failed to start: {e} VectorCraft를 시작하지 못했습니다: {e} @msg Your unsaved changes are kept in this browser: reload the page to get them back. 저장하지 않은 변경 내용은 이 브라우저에 보관되어 있습니다. 페이지를 새로 고치면 복원할 수 있습니다. @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} 처음 시도한 그래픽 프로세서가 창을 표시하지 못해 {summary}에서 VectorCraft를 다시 시작했습니다 +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set 지난번 VectorCraft 시작이 창이 나타나기 전에 {name}에서 끝나서 {summary}에서 시작했습니다. 이 그래픽 프로세서를 다시 시도하려면 환경 설정 › 성능 › 그래픽 프로세서를 변경하거나 WGPU_BACKEND를 설정하고 시작하세요 +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set 지난번 VectorCraft 시작이 그래픽 프로세서 목록을 가져오는 중에 끝나서 OpenGL만 사용해 {summary}에서 시작했습니다. 다른 그래픽 백엔드를 다시 시도하려면 환경 설정 › 성능 › 그래픽 프로세서를 변경하거나 WGPU_BACKEND를 설정하고 시작하세요 @msg not a DXF drawing DXF 도면이 아닙니다 @msg scale must be a number from {_1} to {_2}, not {s} 배율은 {_1}에서 {_2} 사이의 숫자여야 합니다: {s} @msg the artboard to fit must be a positive size up to {_1} pt 맞출 아트보드는 최대 {_1} pt까지의 양수 크기여야 합니다 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 8f68fc7cc..d5a73df7f 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4183,6 +4183,8 @@ imageTrace Outline Kontur @msg {_1} files Pliki {_1} @msg The graphics were lost and have been restarted Utracono grafikę i uruchomiono ją ponownie @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Pierwszy wypróbowany procesor graficzny nie mógł wyświetlić okna, więc VectorCraft uruchomił się ponownie na {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Ostatnie uruchomienie VectorCraft zakończyło się na {name}, zanim pojawiło się okno, więc VectorCraft uruchomił się na {summary}. Aby ponownie wypróbować ten procesor graficzny, zmień Preferencje › Wydajność › Procesor graficzny lub uruchom z ustawioną zmienną WGPU_BACKEND +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Ostatnie uruchomienie VectorCraft zakończyło się podczas wyliczania procesorów graficznych, więc VectorCraft uruchomił się na {summary} tylko z OpenGL. Aby ponownie wypróbować pozostałe interfejsy graficzne, zmień Preferencje › Wydajność › Procesor graficzny lub uruchom z ustawioną zmienną WGPU_BACKEND @msg Loading VectorCraft… Wczytywanie VectorCraft… @msg VectorCraft failed to start: {e} Nie udało się uruchomić VectorCraft: {e} @msg A browser with WebGPU or WebGL2 is required. Wymagana jest przeglądarka z obsługą WebGPU lub WebGL2. diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 33e4670f6..11e2b4f6b 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3657,6 +3657,8 @@ axis Both Ambos @msg {_1} files {_1} arquivos @msg The graphics were lost and have been restarted Os gráficos foram perdidos e reiniciados @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} O processador gráfico tentado primeiro não conseguiu exibir a janela, então o VectorCraft reiniciou em {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set A última inicialização do VectorCraft terminou em {name} antes de a janela aparecer, então o VectorCraft iniciou em {summary}. Para tentar esse processador gráfico novamente, altere Preferências › Desempenho › Processador Gráfico ou inicie com a variável WGPU_BACKEND definida +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set A última inicialização do VectorCraft terminou enquanto listava os processadores gráficos, então o VectorCraft iniciou em {summary} apenas com OpenGL. Para tentar as outras interfaces gráficas novamente, altere Preferências › Desempenho › Processador Gráfico ou inicie com a variável WGPU_BACKEND definida @msg Loading VectorCraft… Carregando o VectorCraft… @msg VectorCraft failed to start: {e} Falha ao iniciar o VectorCraft: {e} @msg A browser with WebGPU or WebGL2 is required. É necessário um navegador com WebGPU ou WebGL2. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 0962693d0..e02dd4efe 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4071,6 +4071,8 @@ axis Both Оба @msg {_1} files {_1} файлов @msg The graphics were lost and have been restarted Графика была потеряна и перезапущена @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Графический процессор, выбранный первым, не смог показать окно, поэтому VectorCraft запустился снова на {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Прошлый запуск VectorCraft завершился на {name} до появления окна, поэтому VectorCraft запустился на {summary}. Чтобы снова попробовать этот графический процессор, измените Настройки › Производительность › Графический процессор или запустите приложение с заданной переменной WGPU_BACKEND +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Прошлый запуск VectorCraft завершился во время перечисления графических процессоров, поэтому VectorCraft запустился на {summary} только с OpenGL. Чтобы снова попробовать другие графические интерфейсы, измените Настройки › Производительность › Графический процессор или запустите приложение с заданной переменной WGPU_BACKEND @msg Loading VectorCraft… Загрузка VectorCraft… @msg VectorCraft failed to start: {e} VectorCraft не удалось запустить: {e} @msg A browser with WebGPU or WebGL2 is required. Требуется браузер с WebGPU или WebGL2. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a116db7a6..bb3a81b08 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4059,6 +4059,8 @@ axis Both Обидві @msg {_1} files {_1} файлів @msg The graphics were lost and have been restarted Графіка була втрачена та перезапущена @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Графічний процесор, обраний першим, не зміг показати вікно, тому VectorCraft запустився знову на {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Попередній запуск VectorCraft завершився на {name} до появи вікна, тому VectorCraft запустився на {summary}. Щоб знову спробувати цей графічний процесор, змініть Налаштування › Продуктивність › Графічний процесор або запустіть застосунок із заданою змінною WGPU_BACKEND +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Попередній запуск VectorCraft завершився під час переліку графічних процесорів, тому VectorCraft запустився на {summary} лише з OpenGL. Щоб знову спробувати інші графічні інтерфейси, змініть Налаштування › Продуктивність › Графічний процесор або запустіть застосунок із заданою змінною WGPU_BACKEND @msg Loading VectorCraft… Завантаження VectorCraft… @msg VectorCraft failed to start: {e} VectorCraft не вдалося запустити: {e} @msg A browser with WebGPU or WebGL2 is required. Потрібен браузер із WebGPU або WebGL2. diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index d04b102cb..9a933b785 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4255,6 +4255,8 @@ theme Appearance Giao diện @msg the file's preview header points outside the file tiêu đề xem trước của tệp trỏ ra ngoài tệp @msg the files come to more than 1 GB: add fewer at a time các tệp tổng cộng hơn 1 GB: thêm ít hơn mỗi lần @msg The graphics processor tried first couldn't show the window, so VectorCraft started again on {summary} Bộ xử lý đồ họa thử trước không hiển thị được cửa sổ, VectorCraft đã khởi động lại trên {summary} +@msg The last start of VectorCraft ended on {name} before its window appeared, so it started on {summary}. To try that graphics processor again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Lần khởi động trước của VectorCraft đã dừng trên {name} trước khi cửa sổ xuất hiện, nên VectorCraft đã khởi động trên {summary}. Để thử lại bộ xử lý đồ họa đó, hãy đổi Tùy chọn › Hiệu suất › Bộ xử lý đồ họa hoặc khởi động với biến WGPU_BACKEND được đặt +@msg The last start of VectorCraft ended while it listed the graphics processors, so it started on {summary} with OpenGL alone. To try the other graphics backends again, change Preferences › Performance › Graphics Processor or start with WGPU_BACKEND set Lần khởi động trước của VectorCraft đã dừng khi đang liệt kê các bộ xử lý đồ họa, nên VectorCraft đã khởi động trên {summary} chỉ với OpenGL. Để thử lại các giao diện đồ họa khác, hãy đổi Tùy chọn › Hiệu suất › Bộ xử lý đồ họa hoặc khởi động với biến WGPU_BACKEND được đặt @msg The graphics were lost and couldn't be restarted. Đồ họa đã mất và không thể khởi động lại. @msg The graphics were lost and have been restarted Đồ họa đã mất và đã được khởi động lại @msg the guide is on a hidden or locked layer đường căn nằm trên lớp ẩn hoặc bị khóa diff --git a/docs/development.md b/docs/development.md index 932941291..b7c14b39f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -50,7 +50,7 @@ How the web shell (`apps/vectorcraft-web/src/web.rs`) differs from desktop: The canvas is rasterized on the CPU (`vectorcraft-render`, vello_cpu); the GPU (wgpu, through eframe) only composites the canvas texture and draws the UI, so any GPU that can show the window will do. The desktop app picks the window's adapter itself (`apps/vectorcraft/src/gpu.rs`, eframe's `native_adapter_selector`) and logs every adapter it found and the one it uses. - **Order:** adapters that report they can't present to the window are never tried; then the one `WGPU_ADAPTER_NAME` names; hardware before software (llvmpipe, WARP); the native backends (Vulkan, Metal, DX12) before OpenGL; with Automatic, the GPU the Wayland compositor draws with, then the GPU that drives a display, and among those the one that drives the primary display; then the power preference; then a GPU's DX12 adapter before its Vulkan one (Intel's Vulkan driver made the whole window flicker black, #545), keeping the system's order among equals. -- **Windows backends:** Direct3D 12, falling back to OpenGL. Vulkan isn't started at all unless `WGPU_BACKEND` asks for it (`WGPU_BACKEND=vulkan`): creating a Vulkan instance loads every installed Vulkan driver into the process, and a faulty one (Intel's `igvk64.dll`, #806) crashed the app before its window appeared. +- **Windows backends:** Direct3D 12, falling back to OpenGL. Vulkan isn't started at all unless `WGPU_BACKEND` asks for it (`WGPU_BACKEND=vulkan`): creating a Vulkan instance loads every installed Vulkan driver into the process, and a faulty one (Intel's `igvk64.dll`, #806) crashed the app before its window appeared. After a start that crashed while it listed the Direct3D 12 adapters, later starts list OpenGL's alone (see the crash bullet below). - **The GPU that drives the display.** A GPU without a monitor is the wrong one even when it says it can show the window: on a desktop with a Ryzen's integrated GPU (no monitor) and an NVIDIA card driving both monitors, power saving picked the integrated GPU, Windows had to present every frame across adapters, and the integrated GPU's driver reset under that: the graphics device was lost, or the desktop's compositor went down and every monitor stayed black for minutes (pdfcraft#378). So Automatic asks the system which GPU drives which display and puts that GPU first; the power preference only orders the rest, and decides alone where the system can't say. - **Windows:** `EnumDisplayDevices` (the display devices attached to the desktop, and the primary one), through the `winsafe` crate; the PCI ids in each device id are matched against wgpu's adapters. The registry doesn't tell a user which adapter drives a display, and WMI reports a resolution for a GPU without a monitor, so neither is used. - **Linux:** the connected connectors in `/sys/class/drm` and their cards' PCI ids; a built-in panel (`eDP`, `LVDS`, `DSI`) counts as the primary display. @@ -63,6 +63,7 @@ The canvas is rasterized on the CPU (`vectorcraft-render`, vello_cpu); the GPU ( - 0.5.0 saved `powerSaving`, its default, for everyone; it reads as `automatic`. - **If the window fails on its adapter while starting up** (an error from wgpu, or a panic inside wgpu or egui-wgpu during the first frames), the app starts again without that adapter and tries the next one, telling which in the status bar; the run that failed keeps its log as `vectorcraft.1.log`. On Unix the new app replaces the process (an AppImage stays mounted); on Windows it starts beside it. Adapters are told apart by backend and PCI ids, or by name where the backend reports no ids (Metal lists every GPU of a dual-GPU Mac as `0000:0000`, #651). The adapters left out travel in `VECTORCRAFT_GPU_SKIP` (`backend:vendor:device`, such as `Vulkan:1002:164e`), one more on each restart, so the restarts end. When no adapter is left, the app logs why and exits with an error instead of panicking. - **If the window's first frame never reaches the screen** within 20 seconds of the app being created (the files named at the start opened), the adapter counts as hanging and the app starts again without it in the same way (#964: High Performance on a hybrid laptop left the app running in the background with no window and nothing in the log). A window minimized at the start isn't counted. When the processor that hung was chosen in Preferences, the status bar points to Automatic. +- **If the app crashes while the window starts**, as when Intel's Direct3D 12 driver (`igd12umd64.dll`) ended 0.9.0 before its window appeared on an Intel UHD Graphics 605 (#2243), no code in the process runs to start it again, so the next start leaves the adapter out. Each start writes `graphics-start.json` in the log folder before the graphics start and adds the adapter once one is picked; when the first frame reaches the screen or the app ends without crashing, the start is cleared from the file. A start that finds an earlier start still recorded there logs the adapter, leaves it out and shows a note in the status bar. When the crash came while the adapters were being listed (wgpu's Direct3D 12 backend creates a device on each GPU to list it), the next start lists OpenGL's adapters alone; on macOS, where wgpu has no OpenGL adapters, it lists them all. Later starts keep leaving them out until the app version or Preferences › Performance › Graphics Processor changes, a start shows its first frame on the adapter that crashed, or no adapter is left that can show the window. `WGPU_BACKEND` and `WGPU_ADAPTER_NAME` choose regardless of the file: a start with `WGPU_BACKEND=dx12` tries Direct3D 12 again, and when its first frame appears, the crash is cleared. A process ended from outside during the start (Task Manager, `kill`) counts as a crash. Runs without preferences (`VECTORCRAFT_NO_PREFS`) record nothing, and a start made while another copy of the app is starting neither reads nor writes the file. - **To recover from a Graphics Processor choice that doesn't work** without editing files: start once with `WGPU_POWER_PREF=low` (or `WGPU_ADAPTER_NAME=…`), which overrides the preference for that start while settings still save, and set Graphics Processor back to Automatic. `VECTORCRAFT_NO_PREFS=1` also starts without it, but saves nothing. - **Environment variables** for when the choice still goes wrong, all read at startup and stronger than the preference: - `WGPU_POWER_PREF`: `low`, `high` or `none` (the system's order). diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index cce6f3a3e..83678458a 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -614,7 +614,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur When the desktop app opens a document on a thread of its own (#1245), that thread also reads the document's linked files and lays out its type, and the UI thread only adds the finished document: in release builds, the step left on the UI thread takes 0.02 ms instead of 1.04 s for a document with 12 linked 3000 × 2000 TIFFs, and it takes 0.1 ms instead of 0.85 s for one with 300 Auto Size area type objects, every second one in three columns. The Layers panel's Delete Selection, Duplicate Selection, Collect in New Layer, Merge Selected, Flatten Artwork, Release to Layers (Sequence) and row drags also take time in proportion to the size of the document, and so do Cut Artboard and Duplicate Artboard with their art and each move of an Alt-drag of one artboard with the Artboard tool while Move/Copy Artwork with Artboard is on (#1429). In Outline view the renderer draws paths and compound paths from the geometry it keeps for them between frames, and it culls the members of clip groups by the bounds it keeps for them. Layers with Preview off and the outlines of Image Trace objects are drawn the same way. -- **Graphics processor:** the canvas is rasterized on the CPU and the GPU only composites it, so any GPU that can show the window will do. Preferences › Performance › Graphics Processor (`gpuPreference`) is Automatic by default: the power-saving GPU on Windows and macOS (the integrated one on hybrid-graphics laptops, where presenting from the discrete GPU made the window flicker, #306), and the GPU the desktop runs on under Linux, where KWin on an NVIDIA GPU refused frames from a Ryzen's integrated GPU and 0.5.0 crashed at startup (#502). Power Saving and High Performance pick a kind of GPU; `WGPU_POWER_PREF`, `WGPU_ADAPTER_NAME` and `WGPU_BACKEND` override them. The app picks among the adapters that can present to the window (native backends before OpenGL, hardware before software, and on Windows a GPU's DX12 adapter before its Vulkan one: Intel's Vulkan driver made the whole window flicker black, #545), and if the window fails on one while starting up it starts again without it instead of crashing; Help › About, `ui.inspect` and the log name the adapter in use. When the browser drops the page's WebGPU device or WebGL context, the web build writes recovery copies, starts again on new graphics with the same app and uploads its textures again; if it can't, it removes the stale canvas and says to reload, which offers the copies back. The desktop app writes the copies and says to restart (#369). On Windows and Linux the tools' cursors are OS cursors (bitmaps rasterized once per display scale), which the system moves itself: painted into the window, they trailed the pointer by the frames waiting to be shown, and the window now queues one frame instead of two (#444). macOS and the web still paint them (macOS sizes cursor bitmaps in points, eframe has no bitmap cursors on the web). Pens and touch drive the tools as the mouse does, and a tap pressed and lifted within one frame clicks where it tapped (#491); pen pressure reaches the Liquify tools' Use Pressure Pen on Windows, while winit 0.30 reports none on macOS or Linux and egui none on the web. On Linux, see `docs/development.md` for pens under Wayland. +- **Graphics processor:** the canvas is rasterized on the CPU and the GPU only composites it, so any GPU that can show the window will do. Preferences › Performance › Graphics Processor (`gpuPreference`) is Automatic by default: the power-saving GPU on Windows and macOS (the integrated one on hybrid-graphics laptops, where presenting from the discrete GPU made the window flicker, #306), and the GPU the desktop runs on under Linux, where KWin on an NVIDIA GPU refused frames from a Ryzen's integrated GPU and 0.5.0 crashed at startup (#502). Power Saving and High Performance pick a kind of GPU; `WGPU_POWER_PREF`, `WGPU_ADAPTER_NAME` and `WGPU_BACKEND` override them. The app picks among the adapters that can present to the window (native backends before OpenGL, hardware before software, and on Windows a GPU's DX12 adapter before its Vulkan one: Intel's Vulkan driver made the whole window flicker black, #545), and if the window fails on one while starting up it starts again without it instead of crashing; after a start that a graphics driver crashed before its first frame, the next starts leave that adapter out, or list OpenGL's adapters alone when the crash came while the adapters were being listed (#2243; verified by unit tests, not on the reporter's Intel UHD Graphics 605); Help › About, `ui.inspect` and the log name the adapter in use. When the browser drops the page's WebGPU device or WebGL context, the web build writes recovery copies, starts again on new graphics with the same app and uploads its textures again; if it can't, it removes the stale canvas and says to reload, which offers the copies back. The desktop app writes the copies and says to restart (#369). On Windows and Linux the tools' cursors are OS cursors (bitmaps rasterized once per display scale), which the system moves itself: painted into the window, they trailed the pointer by the frames waiting to be shown, and the window now queues one frame instead of two (#444). macOS and the web still paint them (macOS sizes cursor bitmaps in points, eframe has no bitmap cursors on the web). Pens and touch drive the tools as the mouse does, and a tap pressed and lifted within one frame clicks where it tapped (#491); pen pressure reaches the Liquify tools' Use Pressure Pen on Windows, while winit 0.30 reports none on macOS or Linux and egui none on the web. On Linux, see `docs/development.md` for pens under Wayland. - **Control transport:** idle sockets expire after 5 minutes, partial lines after an absolute 30 seconds from their first byte, and reply writes after 60 seconds (#1726). Real TCP regressions cover stalled peers and slot recovery; command execution has its separate existing timeout. MCP limits: 16 MiB lines, 64-message batches, 256-step `command_batch` with 8 MiB of kept results and 256 MiB control replies (#1764, fixes #1727). MCP requires the JSON-RPC 2.0 envelope and matches control replies by id, and an app refusal without an id reports the app's reason (#1768, fixes #1728). The MCP server reconnects to the app before a call when the connection was closed or holds stray bytes, after an app restart or an idle expiry (#1739). - **MCP vertical type:** `add_text {vertical: true}` forwards to `text.create` or `text.createInPath`. Regression coverage checks point type, rectangular area type, type in a path and type on a path with true, false and omitted direction. This exposes an existing engine option; it does not change glyph orientation, CJK composition or the type/automation scores. - **Malformed MCP replies:** invalid JSON from the app drops the connection, reports that the operation may have completed and leaves the next call to reconnect; it never replays the edit (#1761). TCP regressions cover malformed and uncorrelated replies, connection closure and a subsequent inspection. Envelope regressions also verify zero backend calls and unchanged initialization state for invalid versions.