Desktop: pick the graphics backend before the window exists; DirectX 12 on Windows (#297)

* Desktop: pick the graphics backend before the window exists; DirectX 12 on Windows

On an AMD hybrid-graphics laptop (Radeon RX 6800M beside an integrated
Radeon, with two versions of amdvlk64.dll loaded into one process) the
app died a few seconds after launch with an access violation inside
AMD's Vulkan driver at the first swapchain present. Nothing reached the
log: a fault inside a driver is a native crash the engine's panic guard
never sees. wgpu's default backend set (PRIMARY | GL) enumerates Vulkan
first, so every start on that machine went down the same path, while
WGPU_BACKEND=dx12 and WGPU_BACKEND=gl ran fine and the headless CPU
renderer was never affected.

The desktop app now chooses the backend before creating the window
(apps/designcraft/src/gpu.rs): DirectX 12 first on Windows, then Vulkan,
then OpenGL; Vulkan then OpenGL on Linux; Metal on macOS. OpenGL stays
in the set as the in-process fallback when the chosen backend has no
adapter. WGPU_BACKEND still overrides everything and turns the fallback
off.

Start-up fallback: gpu.json in the settings directory records the
backend being tried until the third frame begins (the first has been
presented by then) or the app exits normally. A record left behind means
that backend never showed a frame, so the next start skips it, tries the
next candidate and says so in the status bar. The list starts over when
every candidate has failed or the app version changes;
DESIGNCRAFT_NO_PREFS records nothing. The adapter, backend and driver
version are logged at start-up for bug reports.

Verified on the affected machine: a default start runs on DirectX 12; a
simulated crashed DX12 start moved to Vulkan, which faulted for real,
and the start after that came up on OpenGL with the status line. Nine
unit tests cover the chooser and the record; cargo xtask ci passes.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* Merge origin/main; on Windows OpenGL isn't added to the DX12 or Vulkan instance (#167)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-10 04:11:47 -04:00
1 parent 70974afc66
commit 736acffe5f
6 files changed
+482 -15

No files matched your search

Generated
+1
View File
@@ -870,6 +870,7 @@ dependencies = [
"log",
"muda",
"rfd",
"serde",
"serde_json",
"winit",
"winresource",
+15
View File
@@ -137,9 +137,24 @@ The file stops growing at 16 MiB. `--version` writes no file.
| Variable | Effect |
|---|---|
| `RUST_LOG` | Log levels for standard error and the log file. Default: `info` for DesignCraft's own crates, `warn` for everything else. env_logger-style directives replace that, e.g. `RUST_LOG=debug`, `RUST_LOG=warn,designcraft_render=trace` or `RUST_LOG=info,wgpu_core=warn`; a directive ending in `*` covers every target starting with it (`designcraft*=debug`). |
| `WGPU_BACKEND` | The graphics backend that composites the window: `dx12`, `vulkan`, `gl` or `metal` (a comma-separated list lets wgpu choose among them). Default: DirectX 12 on Windows, Vulkan on Linux, Metal on macOS, with OpenGL as the fallback. Setting it also turns off the start-up fallback described below. |
The logger is `apps/designcraft/src/logging.rs`; the web build logs to the browser console instead.
### Graphics backend
The canvas is rendered on the CPU; the GPU only composites the interface, through wgpu. A
graphics driver that faults takes the process down before any Rust code can catch it, so the
backend is chosen before the window exists, and a start that never showed a frame is remembered:
`gpu.json` in the settings directory records the backend being tried until the first frame has
been presented. A record left behind means that backend crashed (or the app was killed) at
start-up, so the next start tries the next one (DirectX 12, Vulkan, then OpenGL on Windows;
Vulkan, then OpenGL on Linux) and says so in the status bar. The list starts over once every
backend has failed, or with a new DesignCraft version; delete the file or set `WGPU_BACKEND` to
start afresh. Seen in the wild: on an AMD hybrid-graphics laptop (a Radeon RX 6800M beside an
integrated Radeon) the AMD Vulkan driver faulted at the first present, which is why DirectX 12 is
the Windows default. The code is `apps/designcraft/src/gpu.rs`.
### Web
```sh
+1
View File
@@ -37,6 +37,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Missing glyphs, as in InDesign: a character the applied font lacks is drawn as that font's missing-glyph box (its .notdef; a drawn box when the font's is empty), on screen and in PDF (as a path, so PDF/A and PDF/UA exports take it), and Preflight lists the characters by font. Preferences › Composition › Draw Missing Glyphs from Fallback Fonts (`document.preferences {glyphFallback}`, stored with the document) draws them from other fonts instead; it is off in new documents and IDML imports, on in documents saved before it existed. A missing font's substitute always uses fallback fonts.
- Damage-region repaint: after an edit the canvas re-renders only the frames whose composed text (or the items that) changed and patches its texture — pixel-identical to a full render; typing in a frame repaints in under a millisecond instead of a full-viewport render (22 ms measured on the sample).
- Crash recovery: unsaved documents are written to the recovery folder every 30 s and reopened (unsaved, remembering their files) after a crash; File › Revert and Save a Copy…; `file.recovery.*`, `file.revert`, `file.saveACopy`.
- Graphics backend chosen before the window exists (DirectX 12 on Windows, Vulkan on Linux, Metal on macOS, OpenGL as the fallback; `WGPU_BACKEND` overrides) with a start-up fallback: a backend whose start never showed a frame is skipped at the next start and the status bar says so (`gpu.json`). Found on an AMD hybrid-graphics laptop whose Vulkan driver faulted at the first present.
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
+1
View File
@@ -14,6 +14,7 @@ designcraft-engine = { workspace = true }
designcraft-ui-egui = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "links"] }
egui = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
+396
View File
@@ -0,0 +1,396 @@
//! Which wgpu backend draws the window, and the fallback when one of them crashes at start-up.
//!
//! The canvas is rendered on the CPU (`designcraft-render`); the GPU only composites the
//! interface, so every backend shows the same pixels and the one that never crashes is the right
//! one. wgpu's default backend set (`PRIMARY | GL`) enumerates Vulkan first, and on an AMD
//! hybrid-graphics laptop (a Radeon RX 6800M beside an integrated Radeon, two versions of
//! `amdvlk64.dll` loaded into the process) AMD's Vulkan driver faulted with an access violation
//! at the first swapchain present. A fault inside a driver can't be caught (the crash guard never
//! sees it), so the backend is chosen before the window exists: on Windows DirectX 12 first (the
//! backend browsers' WebGPU uses), then Vulkan, then OpenGL; on Linux Vulkan, then OpenGL; on
//! macOS Metal. `WGPU_BACKEND` — wgpu's own variable: `dx12`, `vulkan`, `gl`, `metal`, or a
//! comma-separated list — overrides the choice and turns the fallback off.
//!
//! The fallback: before the window is created, `gpu.json` in the settings directory records the
//! backend being tried; once a frame has been presented, that entry is cleared. An entry still
//! there at the next start means the previous start never got that far — the driver crashed, or
//! the app was killed meanwhile — so that backend joins `failed`, the next candidate is tried and
//! the status bar says so. When every candidate has failed the list starts over, and so does a
//! new DesignCraft version (drivers and wgpu move on). `DESIGNCRAFT_NO_PREFS` records nothing.
use std::path::{Path, PathBuf};
use eframe::wgpu;
use serde::{Deserialize, Serialize};
/// The record's file name inside the settings directory.
pub const FILE: &str = "gpu.json";
/// A graphics backend wgpu can draw the window with.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Backend {
Dx12,
Vulkan,
Metal,
Gl,
}
impl Backend {
/// The name the status bar and the log use.
pub fn name(self) -> &'static str {
match self {
Backend::Dx12 => "DirectX 12",
Backend::Vulkan => "Vulkan",
Backend::Metal => "Metal",
Backend::Gl => "OpenGL",
}
}
/// wgpu's bit for this backend.
pub fn bit(self) -> wgpu::Backends {
match self {
Backend::Dx12 => wgpu::Backends::DX12,
Backend::Vulkan => wgpu::Backends::VULKAN,
Backend::Metal => wgpu::Backends::METAL,
Backend::Gl => wgpu::Backends::GL,
}
}
/// The backend of the adapter wgpu picked; `None` for the web and no-op backends.
pub fn of(backend: wgpu::Backend) -> Option<Backend> {
match backend {
wgpu::Backend::Dx12 => Some(Backend::Dx12),
wgpu::Backend::Vulkan => Some(Backend::Vulkan),
wgpu::Backend::Metal => Some(Backend::Metal),
wgpu::Backend::Gl => Some(Backend::Gl),
_ => None,
}
}
}
/// The backends to try on this platform, best first.
pub fn candidates() -> &'static [Backend] {
if cfg!(windows) {
&[Backend::Dx12, Backend::Vulkan, Backend::Gl]
} else if cfg!(target_os = "macos") {
&[Backend::Metal]
} else {
&[Backend::Vulkan, Backend::Gl]
}
}
/// What `gpu.json` holds.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Record {
/// The DesignCraft version that wrote the record; another version starts afresh.
#[serde(default)]
pub version: String,
/// The backend of a start that has not presented a frame yet.
#[serde(default)]
pub trying: Option<Backend>,
/// Backends whose start never presented a frame.
#[serde(default)]
pub failed: Vec<Backend>,
}
/// The backend for a start, from [`choose`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Choice {
pub backend: Backend,
/// The record to write before the window is created.
pub record: Record,
/// The backend the previous start never finished with, when this start moves away from it.
pub switched_from: Option<Backend>,
}
/// Pick the backend for this start from the candidates (best first) and the previous start's
/// record; `None` without candidates.
pub fn choose(candidates: &[Backend], previous: &Record, version: &str) -> Option<Choice> {
let first = *candidates.first()?;
let same_version = previous.version == version;
let mut failed: Vec<Backend> = if same_version { previous.failed.clone() } else { Vec::new() };
failed.retain(|b| candidates.contains(b));
let unfinished = previous.trying.filter(|_| same_version);
if let Some(b) = unfinished
&& !failed.contains(&b)
{
failed.push(b);
}
let backend = match candidates.iter().copied().find(|b| !failed.contains(b)) {
Some(b) => b,
// Every candidate has failed: start over rather than never starting.
None => {
failed.clear();
first
}
};
let switched_from = unfinished.filter(|b| *b != backend);
Some(Choice { backend, record: Record { version: version.to_owned(), trying: Some(backend), failed }, switched_from })
}
/// The record in `path`; a missing or unreadable file is an empty record.
pub fn read(path: &Path) -> Record {
std::fs::read(path).ok().and_then(|bytes| serde_json::from_slice(&bytes).ok()).unwrap_or_default()
}
/// Write `record` to `path` (creating the directory).
pub fn write(path: &Path, record: &Record) -> std::io::Result<()> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
let bytes = serde_json::to_vec_pretty(record).map_err(std::io::Error::other)?;
std::fs::write(path, bytes)
}
/// A start in progress: the backend chosen for it and the record that says so until a frame has
/// been presented.
#[derive(Debug)]
pub struct Startup {
pub backend: Backend,
pub switched_from: Option<Backend>,
/// `gpu.json`, when the record is persisted.
path: Option<PathBuf>,
record: Record,
}
impl Startup {
/// Choose the backend for this start and record it in `path` (nothing is recorded without
/// one). `None` when `WGPU_BACKEND` is set — wgpu follows it and the fallback is off — or the
/// platform has no candidates.
pub fn begin(path: Option<PathBuf>, version: &str) -> Option<Startup> {
if wgpu::Backends::from_env().is_some_and(|b| !b.is_empty()) {
log::info!("graphics: WGPU_BACKEND is set, wgpu picks the backend and the start-up fallback is off");
return None;
}
Self::begin_unforced(path, version)
}
fn begin_unforced(path: Option<PathBuf>, version: &str) -> Option<Startup> {
let previous = path.as_deref().map(read).unwrap_or_default();
let choice = choose(candidates(), &previous, version)?;
if let Some(p) = &path
&& let Err(e) = write(p, &choice.record)
{
log::warn!("graphics: {}: {e}", p.display());
}
match choice.switched_from {
Some(from) => log::warn!(
"graphics: the last start with {} never showed a frame; trying {} (failed so far: {:?})",
from.name(),
choice.backend.name(),
choice.record.failed
),
None => log::info!("graphics: {} (WGPU_BACKEND overrides)", choice.backend.name()),
}
Some(Startup { backend: choice.backend, switched_from: choice.switched_from, path, record: choice.record })
}
/// The backends wgpu may use: the chosen one, with OpenGL as the in-process fallback when it
/// has no adapter (unless OpenGL itself has failed). Not on Windows: some OpenGL drivers
/// (AMD's) crash while the instance is created (#167), before any fallback could help, so
/// there OpenGL is only ever tried as its own candidate.
pub fn backends(&self) -> wgpu::Backends {
let mut backends = self.backend.bit();
if !cfg!(windows) && !self.record.failed.contains(&Backend::Gl) {
backends |= wgpu::Backends::GL;
}
backends
}
/// wgpu picked an adapter of `backend`: record that one, so a crash is blamed on the backend
/// that is actually drawing.
pub fn started_with(&mut self, backend: Backend) {
if self.record.trying == Some(backend) {
return;
}
self.backend = backend;
self.record.trying = Some(backend);
self.save();
}
/// A frame has been presented (or the app exited normally): this start did not crash.
pub fn presented(&mut self) {
if self.record.trying.take().is_some() {
self.save();
}
}
/// The status-bar line for a start that moved away from a backend that crashed.
pub fn status_line(&self) -> Option<String> {
let from = self.switched_from?;
Some(format!(
"Graphics: switched from {} to {} — the last start with {} never showed a frame (set WGPU_BACKEND to choose).",
from.name(),
self.backend.name(),
from.name()
))
}
fn save(&self) {
if let Some(p) = &self.path
&& let Err(e) = write(p, &self.record)
{
log::warn!("graphics: {}: {e}", p.display());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
const V: &str = "0.5.0";
const WIN: &[Backend] = &[Backend::Dx12, Backend::Vulkan, Backend::Gl];
fn temp_path(tag: &str) -> PathBuf {
static N: AtomicUsize = AtomicUsize::new(0);
let d = std::env::temp_dir().join(format!("designcraft-gpu-{tag}-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
let _ = std::fs::remove_dir_all(&d);
d.join(FILE)
}
fn record(trying: Option<Backend>, failed: &[Backend]) -> Record {
Record { version: V.into(), trying, failed: failed.to_vec() }
}
#[test]
fn every_platform_has_candidates_without_duplicates() {
let c = candidates();
assert!(!c.is_empty());
for (i, b) in c.iter().enumerate() {
assert!(!c[..i].contains(b), "{b:?} twice in {c:?}");
}
if cfg!(windows) {
assert_eq!(c[0], Backend::Dx12, "DirectX 12 is the Windows default (AMD's Vulkan driver faulted at the first present)");
assert!(!c.contains(&Backend::Metal));
}
if cfg!(target_os = "macos") {
assert_eq!(c, &[Backend::Metal]);
}
}
#[test]
fn a_fresh_start_takes_the_first_candidate() {
let c = choose(WIN, &Record::default(), V).unwrap();
assert_eq!(c.backend, Backend::Dx12);
assert_eq!(c.record, record(Some(Backend::Dx12), &[]));
assert_eq!(c.switched_from, None);
assert!(choose(&[], &Record::default(), V).is_none());
}
#[test]
fn a_start_that_never_presented_moves_to_the_next_candidate() {
let c = choose(WIN, &record(Some(Backend::Dx12), &[]), V).unwrap();
assert_eq!(c.backend, Backend::Vulkan);
assert_eq!(c.record, record(Some(Backend::Vulkan), &[Backend::Dx12]));
assert_eq!(c.switched_from, Some(Backend::Dx12));
let c = choose(WIN, &record(Some(Backend::Vulkan), &[Backend::Dx12]), V).unwrap();
assert_eq!(c.backend, Backend::Gl);
assert_eq!(c.record.failed, &[Backend::Dx12, Backend::Vulkan]);
assert_eq!(c.switched_from, Some(Backend::Vulkan));
}
#[test]
fn a_finished_start_keeps_the_failed_list_and_switches_silently() {
// `trying` was cleared by a presented frame; the next start still avoids what failed.
let c = choose(WIN, &record(None, &[Backend::Dx12]), V).unwrap();
assert_eq!(c.backend, Backend::Vulkan);
assert_eq!(c.record.failed, &[Backend::Dx12]);
assert_eq!(c.switched_from, None);
// The same backend failing again is listed once.
let c = choose(WIN, &record(Some(Backend::Dx12), &[Backend::Dx12]), V).unwrap();
assert_eq!(c.record.failed, &[Backend::Dx12]);
}
#[test]
fn when_every_candidate_failed_the_list_starts_over() {
let c = choose(WIN, &record(Some(Backend::Gl), &[Backend::Dx12, Backend::Vulkan]), V).unwrap();
assert_eq!(c.backend, Backend::Dx12);
assert_eq!(c.record.failed, &[]);
assert_eq!(c.switched_from, Some(Backend::Gl));
}
#[test]
fn another_version_or_platform_forgets_what_failed() {
let old = Record { version: "0.4.0".into(), trying: Some(Backend::Dx12), failed: vec![Backend::Vulkan] };
let c = choose(WIN, &old, V).unwrap();
assert_eq!(c.backend, Backend::Dx12);
assert_eq!(c.record, record(Some(Backend::Dx12), &[]));
assert_eq!(c.switched_from, None);
// A backend that is no candidate here (a record copied from another machine) is dropped.
let c = choose(&[Backend::Vulkan, Backend::Gl], &record(None, &[Backend::Metal, Backend::Vulkan]), V).unwrap();
assert_eq!(c.backend, Backend::Gl);
assert_eq!(c.record.failed, &[Backend::Vulkan]);
}
#[test]
fn the_record_round_trips_and_garbage_reads_as_empty() {
let path = temp_path("roundtrip");
assert_eq!(read(&path), Record::default());
let r = record(Some(Backend::Vulkan), &[Backend::Dx12]);
write(&path, &r).expect("write");
assert_eq!(read(&path), r);
let text = std::fs::read_to_string(&path).expect("read");
assert!(text.contains("\"trying\": \"vulkan\"") && text.contains("\"dx12\""), "{text}");
std::fs::write(&path, b"{not json").expect("garbage");
assert_eq!(read(&path), Record::default());
// Unknown fields and missing ones are tolerated.
std::fs::write(&path, br#"{"failed":["gl"],"later":1}"#).expect("partial");
assert_eq!(read(&path), Record { version: String::new(), trying: None, failed: vec![Backend::Gl] });
let _ = std::fs::remove_dir_all(path.parent().expect("dir"));
}
#[test]
fn a_start_records_its_backend_until_a_frame_is_presented() {
let path = temp_path("startup");
let mut s = Startup::begin_unforced(Some(path.clone()), V).expect("candidates");
let first = candidates()[0];
assert_eq!(s.backend, first);
assert_eq!(read(&path).trying, Some(first));
assert!(s.backends().contains(first.bit()));
// OpenGL is the in-process fallback, except on Windows where it is only its own candidate.
assert_eq!(s.backends().contains(wgpu::Backends::GL), !cfg!(windows) || first == Backend::Gl);
assert_eq!(s.status_line(), None);
// wgpu actually picked OpenGL (the chosen backend had no adapter): blame that one.
s.started_with(Backend::Gl);
assert_eq!(read(&path).trying, Some(Backend::Gl));
s.presented();
assert_eq!(read(&path), record(None, &[]));
s.presented();
assert_eq!(read(&path), record(None, &[]));
// The next start after one that never presented: switches, says so, and leaves OpenGL
// out of the set once it failed.
write(&path, &record(Some(Backend::Gl), &[])).expect("write");
let s = Startup::begin_unforced(Some(path.clone()), V).expect("candidates");
if candidates().len() > 1 {
assert_eq!(s.switched_from, Some(Backend::Gl));
let line = s.status_line().expect("status");
assert!(line.contains("OpenGL") && line.contains(s.backend.name()) && line.contains("WGPU_BACKEND"), "{line}");
assert!(!s.backends().contains(wgpu::Backends::GL));
} else {
assert_eq!(s.switched_from, None);
}
let _ = std::fs::remove_dir_all(path.parent().expect("dir"));
}
#[test]
fn without_a_path_nothing_is_written_and_a_bad_path_is_not_fatal() {
let mut s = Startup::begin_unforced(None, V).expect("candidates");
s.started_with(Backend::Gl);
s.presented();
// A file where the directory should be: every write fails, nothing panics.
let blocked = temp_path("blocked");
let dir = blocked.parent().expect("dir");
std::fs::create_dir_all(dir.parent().expect("tmp")).expect("tmp");
std::fs::write(dir, "a file where the directory should be").expect("block");
let mut s = Startup::begin_unforced(Some(blocked.clone()), V).expect("candidates");
s.started_with(Backend::Gl);
s.presented();
let _ = std::fs::remove_file(dir);
}
}
+68 -15
View File
@@ -5,10 +5,14 @@
//! `--control <port>` (or `DESIGNCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server:
//! `{"id":1,"method":"ui.inspect","params":{}}` → `{"id":1,"ok":true,"result":…}`.
//! See `designcraft_ui_egui::control` for the methods.
//!
//! The graphics backend (DirectX 12, Vulkan, Metal or OpenGL) is chosen in `gpu` before the window
//! exists, with a fallback for a driver that crashes at start-up; `WGPU_BACKEND` overrides it.
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod control_server;
mod gpu;
mod logging;
#[cfg(target_os = "macos")]
mod native_menu;
@@ -16,33 +20,54 @@ mod native_menu;
use designcraft_engine::Session;
use designcraft_ui_egui::{DesignApp, Services};
struct App(DesignApp, #[cfg(target_os = "macos")] Option<native_menu::NativeMenu>);
struct App {
app: DesignApp,
#[cfg(target_os = "macos")]
menu: Option<native_menu::NativeMenu>,
/// The graphics start in progress (`gpu.json`) until a frame has been presented; see `gpu`.
startup: Option<gpu::Startup>,
/// Frames begun so far.
frames: u32,
}
impl eframe::App for App {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// The third call: two frames went through `ui`, so the first was presented and the driver
// survived it (a fault at the first present is what the fallback in `gpu` is for).
if self.frames >= 2
&& let Some(s) = self.startup.as_mut()
{
s.presented();
self.startup = None;
}
self.frames = self.frames.saturating_add(1);
#[cfg(target_os = "macos")]
{
if self.1.is_none() && std::env::var_os("DESIGNCRAFT_NO_NATIVE_MENU").is_none() {
self.1 = Some(native_menu::NativeMenu::install(&mut self.0));
if self.menu.is_none() && std::env::var_os("DESIGNCRAFT_NO_NATIVE_MENU").is_none() {
self.menu = Some(native_menu::NativeMenu::install(&mut self.app));
}
if let Some(m) = &mut self.1 {
m.poll(&mut self.0, ctx);
if let Some(m) = &mut self.menu {
m.poll(&mut self.app, ctx);
}
}
self.0.logic(ctx);
self.app.logic(ctx);
}
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
self.0.raw_input_hook(raw);
self.app.raw_input_hook(raw);
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.0.ui(ui);
self.app.ui(ui);
}
fn on_exit(&mut self) {
save_prefs(&self.0);
// A normal exit before the third frame is not a crash.
if let Some(s) = self.startup.as_mut() {
s.presented();
}
save_prefs(&self.app);
}
}
/// The per-user settings directory: `ui.json`, `prefs.json` and `logs/` live here.
/// The per-user settings directory: `ui.json`, `prefs.json`, `gpu.json` and `logs/` live here.
fn config_dir() -> Option<std::path::PathBuf> {
if cfg!(target_os = "macos") {
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/DesignCraft"))
@@ -56,12 +81,17 @@ fn config_dir() -> Option<std::path::PathBuf> {
}
}
/// `DESIGNCRAFT_NO_PREFS`: read and write nothing in the settings directory (tests, demos).
fn no_prefs() -> bool {
std::env::var_os("DESIGNCRAFT_NO_PREFS").is_some()
}
fn prefs_path() -> Option<std::path::PathBuf> {
config_dir().map(|b| b.join("ui.json"))
}
fn load_prefs(app: &mut DesignApp) {
if std::env::var_os("DESIGNCRAFT_NO_PREFS").is_some() {
if no_prefs() {
return;
}
if let Some(p) = prefs_path()
@@ -80,7 +110,7 @@ fn load_prefs(app: &mut DesignApp) {
}
fn save_prefs(app: &DesignApp) {
if std::env::var_os("DESIGNCRAFT_NO_PREFS").is_some() {
if no_prefs() {
return;
}
if let Some(p) = prefs_path() {
@@ -241,6 +271,14 @@ fn main() -> eframe::Result {
if let Some(icon) = app_icon() {
options.viewport = options.viewport.with_icon(icon);
}
// The graphics backend, decided before the window exists: a driver that faults takes the
// process down before any Rust code can catch it (see `gpu`).
let gpu = gpu::Startup::begin(if no_prefs() { None } else { config_dir().map(|d| d.join(gpu::FILE)) }, env!("CARGO_PKG_VERSION"));
if let Some(s) = &gpu
&& let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup
{
create.instance_descriptor.backends = s.backends();
}
// winit has no file drag-and-drop on Wayland (only on X11), so dropping images from the file
// manager showed a "no" cursor. Run through XWayland when it's there; DESIGNCRAFT_WAYLAND=1
// keeps the native Wayland backend.
@@ -255,11 +293,24 @@ fn main() -> eframe::Result {
"DesignCraft",
options,
Box::new(move |cc| {
let mut gpu = gpu;
if let Some(rs) = &cc.wgpu_render_state {
// What a bug report needs to know about the graphics driver.
let info = rs.adapter.get_info();
log::info!("graphics: {} on {:?} ({:?}; driver {} {})", info.name, info.backend, info.device_type, info.driver, info.driver_info);
if let (Some(s), Some(backend)) = (gpu.as_mut(), gpu::Backend::of(info.backend)) {
s.started_with(backend);
}
}
let mut session = Session::new();
// Crash recovery: reopen what a previous run left unsaved, then keep it current.
session.recovery_dir = designcraft_engine::recovery::default_dir();
let recovered = session.execute("file.recovery.open", &serde_json::json!({})).ok();
let mut app = DesignApp::new(session, services());
if let Some(line) = gpu.as_ref().and_then(gpu::Startup::status_line) {
app.status(line);
}
// Recovered documents matter more than the graphics note; both are in the log.
if let Some(n) = recovered.as_ref().and_then(|r| r["opened"].as_array()).map(Vec::len).filter(|n| *n > 0) {
app.status(format!("Recovered {n} unsaved document{} from the last session.", if n == 1 { "" } else { "s" }));
}
@@ -277,11 +328,13 @@ fn main() -> eframe::Result {
eprintln!("designcraft: {f}: {e}");
}
}
Ok(Box::new(App(
Ok(Box::new(App {
app,
#[cfg(target_os = "macos")]
None,
)))
menu: None,
startup: gpu,
frames: 0,
}))
}),
)
}