forked from 1b-agent/opencadstudio-upstream
feat(gpu): route legacy hardware to OpenGL, add hardware info to About, and expose Graphics options tab
This commit is contained in:
1 parent
81bb8d48cf
commit
abf302eba5
12 files changed
+838
-21
No files matched your search
+1
-1
@@ -74,7 +74,7 @@ naga = { version = "27", features = ["wgsl-in"] }
|
|||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
libloading = "0.8"
|
libloading = "0.8"
|
||||||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Security", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing", "Win32_Graphics_Gdi", "Win32_Storage_Xps", "Win32_Globalization", "Win32_Security_Authentication_Identity"] }
|
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Security", "Win32_System_Com", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing", "Win32_Graphics_Gdi", "Win32_Storage_Xps", "Win32_Globalization", "Win32_Security_Authentication_Identity"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] }
|
ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] }
|
||||||
|
|||||||
@@ -619,6 +619,10 @@ pub(super) struct OpenCADStudio {
|
|||||||
dyn_mode: i16,
|
dyn_mode: i16,
|
||||||
/// Currently visible page in the application Options dialog.
|
/// Currently visible page in the application Options dialog.
|
||||||
options_tab: crate::ui::window::options::OptionsTab,
|
options_tab: crate::ui::window::options::OptionsTab,
|
||||||
|
/// Saved graphics options (backend, compat renderer); read at launch, so
|
||||||
|
/// edits here only take effect after a restart.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
graphics_prefs: crate::gpu_backend::GraphicsPrefs,
|
||||||
/// The Options window's commit point (see `options_session`).
|
/// The Options window's commit point (see `options_session`).
|
||||||
options_saved: Option<options_session::OptionsSnapshot>,
|
options_saved: Option<options_session::OptionsSnapshot>,
|
||||||
/// Close was pressed with unapplied changes; the discard guard is up.
|
/// Close was pressed with unapplied changes; the discard guard is up.
|
||||||
@@ -2262,6 +2266,15 @@ pub enum Message {
|
|||||||
OptionsOpen,
|
OptionsOpen,
|
||||||
/// Switch the visible page in Options.
|
/// Switch the visible page in Options.
|
||||||
OptionsTabChanged(crate::ui::window::options::OptionsTab),
|
OptionsTabChanged(crate::ui::window::options::OptionsTab),
|
||||||
|
/// Options > Graphics: pin a backend (applies at next launch).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
GraphicsBackendChanged(crate::gpu_backend::BackendChoice),
|
||||||
|
/// Options > Graphics: force the packed compatibility renderer.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
GraphicsCompatToggled(bool),
|
||||||
|
/// Options > Graphics: prefer OpenGL on older GPUs.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
GraphicsLegacyGlToggled(bool),
|
||||||
/// Set CURSORSIZE from the Display-page slider.
|
/// Set CURSORSIZE from the Display-page slider.
|
||||||
CursorSizeChanged(i32),
|
CursorSizeChanged(i32),
|
||||||
/// Set PICKBOX from the Selection-page slider.
|
/// Set PICKBOX from the Selection-page slider.
|
||||||
@@ -4153,6 +4166,8 @@ impl OpenCADStudio {
|
|||||||
dyn_input: true,
|
dyn_input: true,
|
||||||
dyn_mode: 3,
|
dyn_mode: 3,
|
||||||
options_tab: crate::ui::window::options::OptionsTab::General,
|
options_tab: crate::ui::window::options::OptionsTab::General,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
graphics_prefs: crate::gpu_backend::load_prefs(),
|
||||||
options_saved: None,
|
options_saved: None,
|
||||||
options_close_confirm: false,
|
options_close_confirm: false,
|
||||||
spacemouse: {
|
spacemouse: {
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ where
|
|||||||
"modeling" => OptionsTab::Modeling,
|
"modeling" => OptionsTab::Modeling,
|
||||||
"selection" => OptionsTab::Selection,
|
"selection" => OptionsTab::Selection,
|
||||||
"user-preferences" => OptionsTab::UserPreferences,
|
"user-preferences" => OptionsTab::UserPreferences,
|
||||||
|
"graphics" => OptionsTab::Graphics,
|
||||||
_ => OptionsTab::General,
|
_ => OptionsTab::General,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-10
@@ -7932,6 +7932,27 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
Message::GraphicsBackendChanged(choice) => {
|
||||||
|
self.graphics_prefs.backend = choice;
|
||||||
|
crate::gpu_backend::save_prefs(self.graphics_prefs);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
Message::GraphicsCompatToggled(on) => {
|
||||||
|
self.graphics_prefs.compat_renderer = on;
|
||||||
|
crate::gpu_backend::save_prefs(self.graphics_prefs);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
Message::GraphicsLegacyGlToggled(on) => {
|
||||||
|
self.graphics_prefs.legacy_gl = on;
|
||||||
|
crate::gpu_backend::save_prefs(self.graphics_prefs);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::CursorSizeChanged(value) => {
|
Message::CursorSizeChanged(value) => {
|
||||||
self.cursor_size = value.clamp(1, 100);
|
self.cursor_size = value.clamp(1, 100);
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
@@ -8976,16 +8997,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::AboutCopyInfo => {
|
Message::AboutCopyInfo => {
|
||||||
let info = format!(
|
let info = crate::ui::window::about::report();
|
||||||
"Open CAD Studio v{}\nRevision: {}\nCommit date: {}\nProfile: {}\nFeatures: {}\nOS: {}\nArch: {}",
|
|
||||||
env!("OCS_FULL_VERSION"),
|
|
||||||
env!("OCS_GIT_REV"),
|
|
||||||
env!("OCS_COMMIT_DATE"),
|
|
||||||
env!("OCS_BUILD_PROFILE"),
|
|
||||||
env!("OCS_BUILD_FEATURES"),
|
|
||||||
crate::ui::window::about::platform_name(),
|
|
||||||
crate::ui::window::about::architecture_name(),
|
|
||||||
);
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
crate::sys::write_clipboard_text(&info);
|
crate::sys::write_clipboard_text(&info);
|
||||||
|
|||||||
@@ -332,6 +332,16 @@ impl OpenCADStudio {
|
|||||||
self.spacemouse_preferences, self.spacemouse.status(),
|
self.spacemouse_preferences, self.spacemouse.status(),
|
||||||
self.spacemouse_paused, self.spacemouse_details,
|
self.spacemouse_paused, self.spacemouse_details,
|
||||||
),
|
),
|
||||||
|
{
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let page = crate::ui::window::options::graphics::view(
|
||||||
|
self.graphics_prefs,
|
||||||
|
crate::gpu_backend::active_gpu(),
|
||||||
|
);
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let page = iced::widget::Space::new().into();
|
||||||
|
page
|
||||||
|
},
|
||||||
&self.snap_angle_input,
|
&self.snap_angle_input,
|
||||||
&self.zoom_factor_input,
|
&self.zoom_factor_input,
|
||||||
{
|
{
|
||||||
|
|||||||
+347
-7
@@ -76,6 +76,174 @@ pub fn gpu_guard_active() -> bool {
|
|||||||
!candidate_backends().is_empty()
|
!candidate_backends().is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test hook: make the synthetic probe report a legacy (below-baseline) GPU.
|
||||||
|
pub const FORCE_LEGACY_ENV: &str = "OCS_GPU_PROBE_FORCE_LEGACY";
|
||||||
|
|
||||||
|
/// The backend the user pinned in Options > Graphics.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum BackendChoice {
|
||||||
|
/// Probe and pick the best backend (the default).
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
Dx12,
|
||||||
|
Vulkan,
|
||||||
|
Gl,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackendChoice {
|
||||||
|
/// Choices offered on this OS.
|
||||||
|
pub fn available() -> Vec<BackendChoice> {
|
||||||
|
let mut v = vec![BackendChoice::Auto];
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
v.push(BackendChoice::Dx12);
|
||||||
|
}
|
||||||
|
v.push(BackendChoice::Vulkan);
|
||||||
|
v.push(BackendChoice::Gl);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `WGPU_BACKEND` value, or `None` for automatic.
|
||||||
|
pub fn as_str(self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
BackendChoice::Auto => None,
|
||||||
|
BackendChoice::Dx12 => Some("dx12"),
|
||||||
|
BackendChoice::Vulkan => Some("vulkan"),
|
||||||
|
BackendChoice::Gl => Some("gl"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(s: &str) -> Self {
|
||||||
|
match s.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"dx12" => BackendChoice::Dx12,
|
||||||
|
"vulkan" => BackendChoice::Vulkan,
|
||||||
|
"gl" => BackendChoice::Gl,
|
||||||
|
_ => BackendChoice::Auto,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
BackendChoice::Auto => "Automatic (recommended)",
|
||||||
|
BackendChoice::Dx12 => "DirectX 12",
|
||||||
|
BackendChoice::Vulkan => "Vulkan",
|
||||||
|
BackendChoice::Gl => "OpenGL",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Graphics settings that apply at the next launch. They live in their own
|
||||||
|
/// file because the backend is chosen before the application (and its
|
||||||
|
/// settings) exists.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct GraphicsPrefs {
|
||||||
|
pub backend: BackendChoice,
|
||||||
|
/// Force the packed renderer that avoids shader storage buffers.
|
||||||
|
pub compat_renderer: bool,
|
||||||
|
/// On Windows, prefer OpenGL when the GPU is below the WebGPU baseline.
|
||||||
|
pub legacy_gl: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GraphicsPrefs {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
backend: BackendChoice::Auto,
|
||||||
|
compat_renderer: false,
|
||||||
|
legacy_gl: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prefs_path() -> Option<PathBuf> {
|
||||||
|
if let Ok(dir) = std::env::var(SENTINEL_DIR_ENV) {
|
||||||
|
return Some(PathBuf::from(dir).join("graphics.json"));
|
||||||
|
}
|
||||||
|
crate::config::config_dir().map(|d| d.join("graphics.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saved graphics preferences; defaults when absent or unreadable.
|
||||||
|
pub fn load_prefs() -> GraphicsPrefs {
|
||||||
|
let Some(text) = prefs_path().and_then(|p| std::fs::read_to_string(p).ok()) else {
|
||||||
|
return GraphicsPrefs::default();
|
||||||
|
};
|
||||||
|
let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
|
||||||
|
return GraphicsPrefs::default();
|
||||||
|
};
|
||||||
|
let d = GraphicsPrefs::default();
|
||||||
|
GraphicsPrefs {
|
||||||
|
backend: v
|
||||||
|
.get("backend")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.map_or(d.backend, BackendChoice::parse),
|
||||||
|
compat_renderer: v
|
||||||
|
.get("compat_renderer")
|
||||||
|
.and_then(|x| x.as_bool())
|
||||||
|
.unwrap_or(d.compat_renderer),
|
||||||
|
legacy_gl: v
|
||||||
|
.get("legacy_gl")
|
||||||
|
.and_then(|x| x.as_bool())
|
||||||
|
.unwrap_or(d.legacy_gl),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persist graphics preferences (best effort).
|
||||||
|
pub fn save_prefs(prefs: GraphicsPrefs) {
|
||||||
|
let Some(p) = prefs_path() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(parent) = p.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"backend": prefs.backend.as_str().unwrap_or("auto"),
|
||||||
|
"compat_renderer": prefs.compat_renderer,
|
||||||
|
"legacy_gl": prefs.legacy_gl,
|
||||||
|
});
|
||||||
|
let _ = std::fs::write(p, serde_json::to_string_pretty(&json).unwrap_or_default());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What this run ended up using, and why.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct ActiveGpu {
|
||||||
|
/// `WGPU_BACKEND` value in effect (`None` = left to iced's default).
|
||||||
|
pub backend: Option<String>,
|
||||||
|
/// Adapter name when a probe identified it.
|
||||||
|
pub adapter: Option<String>,
|
||||||
|
/// Driver name / version when a probe reported it.
|
||||||
|
pub driver: String,
|
||||||
|
/// How the backend was chosen (`automatic`, `--backend`, ...).
|
||||||
|
pub origin: String,
|
||||||
|
/// The adapter is below the WebGPU baseline.
|
||||||
|
pub legacy: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static ACTIVE: std::sync::OnceLock<ActiveGpu> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
fn record_active(active: ActiveGpu) {
|
||||||
|
let _ = ACTIVE.set(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The backend decision for this run (default-empty before `resolve_gpu`).
|
||||||
|
pub fn active_gpu() -> ActiveGpu {
|
||||||
|
ACTIVE.get().cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Short `Adapter (backend)` text for the About dialog.
|
||||||
|
pub fn active_gpu_summary() -> String {
|
||||||
|
let a = active_gpu();
|
||||||
|
let backend = match a.backend.as_deref() {
|
||||||
|
Some("dx12") => "DirectX 12",
|
||||||
|
Some("vulkan") => "Vulkan",
|
||||||
|
Some("gl") => "OpenGL",
|
||||||
|
Some("metal") => "Metal",
|
||||||
|
Some(other) => other,
|
||||||
|
None => "default",
|
||||||
|
};
|
||||||
|
match a.adapter {
|
||||||
|
Some(name) => format!("{name} ({backend})"),
|
||||||
|
None => backend.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Capabilities reported by a passing probe child (one JSON line on stdout).
|
/// Capabilities reported by a passing probe child (one JSON line on stdout).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ProbeCaps {
|
pub struct ProbeCaps {
|
||||||
@@ -87,6 +255,12 @@ pub struct ProbeCaps {
|
|||||||
pub max_storage: u32,
|
pub max_storage: u32,
|
||||||
/// True when this is a software rasterizer (WARP / llvmpipe / SwiftShader).
|
/// True when this is a software rasterizer (WARP / llvmpipe / SwiftShader).
|
||||||
pub software: bool,
|
pub software: bool,
|
||||||
|
/// True when the adapter falls below the WebGPU baseline (missing
|
||||||
|
/// downlevel flags or shader model < 5): an older GPU that DX12/Vulkan
|
||||||
|
/// accept but run poorly or unreliably.
|
||||||
|
pub legacy: bool,
|
||||||
|
/// Driver name and version as the adapter reports them (may be empty).
|
||||||
|
pub driver: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProbeCaps {
|
impl ProbeCaps {
|
||||||
@@ -100,6 +274,8 @@ impl ProbeCaps {
|
|||||||
adapter: "synthetic-test-adapter".to_string(),
|
adapter: "synthetic-test-adapter".to_string(),
|
||||||
max_storage,
|
max_storage,
|
||||||
software: false,
|
software: false,
|
||||||
|
legacy: std::env::var_os(FORCE_LEGACY_ENV).is_some_and(|_| backend != "gl"),
|
||||||
|
driver: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +287,12 @@ impl ProbeCaps {
|
|||||||
adapter: v.get("adapter")?.as_str().unwrap_or("unknown").to_string(),
|
adapter: v.get("adapter")?.as_str().unwrap_or("unknown").to_string(),
|
||||||
max_storage: v.get("max_storage")?.as_u64()? as u32,
|
max_storage: v.get("max_storage")?.as_u64()? as u32,
|
||||||
software: v.get("software")?.as_bool()?,
|
software: v.get("software")?.as_bool()?,
|
||||||
|
legacy: v.get("legacy").and_then(|x| x.as_bool()).unwrap_or(false),
|
||||||
|
driver: v
|
||||||
|
.get("driver")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +302,8 @@ impl ProbeCaps {
|
|||||||
"adapter": self.adapter,
|
"adapter": self.adapter,
|
||||||
"max_storage": self.max_storage,
|
"max_storage": self.max_storage,
|
||||||
"software": self.software,
|
"software": self.software,
|
||||||
|
"legacy": self.legacy,
|
||||||
|
"driver": self.driver,
|
||||||
})
|
})
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
@@ -463,11 +647,41 @@ fn run_probe_for(arg: &str) -> Option<ProbeCaps> {
|
|||||||
|
|
||||||
/// Resolve and apply `WGPU_BACKEND` for this launch.
|
/// Resolve and apply `WGPU_BACKEND` for this launch.
|
||||||
///
|
///
|
||||||
/// Precedence: `--backend` > existing `WGPU_BACKEND` > `--safe-mode` >
|
/// Precedence: `--backend` > existing `WGPU_BACKEND` > `--safe-mode` > saved
|
||||||
|
/// Graphics option (skipped when that backend died with the previous run) >
|
||||||
/// per-backend probes (minus crash-implicated backends) > software adapter >
|
/// per-backend probes (minus crash-implicated backends) > software adapter >
|
||||||
/// last-resort `gl`. Inactive on macOS/wasm (env untouched).
|
/// last-resort `gl`. Inactive on macOS/wasm (env untouched).
|
||||||
pub fn resolve_gpu(cli_backend: Option<&str>, safe_mode: bool) -> GpuResolution {
|
pub fn resolve_gpu(cli_backend: Option<&str>, safe_mode: bool) -> GpuResolution {
|
||||||
|
let prefs = load_prefs();
|
||||||
let env_backend = std::env::var("WGPU_BACKEND").ok();
|
let env_backend = std::env::var("WGPU_BACKEND").ok();
|
||||||
|
let excluded = if previous_run_crashed() {
|
||||||
|
crashed_backends()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
// A saved backend choice acts like `--backend`, except when it is the
|
||||||
|
// backend that took down the previous run: the user must never be locked
|
||||||
|
// into a setting that cannot start.
|
||||||
|
let saved = prefs
|
||||||
|
.backend
|
||||||
|
.as_str()
|
||||||
|
.filter(|b| !excluded.iter().any(|e| e == b));
|
||||||
|
let origin = if cli_backend.is_some() {
|
||||||
|
"--backend"
|
||||||
|
} else if env_backend.is_some() {
|
||||||
|
"WGPU_BACKEND"
|
||||||
|
} else if safe_mode {
|
||||||
|
"safe mode"
|
||||||
|
} else if saved.is_some() {
|
||||||
|
"Graphics options"
|
||||||
|
} else {
|
||||||
|
"automatic"
|
||||||
|
};
|
||||||
|
let cli_backend = cli_backend.or(if env_backend.is_none() && !safe_mode {
|
||||||
|
saved
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
});
|
||||||
if cli_backend.is_some() || env_backend.is_some() || safe_mode || !gpu_guard_active() {
|
if cli_backend.is_some() || env_backend.is_some() || safe_mode || !gpu_guard_active() {
|
||||||
let d = decide_backend(
|
let d = decide_backend(
|
||||||
cli_backend,
|
cli_backend,
|
||||||
@@ -482,18 +696,21 @@ pub fn resolve_gpu(cli_backend: Option<&str>, safe_mode: bool) -> GpuResolution
|
|||||||
if let Some(v) = &d.backend_value {
|
if let Some(v) = &d.backend_value {
|
||||||
std::env::set_var("WGPU_BACKEND", v);
|
std::env::set_var("WGPU_BACKEND", v);
|
||||||
}
|
}
|
||||||
|
record_active(ActiveGpu {
|
||||||
|
backend: d.backend_value.clone(),
|
||||||
|
adapter: None,
|
||||||
|
driver: String::new(),
|
||||||
|
origin: origin.to_string(),
|
||||||
|
legacy: false,
|
||||||
|
});
|
||||||
return GpuResolution {
|
return GpuResolution {
|
||||||
backend_value: d.backend_value,
|
backend_value: d.backend_value,
|
||||||
reason: d.reason,
|
reason: d.reason,
|
||||||
compat_renderer: false,
|
compat_renderer: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let candidates = candidate_backends();
|
let mut candidates = candidate_backends();
|
||||||
let excluded = if previous_run_crashed() {
|
let mut origin = origin.to_string();
|
||||||
crashed_backends()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
let mut results: Vec<(String, Option<ProbeCaps>)> = Vec::new();
|
let mut results: Vec<(String, Option<ProbeCaps>)> = Vec::new();
|
||||||
for backend in &candidates {
|
for backend in &candidates {
|
||||||
if excluded.iter().any(|e| e == backend) {
|
if excluded.iter().any(|e| e == backend) {
|
||||||
@@ -511,6 +728,29 @@ pub fn resolve_gpu(cli_backend: Option<&str>, safe_mode: bool) -> GpuResolution
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Older GPUs that DX12/Vulkan accept but run poorly: when the winning
|
||||||
|
// probe says the adapter is below the WebGPU baseline, try GL first. GL
|
||||||
|
// only wins if its own probe passes, so this never trades a working
|
||||||
|
// backend for a broken one.
|
||||||
|
if cfg!(target_os = "windows")
|
||||||
|
&& prefs.legacy_gl
|
||||||
|
&& !excluded.iter().any(|e| e == "gl")
|
||||||
|
&& results
|
||||||
|
.iter()
|
||||||
|
.find_map(|(_, c)| c.as_ref())
|
||||||
|
.is_some_and(|c| c.legacy && !c.software && c.backend != "gl")
|
||||||
|
{
|
||||||
|
let gl = run_probe_for("gl");
|
||||||
|
eprintln!(
|
||||||
|
"[gpu] older GPU detected; probe gl: {}",
|
||||||
|
gl.as_ref().map(|c| c.adapter.as_str()).unwrap_or("FAILED")
|
||||||
|
);
|
||||||
|
if gl.is_some() {
|
||||||
|
candidates = prefer_gl(&candidates);
|
||||||
|
origin = "automatic (older GPU, OpenGL preferred)".to_string();
|
||||||
|
}
|
||||||
|
results.push(("gl".to_string(), gl));
|
||||||
|
}
|
||||||
let sw = if results.iter().all(|(_, c)| c.is_none()) {
|
let sw = if results.iter().all(|(_, c)| c.is_none()) {
|
||||||
run_probe_for("sw")
|
run_probe_for("sw")
|
||||||
} else {
|
} else {
|
||||||
@@ -538,6 +778,21 @@ pub fn resolve_gpu(cli_backend: Option<&str>, safe_mode: bool) -> GpuResolution
|
|||||||
eprintln!("[gpu] {}", compat_notice());
|
eprintln!("[gpu] {}", compat_notice());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let chosen = results
|
||||||
|
.iter()
|
||||||
|
.filter(|(b, _)| Some(b) == d.backend_value.as_ref())
|
||||||
|
.find_map(|(_, c)| c.clone());
|
||||||
|
record_active(ActiveGpu {
|
||||||
|
backend: d.backend_value.clone(),
|
||||||
|
adapter: d.adapter_name.clone(),
|
||||||
|
driver: chosen.as_ref().map(|c| c.driver.clone()).unwrap_or_default(),
|
||||||
|
origin: if d.reason.is_some() && origin == "automatic" {
|
||||||
|
"automatic (fallback)".to_string()
|
||||||
|
} else {
|
||||||
|
origin
|
||||||
|
},
|
||||||
|
legacy: chosen.is_some_and(|c| c.legacy),
|
||||||
|
});
|
||||||
GpuResolution {
|
GpuResolution {
|
||||||
backend_value: d.backend_value,
|
backend_value: d.backend_value,
|
||||||
reason: d.reason,
|
reason: d.reason,
|
||||||
@@ -545,6 +800,13 @@ pub fn resolve_gpu(cli_backend: Option<&str>, safe_mode: bool) -> GpuResolution
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `candidates` with `gl` moved to the front.
|
||||||
|
pub fn prefer_gl(candidates: &[String]) -> Vec<String> {
|
||||||
|
let mut out = vec!["gl".to_string()];
|
||||||
|
out.extend(candidates.iter().filter(|b| *b != "gl").cloned());
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Child-process entry point for `--gpu-probe <backend|sw>`: exercise one
|
/// Child-process entry point for `--gpu-probe <backend|sw>`: exercise one
|
||||||
/// backend the way the canvas does (device + staging-style mapped upload +
|
/// backend the way the canvas does (device + staging-style mapped upload +
|
||||||
/// render pipeline creation + offscreen render + readback). Prints one JSON
|
/// render pipeline creation + offscreen render + readback). Prints one JSON
|
||||||
@@ -718,6 +980,8 @@ fn exercise_adapter(adapter: &iced::wgpu::Adapter) -> Option<ProbeCaps> {
|
|||||||
adapter: info.name.clone(),
|
adapter: info.name.clone(),
|
||||||
max_storage: adapter.limits().max_storage_buffers_per_shader_stage,
|
max_storage: adapter.limits().max_storage_buffers_per_shader_stage,
|
||||||
software: is_software_adapter(&info),
|
software: is_software_adapter(&info),
|
||||||
|
legacy: !adapter.get_downlevel_capabilities().is_webgpu_compliant(),
|
||||||
|
driver: format!("{} {}", info.driver, info.driver_info).trim().to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -902,6 +1166,8 @@ mod tests {
|
|||||||
adapter: format!("test {backend}"),
|
adapter: format!("test {backend}"),
|
||||||
max_storage: storage,
|
max_storage: storage,
|
||||||
software: false,
|
software: false,
|
||||||
|
legacy: false,
|
||||||
|
driver: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,6 +1279,8 @@ mod tests {
|
|||||||
adapter: "llvmpipe".to_string(),
|
adapter: "llvmpipe".to_string(),
|
||||||
max_storage: 8,
|
max_storage: 8,
|
||||||
software: true,
|
software: true,
|
||||||
|
legacy: false,
|
||||||
|
driver: String::new(),
|
||||||
};
|
};
|
||||||
let d = decide_backend(
|
let d = decide_backend(
|
||||||
None,
|
None,
|
||||||
@@ -1060,6 +1328,8 @@ mod tests {
|
|||||||
adapter: "Intel HD 405 (Braswell)".to_string(),
|
adapter: "Intel HD 405 (Braswell)".to_string(),
|
||||||
max_storage: 8,
|
max_storage: 8,
|
||||||
software: false,
|
software: false,
|
||||||
|
legacy: false,
|
||||||
|
driver: String::new(),
|
||||||
};
|
};
|
||||||
let line = caps.to_json_line();
|
let line = caps.to_json_line();
|
||||||
assert_eq!(ProbeCaps::from_json_line(&line), Some(caps));
|
assert_eq!(ProbeCaps::from_json_line(&line), Some(caps));
|
||||||
@@ -1214,4 +1484,74 @@ mod tests {
|
|||||||
unsafe { std::env::remove_var(FORCE_OK_ENV) };
|
unsafe { std::env::remove_var(FORCE_OK_ENV) };
|
||||||
unsafe { std::env::remove_var(FORCE_CAPS_ENV) };
|
unsafe { std::env::remove_var(FORCE_CAPS_ENV) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_gpu_routes_to_gl_first_but_gl_must_still_pass() {
|
||||||
|
let order = prefer_gl(&cands(&["dx12", "vulkan", "gl"]));
|
||||||
|
assert_eq!(order, cands(&["gl", "dx12", "vulkan"]));
|
||||||
|
// GL probe passed: it wins silently.
|
||||||
|
let results = vec![
|
||||||
|
("dx12".to_string(), Some(ProbeCaps { legacy: true, ..ok_caps("dx12", 8) })),
|
||||||
|
("gl".to_string(), Some(ok_caps("gl", 8))),
|
||||||
|
];
|
||||||
|
let d = decide_backend(None, None, false, &order, &[], &results, None, true);
|
||||||
|
assert_eq!(d.backend_value.as_deref(), Some("gl"));
|
||||||
|
assert!(d.reason.is_none());
|
||||||
|
// GL probe failed: the working DX12 stays.
|
||||||
|
let results = vec![
|
||||||
|
("dx12".to_string(), Some(ProbeCaps { legacy: true, ..ok_caps("dx12", 8) })),
|
||||||
|
("gl".to_string(), None),
|
||||||
|
];
|
||||||
|
let d = decide_backend(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
&cands(&["dx12", "vulkan", "gl"]),
|
||||||
|
&[],
|
||||||
|
&results,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert_eq!(d.backend_value.as_deref(), Some("dx12"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn probe_json_without_new_fields_still_parses() {
|
||||||
|
let line = r#"{"backend":"dx12","adapter":"x","max_storage":8,"software":false}"#;
|
||||||
|
let caps = ProbeCaps::from_json_line(line).expect("old probe output parses");
|
||||||
|
assert!(!caps.legacy);
|
||||||
|
assert!(caps.driver.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backend_choice_round_trips() {
|
||||||
|
for c in [
|
||||||
|
BackendChoice::Auto,
|
||||||
|
BackendChoice::Dx12,
|
||||||
|
BackendChoice::Vulkan,
|
||||||
|
BackendChoice::Gl,
|
||||||
|
] {
|
||||||
|
assert_eq!(BackendChoice::parse(c.as_str().unwrap_or("auto")), c);
|
||||||
|
}
|
||||||
|
assert_eq!(BackendChoice::parse("garbage"), BackendChoice::Auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graphics_prefs_persist_and_default_when_missing() {
|
||||||
|
let _guard = SENTINEL_TESTS.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let dir = std::env::temp_dir().join(format!("ocs-gpu-prefs-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
// SAFETY: serialized by SENTINEL_TESTS; process-local test hook.
|
||||||
|
unsafe { std::env::set_var(SENTINEL_DIR_ENV, &dir) };
|
||||||
|
assert_eq!(load_prefs(), GraphicsPrefs::default());
|
||||||
|
let p = GraphicsPrefs {
|
||||||
|
backend: BackendChoice::Vulkan,
|
||||||
|
compat_renderer: true,
|
||||||
|
legacy_gl: false,
|
||||||
|
};
|
||||||
|
save_prefs(p);
|
||||||
|
assert_eq!(load_prefs(), p);
|
||||||
|
unsafe { std::env::remove_var(SENTINEL_DIR_ENV) };
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -29,5 +29,6 @@ pub mod rest;
|
|||||||
pub mod scene;
|
pub mod scene;
|
||||||
pub mod snap;
|
pub mod snap;
|
||||||
pub mod sys;
|
pub mod sys;
|
||||||
|
pub mod sysinfo;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod videos;
|
pub mod videos;
|
||||||
+3
-2
@@ -194,8 +194,9 @@ fn main() -> iced::Result {
|
|||||||
// The probe enables the packed renderer automatically on GPUs without
|
// The probe enables the packed renderer automatically on GPUs without
|
||||||
// shader storage buffers; an explicit `--compat-renderer` does the
|
// shader storage buffers; an explicit `--compat-renderer` does the
|
||||||
// same without the notice.
|
// same without the notice.
|
||||||
let compat_renderer = args.compat_renderer || gpu.compat_renderer;
|
let saved_compat = OpenCADStudio::gpu_backend::load_prefs().compat_renderer;
|
||||||
let gpu_compat_auto = !args.compat_renderer && gpu.compat_renderer;
|
let compat_renderer = args.compat_renderer || saved_compat || gpu.compat_renderer;
|
||||||
|
let gpu_compat_auto = !args.compat_renderer && !saved_compat && gpu.compat_renderer;
|
||||||
let _ = cli::GUI_CONFIG.set(cli::GuiConfig {
|
let _ = cli::GUI_CONFIG.set(cli::GuiConfig {
|
||||||
files: if args.new { Vec::new() } else { args.files },
|
files: if args.new { Vec::new() } else { args.files },
|
||||||
new: args.new,
|
new: args.new,
|
||||||
|
|||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
//! Host hardware facts for About and the copied system report.
|
||||||
|
//!
|
||||||
|
//! Read straight from the OS (registry, `/proc`, `sysctl`) so no extra crate
|
||||||
|
//! is pulled in for three strings. Everything degrades to "Unknown" rather
|
||||||
|
//! than failing: this is diagnostics, never a reason to refuse to open.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct SystemInfo {
|
||||||
|
/// Marketing name, e.g. `Intel(R) Core(TM) Ultra 7 255U`.
|
||||||
|
pub cpu: String,
|
||||||
|
/// Logical processors available to this process.
|
||||||
|
pub logical_cores: usize,
|
||||||
|
/// Installed physical memory, bytes (0 = unknown).
|
||||||
|
pub ram_total: u64,
|
||||||
|
/// Memory available right now, bytes (0 = unknown).
|
||||||
|
pub ram_available: u64,
|
||||||
|
/// OS name / version / build.
|
||||||
|
pub os: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
static INFO: OnceLock<SystemInfo> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Host facts, read once per run.
|
||||||
|
pub fn system_info() -> &'static SystemInfo {
|
||||||
|
INFO.get_or_init(read)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `16.0 GB`, or `Unknown` for 0.
|
||||||
|
pub fn format_bytes(bytes: u64) -> String {
|
||||||
|
if bytes == 0 {
|
||||||
|
return "Unknown".to_string();
|
||||||
|
}
|
||||||
|
format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-line CPU description for the About row.
|
||||||
|
pub fn cpu_summary() -> String {
|
||||||
|
let i = system_info();
|
||||||
|
if i.logical_cores > 0 {
|
||||||
|
format!("{} ({} threads)", i.cpu, i.logical_cores)
|
||||||
|
} else {
|
||||||
|
i.cpu.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-line RAM description for the About row.
|
||||||
|
pub fn ram_summary() -> String {
|
||||||
|
format_bytes(system_info().ram_total)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn unknown_if_empty(s: String) -> String {
|
||||||
|
let s = s.trim().to_string();
|
||||||
|
if s.is_empty() {
|
||||||
|
"Unknown".to_string()
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn read() -> SystemInfo {
|
||||||
|
SystemInfo {
|
||||||
|
cpu: "Unknown".into(),
|
||||||
|
logical_cores: 0,
|
||||||
|
ram_total: 0,
|
||||||
|
ram_available: 0,
|
||||||
|
os: "Web".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn read() -> SystemInfo {
|
||||||
|
let (cpu, ram_total, ram_available, os) = platform();
|
||||||
|
SystemInfo {
|
||||||
|
cpu: unknown_if_empty(cpu),
|
||||||
|
logical_cores: std::thread::available_parallelism().map_or(0, |n| n.get()),
|
||||||
|
ram_total,
|
||||||
|
ram_available,
|
||||||
|
os: unknown_if_empty(os),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn platform() -> (String, u64, u64, String) {
|
||||||
|
use windows_sys::Win32::System::Registry::{
|
||||||
|
RegGetValueW, HKEY_LOCAL_MACHINE, RRF_RT_REG_SZ,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX};
|
||||||
|
|
||||||
|
fn wide(s: &str) -> Vec<u16> {
|
||||||
|
s.encode_utf16().chain(std::iter::once(0)).collect()
|
||||||
|
}
|
||||||
|
fn reg_string(subkey: &str, value: &str) -> String {
|
||||||
|
let (subkey, value) = (wide(subkey), wide(value));
|
||||||
|
let mut buf = [0u16; 512];
|
||||||
|
let mut bytes = (buf.len() * 2) as u32;
|
||||||
|
// SAFETY: both names are NUL-terminated and `buf` / `bytes` describe
|
||||||
|
// the writable buffer exactly.
|
||||||
|
let status = unsafe {
|
||||||
|
RegGetValueW(
|
||||||
|
HKEY_LOCAL_MACHINE,
|
||||||
|
subkey.as_ptr(),
|
||||||
|
value.as_ptr(),
|
||||||
|
RRF_RT_REG_SZ,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
buf.as_mut_ptr().cast(),
|
||||||
|
&mut bytes,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if status != 0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||||
|
String::from_utf16_lossy(&buf[..len])
|
||||||
|
}
|
||||||
|
|
||||||
|
let cpu = reg_string(
|
||||||
|
r"HARDWARE\DESCRIPTION\System\CentralProcessor\0",
|
||||||
|
"ProcessorNameString",
|
||||||
|
);
|
||||||
|
|
||||||
|
// SAFETY: `MEMORYSTATUSEX` is plain data; `dwLength` is set as required.
|
||||||
|
let (total, avail) = unsafe {
|
||||||
|
let mut m: MEMORYSTATUSEX = std::mem::zeroed();
|
||||||
|
m.dwLength = std::mem::size_of::<MEMORYSTATUSEX>() as u32;
|
||||||
|
if GlobalMemoryStatusEx(&mut m) != 0 {
|
||||||
|
(m.ullTotalPhys, m.ullAvailPhys)
|
||||||
|
} else {
|
||||||
|
(0, 0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion";
|
||||||
|
let mut name = reg_string(key, "ProductName");
|
||||||
|
let build = reg_string(key, "CurrentBuildNumber");
|
||||||
|
let display = reg_string(key, "DisplayVersion");
|
||||||
|
// The registry still says "Windows 10" on Windows 11; the build number
|
||||||
|
// (22000+) is the reliable tell.
|
||||||
|
if build.parse::<u32>().is_ok_and(|b| b >= 22000) {
|
||||||
|
name = name.replace("Windows 10", "Windows 11");
|
||||||
|
}
|
||||||
|
let mut os = name;
|
||||||
|
if !display.is_empty() {
|
||||||
|
os.push(' ');
|
||||||
|
os.push_str(&display);
|
||||||
|
}
|
||||||
|
if !build.is_empty() {
|
||||||
|
os.push_str(&format!(" (build {build})"));
|
||||||
|
}
|
||||||
|
(cpu, total, avail, os)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn platform() -> (String, u64, u64, String) {
|
||||||
|
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").unwrap_or_default();
|
||||||
|
let cpu = cpuinfo
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.strip_prefix("model name"))
|
||||||
|
.and_then(|l| l.split_once(':'))
|
||||||
|
.map(|(_, v)| v.trim().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let meminfo = std::fs::read_to_string("/proc/meminfo").unwrap_or_default();
|
||||||
|
let kb = |key: &str| -> u64 {
|
||||||
|
meminfo
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.strip_prefix(key))
|
||||||
|
.and_then(|l| l.trim_start_matches(':').split_whitespace().next())
|
||||||
|
.and_then(|n| n.parse::<u64>().ok())
|
||||||
|
.unwrap_or(0)
|
||||||
|
* 1024
|
||||||
|
};
|
||||||
|
let os_release = std::fs::read_to_string("/etc/os-release").unwrap_or_default();
|
||||||
|
let os = os_release
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.strip_prefix("PRETTY_NAME="))
|
||||||
|
.map(|v| v.trim_matches('"').to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
(cpu, kb("MemTotal"), kb("MemAvailable"), os)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn platform() -> (String, u64, u64, String) {
|
||||||
|
fn run(cmd: &str, args: &[&str]) -> String {
|
||||||
|
std::process::Command::new(cmd)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
let cpu = run("sysctl", &["-n", "machdep.cpu.brand_string"]);
|
||||||
|
let total = run("sysctl", &["-n", "hw.memsize"]).parse().unwrap_or(0);
|
||||||
|
let os = format!("macOS {}", run("sw_vers", &["-productVersion"]));
|
||||||
|
(cpu, total, 0, os)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
not(target_arch = "wasm32"),
|
||||||
|
not(any(target_os = "windows", target_os = "linux", target_os = "macos"))
|
||||||
|
))]
|
||||||
|
fn platform() -> (String, u64, u64, String) {
|
||||||
|
(String::new(), 0, 0, String::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_bytes_and_unknown() {
|
||||||
|
assert_eq!(format_bytes(0), "Unknown");
|
||||||
|
assert_eq!(format_bytes(16 * 1024 * 1024 * 1024), "16.0 GB");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_facts_are_never_empty() {
|
||||||
|
let i = system_info();
|
||||||
|
assert!(!i.cpu.is_empty());
|
||||||
|
assert!(!i.os.is_empty());
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||||
|
assert!(i.ram_total > 0, "installed RAM should be readable");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,39 @@ fn info_card<'a>(
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A full-width label/value line. CPU and GPU names are too long for the
|
||||||
|
/// fixed-width cards above.
|
||||||
|
fn info_row<'a>(
|
||||||
|
label: std::borrow::Cow<'static, str>,
|
||||||
|
value: impl iced::widget::text::IntoFragment<'a>,
|
||||||
|
width: Length,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
container(
|
||||||
|
row![
|
||||||
|
text(label).size(11).style(muted_style).width(Length::Fixed(110.0)),
|
||||||
|
text(value).size(13).width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.padding([8, 12])
|
||||||
|
.width(width)
|
||||||
|
.style(surface_style)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Adapter (backend)` for the GPU row.
|
||||||
|
fn gpu_summary() -> String {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
"WebGPU / WebGL".to_string()
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
crate::gpu_backend::active_gpu_summary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn platform_name() -> &'static str {
|
pub(crate) fn platform_name() -> &'static str {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
@@ -69,6 +102,59 @@ pub(crate) fn architecture_name() -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The text `Copy Info` puts on the clipboard: everything a bug report needs
|
||||||
|
/// about the build and the machine, including which graphics backend the
|
||||||
|
/// resolver picked and why.
|
||||||
|
pub(crate) fn report() -> String {
|
||||||
|
use crate::sysinfo::{format_bytes, system_info};
|
||||||
|
let sys = system_info();
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut out = format!(
|
||||||
|
"Open CAD Studio v{}\nRevision: {}\nCommit date: {}\nProfile: {}\nFeatures: {}\n\n\
|
||||||
|
[System]\nOS: {} ({} {})\nCPU: {}\nLogical processors: {}\nRAM: {} total, {} available\n",
|
||||||
|
env!("OCS_FULL_VERSION"),
|
||||||
|
env!("OCS_GIT_REV"),
|
||||||
|
env!("OCS_COMMIT_DATE"),
|
||||||
|
env!("OCS_BUILD_PROFILE"),
|
||||||
|
env!("OCS_BUILD_FEATURES"),
|
||||||
|
sys.os,
|
||||||
|
platform_name(),
|
||||||
|
architecture_name(),
|
||||||
|
sys.cpu,
|
||||||
|
sys.logical_cores,
|
||||||
|
format_bytes(sys.ram_total),
|
||||||
|
format_bytes(sys.ram_available),
|
||||||
|
);
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
use crate::gpu_backend::{active_gpu, load_prefs};
|
||||||
|
let gpu = active_gpu();
|
||||||
|
let prefs = load_prefs();
|
||||||
|
let or_unknown = |s: &str| {
|
||||||
|
if s.is_empty() {
|
||||||
|
"Unknown".to_string()
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
out.push_str(&format!(
|
||||||
|
"\n[Graphics]\nGPU: {}\nDriver: {}\nBackend: {}\nSelected by: {}\n\
|
||||||
|
Below WebGPU baseline: {}\nWGPU_BACKEND: {}\n\
|
||||||
|
Saved options: backend={}, compat renderer={}, OpenGL on older GPUs={}\n",
|
||||||
|
or_unknown(gpu.adapter.as_deref().unwrap_or("")),
|
||||||
|
or_unknown(&gpu.driver),
|
||||||
|
or_unknown(gpu.backend.as_deref().unwrap_or("")),
|
||||||
|
or_unknown(&gpu.origin),
|
||||||
|
if gpu.legacy { "yes" } else { "no" },
|
||||||
|
std::env::var("WGPU_BACKEND").unwrap_or_else(|_| "(unset)".into()),
|
||||||
|
prefs.backend.as_str().unwrap_or("auto"),
|
||||||
|
prefs.compat_renderer,
|
||||||
|
prefs.legacy_gl,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Build card text: the metadata suffix without its leading `+`
|
/// Build card text: the metadata suffix without its leading `+`
|
||||||
/// (`194.gef189d77`, `gef189d77.dirty`), or "Release" for a clean build on
|
/// (`194.gef189d77`, `gef189d77.dirty`), or "Release" for a clean build on
|
||||||
/// the release tag.
|
/// the release tag.
|
||||||
@@ -137,6 +223,19 @@ pub fn view_window(
|
|||||||
.spacing(8)
|
.spacing(8)
|
||||||
.width(content_width);
|
.width(content_width);
|
||||||
|
|
||||||
|
let row_width = if matches!(sizing.width, Length::Fill) {
|
||||||
|
Fill
|
||||||
|
} else {
|
||||||
|
Length::Fixed(148.0 * 3.0 + 16.0)
|
||||||
|
};
|
||||||
|
let system = column![
|
||||||
|
info_row(t!("CPU"), crate::sysinfo::cpu_summary(), row_width),
|
||||||
|
info_row(t!("RAM"), crate::sysinfo::ram_summary(), row_width),
|
||||||
|
info_row(t!("GPU (backend)"), gpu_summary(), row_width),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.width(content_width);
|
||||||
|
|
||||||
let copy = dialog_button(t!("Copy Info"), Message::AboutCopyInfo, true);
|
let copy = dialog_button(t!("Copy Info"), Message::AboutCopyInfo, true);
|
||||||
|
|
||||||
container(
|
container(
|
||||||
@@ -144,6 +243,7 @@ pub fn view_window(
|
|||||||
hero,
|
hero,
|
||||||
metadata,
|
metadata,
|
||||||
build_info,
|
build_info,
|
||||||
|
system,
|
||||||
row![Space::new().width(content_width), copy]
|
row![Space::new().width(content_width), copy]
|
||||||
.width(sizing.width)
|
.width(sizing.width)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub(crate) mod graphics;
|
||||||
pub(crate) mod spacemouse;
|
pub(crate) mod spacemouse;
|
||||||
use crate::app::config::UiThemeConfig;
|
use crate::app::config::UiThemeConfig;
|
||||||
use crate::app::settings;
|
use crate::app::settings;
|
||||||
@@ -35,6 +37,7 @@ pub enum OptionsTab {
|
|||||||
Modeling,
|
Modeling,
|
||||||
Selection,
|
Selection,
|
||||||
UserPreferences,
|
UserPreferences,
|
||||||
|
Graphics,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Application preferences the dialog reads that are plain scalars on the app.
|
/// Application preferences the dialog reads that are plain scalars on the app.
|
||||||
@@ -201,6 +204,7 @@ pub fn view_window<'a>(
|
|||||||
selection: SelectionPrefs,
|
selection: SelectionPrefs,
|
||||||
prefs: AppPrefs,
|
prefs: AppPrefs,
|
||||||
spacemouse: Element<'a, Message>,
|
spacemouse: Element<'a, Message>,
|
||||||
|
graphics: Element<'a, Message>,
|
||||||
snap_angle_input: &'a str,
|
snap_angle_input: &'a str,
|
||||||
zoom_factor_input: &'a str,
|
zoom_factor_input: &'a str,
|
||||||
drawing_prefs: DrawingPrefs,
|
drawing_prefs: DrawingPrefs,
|
||||||
@@ -1675,6 +1679,7 @@ pub fn view_window<'a>(
|
|||||||
OptionsTab::Drafting => drafting.into(),
|
OptionsTab::Drafting => drafting.into(),
|
||||||
OptionsTab::Modeling => modeling.into(),
|
OptionsTab::Modeling => modeling.into(),
|
||||||
OptionsTab::UserPreferences => user_prefs.into(),
|
OptionsTab::UserPreferences => user_prefs.into(),
|
||||||
|
OptionsTab::Graphics => graphics,
|
||||||
};
|
};
|
||||||
|
|
||||||
// A vertical rail rather than a horizontal strip: the tab names are
|
// A vertical rail rather than a horizontal strip: the tab names are
|
||||||
@@ -1697,7 +1702,11 @@ pub fn view_window<'a>(
|
|||||||
tab_button(crate::t!("3D Modeling"), OptionsTab::Modeling),
|
tab_button(crate::t!("3D Modeling"), OptionsTab::Modeling),
|
||||||
tab_button(crate::t!("Selection"), OptionsTab::Selection),
|
tab_button(crate::t!("Selection"), OptionsTab::Selection),
|
||||||
tab_button(crate::t!("User Preferences"), OptionsTab::UserPreferences),
|
tab_button(crate::t!("User Preferences"), OptionsTab::UserPreferences),
|
||||||
]
|
];
|
||||||
|
// The web build has no backend to choose, so it has no Graphics page.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let tabs = tabs.push(tab_button(crate::t!("Graphics"), OptionsTab::Graphics));
|
||||||
|
let tabs = tabs
|
||||||
.spacing(2)
|
.spacing(2)
|
||||||
.width(iced::Length::Fixed(TAB_RAIL_WIDTH));
|
.width(iced::Length::Fixed(TAB_RAIL_WIDTH));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
//! Options > Graphics: which graphics backend the next launch uses.
|
||||||
|
//!
|
||||||
|
//! Everything here is read before the application boots (the backend is
|
||||||
|
//! chosen first), so changes are saved at once and apply on restart rather
|
||||||
|
//! than going through OK / Apply.
|
||||||
|
|
||||||
|
use crate::app::Message;
|
||||||
|
use crate::gpu_backend::{ActiveGpu, BackendChoice, GraphicsPrefs};
|
||||||
|
use iced::widget::{checkbox, column, pick_list, row, text, Space};
|
||||||
|
use iced::{Element, Fill};
|
||||||
|
|
||||||
|
fn current_row(label: std::borrow::Cow<'static, str>, value: String) -> Element<'static, Message> {
|
||||||
|
row![
|
||||||
|
text(label).size(12).width(150),
|
||||||
|
text(value).size(12).width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unknown(s: &str) -> String {
|
||||||
|
if s.is_empty() {
|
||||||
|
crate::t!("Unknown").into_owned()
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn view(prefs: GraphicsPrefs, active: ActiveGpu) -> Element<'static, Message> {
|
||||||
|
let backend = pick_list(
|
||||||
|
Some(prefs.backend),
|
||||||
|
BackendChoice::available(),
|
||||||
|
|choice: &BackendChoice| crate::t!(choice.label()).into_owned(),
|
||||||
|
)
|
||||||
|
.on_select(Message::GraphicsBackendChanged)
|
||||||
|
.width(260);
|
||||||
|
|
||||||
|
column![
|
||||||
|
text(crate::t!("Graphics")).size(15),
|
||||||
|
Space::new().height(6),
|
||||||
|
text(crate::t!(
|
||||||
|
"Saved immediately. Changes take effect the next time Open CAD Studio starts."
|
||||||
|
))
|
||||||
|
.size(11)
|
||||||
|
.width(Fill),
|
||||||
|
Space::new().height(16),
|
||||||
|
text(crate::t!("This session")).size(13),
|
||||||
|
Space::new().height(8),
|
||||||
|
current_row(
|
||||||
|
crate::t!("GPU"),
|
||||||
|
unknown(active.adapter.as_deref().unwrap_or(""))
|
||||||
|
),
|
||||||
|
Space::new().height(4),
|
||||||
|
current_row(crate::t!("Driver"), unknown(&active.driver)),
|
||||||
|
Space::new().height(4),
|
||||||
|
current_row(
|
||||||
|
crate::t!("Backend"),
|
||||||
|
unknown(active.backend.as_deref().unwrap_or(""))
|
||||||
|
),
|
||||||
|
Space::new().height(4),
|
||||||
|
current_row(crate::t!("Selected by"), unknown(&active.origin)),
|
||||||
|
Space::new().height(20),
|
||||||
|
text(crate::t!("Next launch")).size(13),
|
||||||
|
Space::new().height(8),
|
||||||
|
row![text(crate::t!("Graphics backend")).size(12).width(150), backend]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(12),
|
||||||
|
row![
|
||||||
|
checkbox(prefs.compat_renderer)
|
||||||
|
.on_toggle(Message::GraphicsCompatToggled)
|
||||||
|
.size(15),
|
||||||
|
text(crate::t!(
|
||||||
|
"Use the compatibility renderer (for GPUs without shader storage buffers)"
|
||||||
|
))
|
||||||
|
.size(12),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(10),
|
||||||
|
row![
|
||||||
|
checkbox(prefs.legacy_gl)
|
||||||
|
.on_toggle(Message::GraphicsLegacyGlToggled)
|
||||||
|
.size(15),
|
||||||
|
text(crate::t!(
|
||||||
|
"Prefer OpenGL on older GPUs (Windows, automatic backend only)"
|
||||||
|
))
|
||||||
|
.size(12),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
Space::new().height(16),
|
||||||
|
text(crate::t!(
|
||||||
|
"If a pinned backend fails to start, the next launch falls back to automatic selection. \
|
||||||
|
The --backend option and the WGPU_BACKEND variable override this page."
|
||||||
|
))
|
||||||
|
.size(11)
|
||||||
|
.width(Fill),
|
||||||
|
]
|
||||||
|
.spacing(0)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
Reference in new issue
Block a user