feat(graphics): smart GPU preference with boot watchdog and crash recovery
Solves the long-running performance complaints (especially Optimus
laptops where the dGPU silently hijacks WebKit). The end-to-end shape:
- graphics_detection enumerates physical GPUs from /sys/class/drm and
cross-references EGL vendor JSONs under /usr/share/glvnd/egl_vendor.d
to expose only stacks the running process can actually use. Works
inside Flatpak/Snap because /sys is still mounted read-only and the
sandbox-visible EGL vendor JSONs are the right thing to read.
- preferred_gpu column in graphics_settings.db (string: auto, integrated,
discrete, software, or a specific PCI slot id). Migrated with
ALTER TABLE ... DEFAULT 'auto'.
- env_vars_for_preferred_gpu returns Set/Unset actions applied at startup
before WebKit init. Integrated/AMD path explicitly unsets
__NV_PRIME_RENDER_OFFLOAD so KDE/Optimus sessions that pre-set it to 1
no longer override the EGL pin and yank rendering back onto NVIDIA.
- Auto resolver: on hybrid systems (iGPU + dGPU both usable), Auto pins
to the iGPU. Single-GPU and no-GPU systems remain no-op. The dropdown
surfaces what Auto resolved to ("Auto (Intel Graphics)") so the user
sees the decision without picking explicitly.
- boot_watchdog writes a pending.json before WebKit init listing the
risky knobs in effect. Frontend calls v2_mark_boot_succeeded after
first paint to clear it and promote to last-good.json. If the next
launch sees pending without a success ack, it blames matching fields,
reverts them in-DB, and increments per-knob failure counters; two
consecutive failures lock the knob until the user clears it.
- Settings > Graphics gets a Rendering GPU dropdown (between HW accel
and DMA-BUF) and a crash-recovery banner that lets the user re-enable
any auto-disabled knob. Restart-required affordance shared with the
other risky toggles.
- i18n keys for the dropdown, banner copy, and autoWithTarget label
across all five locales.
cc
This commit is contained in:
14 files changed
+1391
-5
No files matched your search
@@ -0,0 +1,329 @@
|
||||
//! Boot watchdog for graphics settings that can crash WebKit before it
|
||||
//! paints a single frame (HW acceleration, DMA-BUF, preferred-GPU
|
||||
//! selection that picks an unreachable stack).
|
||||
//!
|
||||
//! Flow:
|
||||
//! 1. `before_webkit_init()` is called at startup with the risky
|
||||
//! settings of the current boot. It writes a `pending.json` marker
|
||||
//! to `<data_dir>/qbz/boot_state/` recording which settings were
|
||||
//! attempted.
|
||||
//! 2. After WebKit boots and the frontend signals first-paint (via
|
||||
//! `v2_mark_boot_succeeded`), `mark_boot_succeeded()` removes the
|
||||
//! pending marker and writes `last-good.json` carrying the
|
||||
//! successful settings.
|
||||
//! 3. On the NEXT boot, `before_webkit_init` first calls
|
||||
//! `check_previous_boot()`. If a pending marker exists without a
|
||||
//! matching success, the previous boot crashed during graphics
|
||||
//! init — we auto-revert the offending setting(s), record a
|
||||
//! crash recovery flag, and only THEN write the new pending
|
||||
//! marker for the current boot.
|
||||
//!
|
||||
//! Two consecutive crashes with the same setting promote to "lockout"
|
||||
//! state: the setting stays disabled until the user explicitly clears
|
||||
//! the flag from Settings. Prevents an infinite retry loop.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const STATE_DIR: &str = "boot_state";
|
||||
const PENDING_FILE: &str = "pending.json";
|
||||
const LAST_GOOD_FILE: &str = "last-good.json";
|
||||
const CRASH_FLAGS_FILE: &str = "crash-flags.json";
|
||||
|
||||
/// Snapshot of the risky settings attempted in a boot. Compared against
|
||||
/// the previous boot's snapshot to decide which setting "caused" a crash.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct BootAttempt {
|
||||
pub hardware_acceleration: bool,
|
||||
pub force_dmabuf: bool,
|
||||
pub preferred_gpu: String,
|
||||
pub force_x11: bool,
|
||||
}
|
||||
|
||||
/// Sticky flags written when a setting was auto-reverted after a crash.
|
||||
/// The UI reads these to render the recovery banner. Frontend can also
|
||||
/// clear them via `v2_clear_crash_recovery_flag`.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct CrashFlags {
|
||||
/// Last boot crashed with hardware_acceleration ON; we reverted it.
|
||||
pub hardware_acceleration_disabled: bool,
|
||||
pub hardware_acceleration_consecutive_failures: u8,
|
||||
/// Same for force_dmabuf.
|
||||
pub force_dmabuf_disabled: bool,
|
||||
pub force_dmabuf_consecutive_failures: u8,
|
||||
/// Same for preferred_gpu (reset to "auto").
|
||||
pub preferred_gpu_disabled: bool,
|
||||
pub preferred_gpu_consecutive_failures: u8,
|
||||
/// Lockout — set when consecutive_failures >= 2 for a given knob.
|
||||
/// The runtime continues to revert; the UI shows a stronger
|
||||
/// "disabled due to repeated crashes" message and gates re-enable
|
||||
/// behind an explicit clear action.
|
||||
pub hardware_acceleration_locked: bool,
|
||||
pub force_dmabuf_locked: bool,
|
||||
pub preferred_gpu_locked: bool,
|
||||
}
|
||||
|
||||
/// Result of `before_webkit_init`. The caller (main.rs) replaces its
|
||||
/// own variables for hardware_accel / force_dmabuf / preferred_gpu
|
||||
/// with these values before proceeding to apply env vars.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WatchdogResolution {
|
||||
pub hardware_acceleration: bool,
|
||||
pub force_dmabuf: bool,
|
||||
pub preferred_gpu: String,
|
||||
/// Reverted-in-this-boot flags so the UI knows which setting was
|
||||
/// just rolled back. Persisted in CrashFlags as well.
|
||||
pub recovery_messages: Vec<String>,
|
||||
}
|
||||
|
||||
fn state_dir() -> Option<PathBuf> {
|
||||
let base = dirs::data_dir()?.join("qbz").join(STATE_DIR);
|
||||
fs::create_dir_all(&base).ok()?;
|
||||
Some(base)
|
||||
}
|
||||
|
||||
fn read_pending() -> Option<BootAttempt> {
|
||||
let path = state_dir()?.join(PENDING_FILE);
|
||||
let text = fs::read_to_string(&path).ok()?;
|
||||
serde_json::from_str(&text).ok()
|
||||
}
|
||||
|
||||
fn write_pending(attempt: &BootAttempt) {
|
||||
let Some(dir) = state_dir() else {
|
||||
return;
|
||||
};
|
||||
if let Ok(text) = serde_json::to_string_pretty(attempt) {
|
||||
let _ = fs::write(dir.join(PENDING_FILE), text);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_pending() {
|
||||
if let Some(dir) = state_dir() {
|
||||
let _ = fs::remove_file(dir.join(PENDING_FILE));
|
||||
}
|
||||
}
|
||||
|
||||
fn write_last_good(attempt: &BootAttempt) {
|
||||
let Some(dir) = state_dir() else {
|
||||
return;
|
||||
};
|
||||
if let Ok(text) = serde_json::to_string_pretty(attempt) {
|
||||
let _ = fs::write(dir.join(LAST_GOOD_FILE), text);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_crash_flags() -> CrashFlags {
|
||||
let Some(dir) = state_dir() else {
|
||||
return CrashFlags::default();
|
||||
};
|
||||
let path = dir.join(CRASH_FLAGS_FILE);
|
||||
let Ok(text) = fs::read_to_string(&path) else {
|
||||
return CrashFlags::default();
|
||||
};
|
||||
serde_json::from_str(&text).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn write_crash_flags(flags: &CrashFlags) {
|
||||
let Some(dir) = state_dir() else {
|
||||
return;
|
||||
};
|
||||
if let Ok(text) = serde_json::to_string_pretty(flags) {
|
||||
let _ = fs::write(dir.join(CRASH_FLAGS_FILE), text);
|
||||
}
|
||||
}
|
||||
|
||||
/// Called at startup with the settings the current boot WOULD apply if
|
||||
/// nothing crashed. Returns the resolved settings to actually use —
|
||||
/// usually the same input, but with risky knobs reverted if the
|
||||
/// previous boot left a pending marker.
|
||||
///
|
||||
/// Also writes the new pending marker for THIS boot so a crash before
|
||||
/// the frontend signals success is detectable on the next launch.
|
||||
pub fn before_webkit_init(intended: BootAttempt) -> WatchdogResolution {
|
||||
let mut flags = read_crash_flags();
|
||||
let mut resolved = intended.clone();
|
||||
let mut messages: Vec<String> = Vec::new();
|
||||
|
||||
// If a previous boot left a pending marker, it crashed before
|
||||
// first paint. Compare what it tried against what we'd try now,
|
||||
// and revert the field(s) that look like the cause.
|
||||
if let Some(prev) = read_pending() {
|
||||
// Hardware acceleration: if previous tried ON and we're also
|
||||
// trying ON, blame this knob and revert.
|
||||
if prev.hardware_acceleration && resolved.hardware_acceleration {
|
||||
flags.hardware_acceleration_consecutive_failures =
|
||||
flags.hardware_acceleration_consecutive_failures.saturating_add(1);
|
||||
resolved.hardware_acceleration = false;
|
||||
flags.hardware_acceleration_disabled = true;
|
||||
if flags.hardware_acceleration_consecutive_failures >= 2 {
|
||||
flags.hardware_acceleration_locked = true;
|
||||
}
|
||||
messages.push(
|
||||
"Hardware acceleration was disabled because the previous launch crashed during \
|
||||
graphics init."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
// Force DMA-BUF: same logic.
|
||||
if prev.force_dmabuf && resolved.force_dmabuf {
|
||||
flags.force_dmabuf_consecutive_failures =
|
||||
flags.force_dmabuf_consecutive_failures.saturating_add(1);
|
||||
resolved.force_dmabuf = false;
|
||||
flags.force_dmabuf_disabled = true;
|
||||
if flags.force_dmabuf_consecutive_failures >= 2 {
|
||||
flags.force_dmabuf_locked = true;
|
||||
}
|
||||
messages.push(
|
||||
"DMA-BUF renderer was disabled because the previous launch crashed during \
|
||||
graphics init."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
// Preferred GPU: only revert when the SAME non-auto choice
|
||||
// was attempted. Switching from one explicit GPU to another
|
||||
// shouldn't inherit a crash flag from the prior choice.
|
||||
if prev.preferred_gpu == resolved.preferred_gpu && resolved.preferred_gpu != "auto" {
|
||||
flags.preferred_gpu_consecutive_failures =
|
||||
flags.preferred_gpu_consecutive_failures.saturating_add(1);
|
||||
resolved.preferred_gpu = "auto".to_string();
|
||||
flags.preferred_gpu_disabled = true;
|
||||
if flags.preferred_gpu_consecutive_failures >= 2 {
|
||||
flags.preferred_gpu_locked = true;
|
||||
}
|
||||
messages.push(format!(
|
||||
"Rendering GPU preference was reset to Auto because pinning to {:?} crashed the \
|
||||
previous launch.",
|
||||
prev.preferred_gpu
|
||||
));
|
||||
}
|
||||
// If a previous attempt is present but none of the three
|
||||
// matched, the crash was caused by something else — leave the
|
||||
// flags as-is. The pending marker still gets cleared below.
|
||||
|
||||
// Persist the revert decisions back to graphics_settings.db so
|
||||
// the user sees the new state in the UI and the next boot
|
||||
// doesn't try the same setting again.
|
||||
persist_reverts_to_db(&resolved, &intended);
|
||||
|
||||
// Clear consecutive-failure counters for any knob that was
|
||||
// attempted AND succeeded (we infer "succeeded" from "this
|
||||
// boot's input matches last-good"). Handled in
|
||||
// mark_boot_succeeded for paths that actually paint.
|
||||
} else {
|
||||
// No pending marker → previous boot either succeeded or never
|
||||
// ran the watchdog. Reset disabled flags to non-sticky values
|
||||
// so the UI doesn't keep showing a banner across launches
|
||||
// where everything's fine. Locked flags STAY sticky — those
|
||||
// require explicit user clear.
|
||||
if !flags.hardware_acceleration_locked {
|
||||
flags.hardware_acceleration_disabled = false;
|
||||
flags.hardware_acceleration_consecutive_failures = 0;
|
||||
}
|
||||
if !flags.force_dmabuf_locked {
|
||||
flags.force_dmabuf_disabled = false;
|
||||
flags.force_dmabuf_consecutive_failures = 0;
|
||||
}
|
||||
if !flags.preferred_gpu_locked {
|
||||
flags.preferred_gpu_disabled = false;
|
||||
flags.preferred_gpu_consecutive_failures = 0;
|
||||
}
|
||||
}
|
||||
|
||||
write_crash_flags(&flags);
|
||||
write_pending(&resolved);
|
||||
|
||||
WatchdogResolution {
|
||||
hardware_acceleration: resolved.hardware_acceleration,
|
||||
force_dmabuf: resolved.force_dmabuf,
|
||||
preferred_gpu: resolved.preferred_gpu,
|
||||
recovery_messages: messages,
|
||||
}
|
||||
}
|
||||
|
||||
/// Called from a Tauri command after the frontend signals first paint.
|
||||
/// Removes the pending marker so the next launch doesn't think this
|
||||
/// boot crashed; writes last-good as the new baseline; clears
|
||||
/// consecutive-failure counters for whatever settings were active.
|
||||
pub fn mark_boot_succeeded() {
|
||||
// Read what we attempted in this boot — the pending file is the
|
||||
// source of truth for "what was active when WebKit first paint
|
||||
// happened".
|
||||
let attempt = read_pending().unwrap_or_default();
|
||||
clear_pending();
|
||||
write_last_good(&attempt);
|
||||
|
||||
let mut flags = read_crash_flags();
|
||||
// Successful paint with HW accel ON resets that knob's failure
|
||||
// streak entirely (including lockout — the issue resolved).
|
||||
if attempt.hardware_acceleration {
|
||||
flags.hardware_acceleration_consecutive_failures = 0;
|
||||
flags.hardware_acceleration_disabled = false;
|
||||
flags.hardware_acceleration_locked = false;
|
||||
}
|
||||
if attempt.force_dmabuf {
|
||||
flags.force_dmabuf_consecutive_failures = 0;
|
||||
flags.force_dmabuf_disabled = false;
|
||||
flags.force_dmabuf_locked = false;
|
||||
}
|
||||
if attempt.preferred_gpu != "auto" {
|
||||
flags.preferred_gpu_consecutive_failures = 0;
|
||||
flags.preferred_gpu_disabled = false;
|
||||
flags.preferred_gpu_locked = false;
|
||||
}
|
||||
write_crash_flags(&flags);
|
||||
}
|
||||
|
||||
/// Read-only view of the crash flags for the Settings UI.
|
||||
pub fn get_crash_flags() -> CrashFlags {
|
||||
read_crash_flags()
|
||||
}
|
||||
|
||||
/// Clear a single crash recovery flag by name. Frontend uses this when
|
||||
/// the user clicks "Try again" / "I want to re-enable this" on the
|
||||
/// recovery banner. Unknown names are no-ops.
|
||||
pub fn clear_crash_flag(flag: &str) {
|
||||
let mut flags = read_crash_flags();
|
||||
match flag {
|
||||
"hardware_acceleration" => {
|
||||
flags.hardware_acceleration_disabled = false;
|
||||
flags.hardware_acceleration_consecutive_failures = 0;
|
||||
flags.hardware_acceleration_locked = false;
|
||||
}
|
||||
"force_dmabuf" => {
|
||||
flags.force_dmabuf_disabled = false;
|
||||
flags.force_dmabuf_consecutive_failures = 0;
|
||||
flags.force_dmabuf_locked = false;
|
||||
}
|
||||
"preferred_gpu" => {
|
||||
flags.preferred_gpu_disabled = false;
|
||||
flags.preferred_gpu_consecutive_failures = 0;
|
||||
flags.preferred_gpu_locked = false;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
write_crash_flags(&flags);
|
||||
}
|
||||
|
||||
/// Best-effort write of reverted values back to graphics_settings.db
|
||||
/// and developer_settings.db. Failure is non-fatal — the runtime
|
||||
/// values for THIS boot are still the reverted ones (we pass them
|
||||
/// back to main.rs via WatchdogResolution), and the next boot's read
|
||||
/// will see the reverted DB rows too.
|
||||
fn persist_reverts_to_db(resolved: &BootAttempt, intended: &BootAttempt) {
|
||||
if resolved.hardware_acceleration != intended.hardware_acceleration {
|
||||
if let Ok(store) = crate::config::graphics_settings::GraphicsSettingsStore::new() {
|
||||
let _ = store.set_hardware_acceleration(resolved.hardware_acceleration);
|
||||
}
|
||||
}
|
||||
if resolved.force_dmabuf != intended.force_dmabuf {
|
||||
if let Ok(store) = crate::config::developer_settings::DeveloperSettingsStore::new() {
|
||||
let _ = store.set_force_dmabuf(resolved.force_dmabuf);
|
||||
}
|
||||
}
|
||||
if resolved.preferred_gpu != intended.preferred_gpu {
|
||||
if let Ok(store) = crate::config::graphics_settings::GraphicsSettingsStore::new() {
|
||||
let _ = store.set_preferred_gpu(&resolved.preferred_gpu);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,3 +47,60 @@ pub fn v2_apply_graphics_recommendation() -> Result<Vec<String>, Vec<String>> {
|
||||
Err(errors) => Err(errors),
|
||||
}
|
||||
}
|
||||
|
||||
/// Payload for the rendering-GPU dropdown in Settings > Graphics.
|
||||
/// `gpus` is the full list (physical PCI devices intersected with
|
||||
/// available EGL vendor stacks, plus the Software fallback).
|
||||
/// `auto_resolved_id` says which entry, if any, "Auto" currently pins
|
||||
/// to on this system — frontend uses it to label the Auto option (e.g.
|
||||
/// "Automatic (Intel Graphics)") so the user can see what default they
|
||||
/// are getting without picking explicitly. `None` means Auto is a
|
||||
/// no-op on this system (single GPU or no GPU detected).
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct GpuList {
|
||||
pub gpus: Vec<crate::graphics_detection::DetectedGpu>,
|
||||
pub auto_resolved_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Enumerate detected GPUs for the rendering-GPU dropdown in
|
||||
/// Settings > Graphics. Returns physical PCI devices intersected with
|
||||
/// available EGL vendor stacks — entries with `is_usable = false`
|
||||
/// should be hidden / disabled by the UI. Always includes a Software
|
||||
/// fallback so the list is never empty.
|
||||
#[tauri::command]
|
||||
pub fn v2_enumerate_gpus() -> GpuList {
|
||||
let gpus = crate::graphics_detection::enumerate_gpus();
|
||||
let auto_resolved_id =
|
||||
crate::graphics_detection::auto_resolves_to(&gpus).map(|g| g.id.clone());
|
||||
GpuList {
|
||||
gpus,
|
||||
auto_resolved_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by the frontend after first paint to clear the boot
|
||||
/// watchdog's pending marker. If this is never called (because WebKit
|
||||
/// crashed before painting), the next launch sees the marker and
|
||||
/// auto-reverts the offending risky setting.
|
||||
#[tauri::command]
|
||||
pub fn v2_mark_boot_succeeded() {
|
||||
crate::boot_watchdog::mark_boot_succeeded();
|
||||
}
|
||||
|
||||
/// Read the sticky crash flags so Settings > Graphics can render
|
||||
/// the recovery banner. The flags say which risky setting was
|
||||
/// auto-reverted in this boot, the consecutive-failure counter, and
|
||||
/// whether the knob is in lockout state (2+ failures → user must
|
||||
/// explicitly clear before it can be re-enabled).
|
||||
#[tauri::command]
|
||||
pub fn v2_get_crash_flags() -> crate::boot_watchdog::CrashFlags {
|
||||
crate::boot_watchdog::get_crash_flags()
|
||||
}
|
||||
|
||||
/// User-driven clear of a single crash recovery flag. Called when the
|
||||
/// user clicks "Try again" / "Re-enable" on the banner. Valid flag
|
||||
/// names: "hardware_acceleration", "force_dmabuf", "preferred_gpu".
|
||||
#[tauri::command]
|
||||
pub fn v2_clear_crash_flag(flag: String) {
|
||||
crate::boot_watchdog::clear_crash_flag(&flag);
|
||||
}
|
||||
@@ -815,6 +815,21 @@ pub fn v2_set_gsk_renderer(
|
||||
store.set_gsk_renderer(value)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_set_preferred_gpu(
|
||||
value: String,
|
||||
state: State<'_, GraphicsSettingsState>,
|
||||
) -> Result<(), String> {
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or("Graphics settings store not initialized")?;
|
||||
store.set_preferred_gpu(&value)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_clear_cache(state: State<'_, AppState>) -> Result<(), String> {
|
||||
state.audio_cache.clear();
|
||||
|
||||
@@ -62,6 +62,16 @@ pub struct GraphicsSettings {
|
||||
pub gdk_dpi_scale: Option<String>,
|
||||
/// GSK_RENDERER override (None = auto). Values: "gl", "ngl", "vulkan", "cairo"
|
||||
pub gsk_renderer: Option<String>,
|
||||
/// Rendering GPU selection. Values:
|
||||
/// "auto" — let the system pick (default).
|
||||
/// "integrated" — pin to the iGPU (Intel/AMD).
|
||||
/// "discrete" — pin to the dGPU (NVIDIA, or AMD/Intel discrete).
|
||||
/// "software" — software rendering via llvmpipe.
|
||||
/// "<pci-slot>" — pin to a specific detected GPU (e.g. "0000:01:00.0").
|
||||
/// main.rs applies the choice via env vars (EGL vendor pin, DRI_PRIME,
|
||||
/// NV_PRIME_RENDER_OFFLOAD) before WebKit init. Requires restart to
|
||||
/// take effect.
|
||||
pub preferred_gpu: String,
|
||||
}
|
||||
|
||||
impl Default for GraphicsSettings {
|
||||
@@ -72,6 +82,7 @@ impl Default for GraphicsSettings {
|
||||
gdk_scale: None,
|
||||
gdk_dpi_scale: None,
|
||||
gsk_renderer: None,
|
||||
preferred_gpu: "auto".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +153,13 @@ impl GraphicsSettingsStore {
|
||||
let _ = conn.execute_batch("ALTER TABLE graphics_settings ADD COLUMN gdk_scale TEXT;");
|
||||
let _ = conn.execute_batch("ALTER TABLE graphics_settings ADD COLUMN gdk_dpi_scale TEXT;");
|
||||
let _ = conn.execute_batch("ALTER TABLE graphics_settings ADD COLUMN gsk_renderer TEXT;");
|
||||
// 1.2.13: rendering GPU selection. Default 'auto' so existing
|
||||
// installs keep their current behaviour after migration. Stored
|
||||
// as TEXT to accept both the well-known sentinels (auto /
|
||||
// integrated / discrete / software) and specific PCI slot ids.
|
||||
let _ = conn.execute_batch(
|
||||
"ALTER TABLE graphics_settings ADD COLUMN preferred_gpu TEXT NOT NULL DEFAULT 'auto';",
|
||||
);
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
@@ -149,7 +167,7 @@ impl GraphicsSettingsStore {
|
||||
pub fn get_settings(&self) -> Result<GraphicsSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
"SELECT hardware_acceleration, force_x11, gdk_scale, gdk_dpi_scale, gsk_renderer FROM graphics_settings WHERE id = 1",
|
||||
"SELECT hardware_acceleration, force_x11, gdk_scale, gdk_dpi_scale, gsk_renderer, preferred_gpu FROM graphics_settings WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(GraphicsSettings {
|
||||
@@ -158,6 +176,9 @@ impl GraphicsSettingsStore {
|
||||
gdk_scale: row.get::<_, Option<String>>(2)?,
|
||||
gdk_dpi_scale: row.get::<_, Option<String>>(3)?,
|
||||
gsk_renderer: row.get::<_, Option<String>>(4)?,
|
||||
preferred_gpu: row
|
||||
.get::<_, Option<String>>(5)?
|
||||
.unwrap_or_else(|| "auto".to_string()),
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -213,6 +234,16 @@ impl GraphicsSettingsStore {
|
||||
.map_err(|e| format!("Failed to set gsk_renderer: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_preferred_gpu(&self, value: &str) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE graphics_settings SET preferred_gpu = ?1 WHERE id = 1",
|
||||
params![value],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set preferred_gpu: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper for Tauri state management
|
||||
@@ -326,6 +357,25 @@ pub fn set_gdk_dpi_scale(
|
||||
store.set_gdk_dpi_scale(value)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_preferred_gpu(
|
||||
state: tauri::State<'_, GraphicsSettingsState>,
|
||||
value: String,
|
||||
) -> Result<(), String> {
|
||||
log::info!(
|
||||
"[GraphicsSettings] Setting preferred_gpu to {:?} (restart required)",
|
||||
value
|
||||
);
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or("Graphics settings store not initialized")?;
|
||||
store.set_preferred_gpu(&value)
|
||||
}
|
||||
|
||||
// --- Startup Status (set once at app launch, read-only thereafter) ---
|
||||
|
||||
/// Tracks graphics configuration status from startup
|
||||
|
||||
@@ -0,0 +1,494 @@
|
||||
//! GPU enumeration for the Settings > Graphics rendering-GPU picker.
|
||||
//!
|
||||
//! Cross-references the PCI devices visible under `/sys/class/drm/` (real
|
||||
//! physical GPUs) with the EGL vendor JSONs available under
|
||||
//! `/usr/share/glvnd/egl_vendor.d/` (stacks the running process can
|
||||
//! actually use). The intersection is what we expose to the user.
|
||||
//!
|
||||
//! Inside Flatpak/Snap the `/sys` path is still mounted (read-only) so
|
||||
//! PCI detection works; the `/usr/share/glvnd/...` view reflects the
|
||||
//! sandbox's own vendor JSONs which is the correct thing to read — that's
|
||||
//! what the WebKit process inside the sandbox can load.
|
||||
//!
|
||||
//! Companion to `autoconfig_graphics` (which decides "what config should
|
||||
//! we recommend") — this module answers "what hardware/stacks do we
|
||||
//! actually have".
|
||||
//!
|
||||
//! Cross-platform contract: the public API (`enumerate_gpus`,
|
||||
//! `env_vars_for_preferred_gpu`, `PreferredGpu`) compiles on every
|
||||
//! platform. macOS/Windows fall through to empty enumerations and a
|
||||
//! no-op env-var application — the dropdown shows only the Software
|
||||
//! entry there, and the preference can still round-trip through
|
||||
//! storage without breaking the build.
|
||||
|
||||
use serde::Serialize;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::fs;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::path::Path;
|
||||
|
||||
/// PCI vendor IDs we recognize.
|
||||
const VENDOR_INTEL: &str = "0x8086";
|
||||
const VENDOR_AMD: &str = "0x1002";
|
||||
const VENDOR_NVIDIA: &str = "0x10de";
|
||||
|
||||
/// Coarse classification of a GPU's role on the system. Drives the
|
||||
/// label the Settings dropdown shows ("Integrated" vs "Discrete") and
|
||||
/// the default `preferred_gpu` selection when the user hasn't picked
|
||||
/// one yet.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GpuKind {
|
||||
Integrated,
|
||||
Discrete,
|
||||
/// Pure-software rendering (llvmpipe / cpu fallback). Not a real
|
||||
/// device — present in the list only so the dropdown can offer
|
||||
/// "Software (CPU)" as an explicit choice.
|
||||
Software,
|
||||
}
|
||||
|
||||
/// One detected GPU with everything the picker needs to render and the
|
||||
/// env-var application path needs to act on the user's choice.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct DetectedGpu {
|
||||
/// Stable identifier across launches: the PCI slot string for real
|
||||
/// devices (e.g. "0000:01:00.0") or a sentinel for software.
|
||||
pub id: String,
|
||||
pub vendor: String,
|
||||
/// Human-readable model where available (from drm
|
||||
/// `device/product_name` or vendor heuristic fallback).
|
||||
pub name: String,
|
||||
/// PCI ID as `vendor:device` for diagnostics. Stripped of the `0x`
|
||||
/// prefix so it reads like `8086:7d55`.
|
||||
pub pci_id: Option<String>,
|
||||
pub kind: GpuKind,
|
||||
/// Absolute path of the EGL vendor JSON that exposes this GPU's
|
||||
/// stack to the running process, if one is available. None means
|
||||
/// the user's sandbox/install doesn't have the vendor library
|
||||
/// reachable, in which case picking this GPU would fall back to
|
||||
/// the system default.
|
||||
pub egl_vendor_json: Option<String>,
|
||||
/// True when the GPU is BOTH physically present AND reachable via
|
||||
/// an EGL vendor JSON. The picker should grey out (or omit)
|
||||
/// entries where this is false.
|
||||
pub is_usable: bool,
|
||||
}
|
||||
|
||||
/// Enumerate every GPU the user could pick. Always returns at least
|
||||
/// the `Software` entry so the dropdown is never empty on systems with
|
||||
/// no detectable hardware.
|
||||
pub fn enumerate_gpus() -> Vec<DetectedGpu> {
|
||||
let physical = detect_physical_gpus();
|
||||
let egl = enumerate_egl_vendor_jsons();
|
||||
|
||||
let mut result: Vec<DetectedGpu> = physical
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let egl_path = match p.vendor.as_str() {
|
||||
"NVIDIA" => egl.nvidia.clone(),
|
||||
// Intel + AMD + Nouveau all ship through Mesa EGL.
|
||||
_ => egl.mesa.clone(),
|
||||
};
|
||||
DetectedGpu {
|
||||
id: p.pci_slot.clone(),
|
||||
vendor: p.vendor.clone(),
|
||||
name: p.name.clone(),
|
||||
pci_id: Some(format!("{}:{}", p.pci_vendor_hex, p.pci_device_hex)),
|
||||
kind: p.kind,
|
||||
is_usable: egl_path.is_some(),
|
||||
egl_vendor_json: egl_path,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Always expose Software as a fallback. Mesa ships llvmpipe through
|
||||
// the same JSON the user can pin EGL to via LIBGL_ALWAYS_SOFTWARE=1;
|
||||
// the runtime branch in main.rs handles the env wiring.
|
||||
result.push(DetectedGpu {
|
||||
id: "software".to_string(),
|
||||
vendor: "Software".to_string(),
|
||||
name: "Software (CPU, llvmpipe)".to_string(),
|
||||
pci_id: None,
|
||||
kind: GpuKind::Software,
|
||||
egl_vendor_json: egl.mesa.clone(),
|
||||
is_usable: egl.mesa.is_some(),
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ---- Physical PCI detection ------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PhysicalGpu {
|
||||
pci_slot: String,
|
||||
pci_vendor_hex: String,
|
||||
pci_device_hex: String,
|
||||
vendor: String,
|
||||
name: String,
|
||||
kind: GpuKind,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn detect_physical_gpus() -> Vec<PhysicalGpu> {
|
||||
// macOS / Windows: no sysfs path to enumerate from. The picker will
|
||||
// only show the Software fallback there, which is the right shape
|
||||
// until we add platform-specific detection.
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn enumerate_egl_vendor_jsons() -> EglVendors {
|
||||
EglVendors::default()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn detect_physical_gpus() -> Vec<PhysicalGpu> {
|
||||
let drm_root = Path::new("/sys/class/drm");
|
||||
let entries = match fs::read_dir(drm_root) {
|
||||
Ok(it) => it,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut gpus: Vec<PhysicalGpu> = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
// Match `card0`, `card1`, ... — skip the `card0-DP-1` connector
|
||||
// pseudo-entries (have a dash in the name) and the renderD*
|
||||
// nodes (different prefix).
|
||||
if !name.starts_with("card") || name.contains('-') {
|
||||
continue;
|
||||
}
|
||||
let device_path = entry.path().join("device");
|
||||
let canonical = match fs::canonicalize(&device_path) {
|
||||
Ok(p) => p,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let pci_slot = pci_slot_from_path(&canonical).unwrap_or_else(|| name.clone());
|
||||
|
||||
let pci_vendor_hex = read_trimmed(&device_path.join("vendor")).unwrap_or_default();
|
||||
let pci_device_hex = read_trimmed(&device_path.join("device")).unwrap_or_default();
|
||||
let vendor = classify_vendor(&pci_vendor_hex);
|
||||
if vendor == "Unknown" {
|
||||
// Not a known consumer GPU vendor — skip rather than expose
|
||||
// server / virtual GPU rows the user can't really act on.
|
||||
continue;
|
||||
}
|
||||
|
||||
let product_name = read_trimmed(&device_path.join("product_name"))
|
||||
.filter(|s| !s.is_empty());
|
||||
let name = build_human_name(&vendor, product_name.as_deref());
|
||||
|
||||
gpus.push(PhysicalGpu {
|
||||
pci_slot,
|
||||
pci_vendor_hex: strip_hex_prefix(&pci_vendor_hex),
|
||||
pci_device_hex: strip_hex_prefix(&pci_device_hex),
|
||||
vendor,
|
||||
name,
|
||||
// Kind needs the full list to compare slots → fill in below.
|
||||
kind: GpuKind::Discrete,
|
||||
});
|
||||
}
|
||||
|
||||
classify_kinds(&mut gpus);
|
||||
gpus
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn pci_slot_from_path(path: &Path) -> Option<String> {
|
||||
// /sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0 → "0000:01:00.0"
|
||||
path.file_name().map(|s| s.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn read_trimmed(p: &Path) -> Option<String> {
|
||||
fs::read_to_string(p).ok().map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
fn strip_hex_prefix(s: &str) -> String {
|
||||
s.strip_prefix("0x").unwrap_or(s).to_string()
|
||||
}
|
||||
|
||||
fn classify_vendor(pci_vendor: &str) -> String {
|
||||
match pci_vendor {
|
||||
VENDOR_INTEL => "Intel".to_string(),
|
||||
VENDOR_AMD => "AMD".to_string(),
|
||||
VENDOR_NVIDIA => "NVIDIA".to_string(),
|
||||
_ => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_human_name(vendor: &str, product: Option<&str>) -> String {
|
||||
match (vendor, product) {
|
||||
(_, Some(p)) if !p.is_empty() => p.to_string(),
|
||||
("Intel", None) => "Intel Graphics".to_string(),
|
||||
("AMD", None) => "AMD Graphics".to_string(),
|
||||
("NVIDIA", None) => "NVIDIA Graphics".to_string(),
|
||||
_ => "GPU".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify each GPU as Integrated vs Discrete using a small heuristic
|
||||
/// over the full list:
|
||||
/// - NVIDIA is always Discrete (consumer NVIDIA iGPUs don't exist).
|
||||
/// - Intel: if there is also a NVIDIA/AMD discrete present, Intel is
|
||||
/// Integrated. If Intel is alone, infer from PCI slot — slot
|
||||
/// `0000:00:...` (root bus) is integrated, anything beyond is
|
||||
/// treated as discrete (covers Intel Arc desktop cards).
|
||||
/// - AMD: tricky. If there is also a NVIDIA, AMD is the iGPU (APU).
|
||||
/// If AMD is alone, infer from slot like Intel.
|
||||
fn classify_kinds(gpus: &mut [PhysicalGpu]) {
|
||||
let has_nvidia = gpus.iter().any(|g| g.vendor == "NVIDIA");
|
||||
let has_amd = gpus.iter().any(|g| g.vendor == "AMD");
|
||||
|
||||
for g in gpus.iter_mut() {
|
||||
g.kind = match g.vendor.as_str() {
|
||||
"NVIDIA" => GpuKind::Discrete,
|
||||
"Intel" => {
|
||||
if has_nvidia || has_amd {
|
||||
GpuKind::Integrated
|
||||
} else if slot_is_root_bus(&g.pci_slot) {
|
||||
GpuKind::Integrated
|
||||
} else {
|
||||
GpuKind::Discrete
|
||||
}
|
||||
}
|
||||
"AMD" => {
|
||||
if has_nvidia {
|
||||
GpuKind::Integrated
|
||||
} else if slot_is_root_bus(&g.pci_slot) {
|
||||
GpuKind::Integrated
|
||||
} else {
|
||||
GpuKind::Discrete
|
||||
}
|
||||
}
|
||||
_ => GpuKind::Discrete,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn slot_is_root_bus(slot: &str) -> bool {
|
||||
// PCI slot format: "DDDD:BB:DD.F" where BB is the bus. Root bus is 00.
|
||||
slot.split(':').nth(1).map(|bb| bb == "00").unwrap_or(false)
|
||||
}
|
||||
|
||||
// ---- EGL vendor JSON enumeration ------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct EglVendors {
|
||||
mesa: Option<String>,
|
||||
nvidia: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn enumerate_egl_vendor_jsons() -> EglVendors {
|
||||
let candidates: [&str; 2] = [
|
||||
"/usr/share/glvnd/egl_vendor.d",
|
||||
"/etc/glvnd/egl_vendor.d",
|
||||
];
|
||||
let mut out = EglVendors::default();
|
||||
for root in candidates {
|
||||
let Ok(entries) = fs::read_dir(root) else {
|
||||
continue;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let Some(file_name) = path.file_name().and_then(|s| s.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if !file_name.ends_with(".json") {
|
||||
continue;
|
||||
}
|
||||
let lower = file_name.to_lowercase();
|
||||
let abs = path.to_string_lossy().to_string();
|
||||
if lower.contains("nvidia") {
|
||||
if out.nvidia.is_none() {
|
||||
out.nvidia = Some(abs);
|
||||
}
|
||||
} else if lower.contains("mesa") {
|
||||
if out.mesa.is_none() {
|
||||
out.mesa = Some(abs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// An action to apply to the process environment. We need `Unset` (not
|
||||
/// just `Set`) because the parent shell/session may have already set
|
||||
/// vars that pull rendering back to the wrong GPU — most importantly
|
||||
/// `__NV_PRIME_RENDER_OFFLOAD=1` on Optimus laptops, which makes NVIDIA
|
||||
/// hijack the render even after we pin EGL to Mesa.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EnvAction {
|
||||
Set(String, String),
|
||||
Unset(String),
|
||||
}
|
||||
|
||||
/// Decide what `PreferredGpu::Auto` should pin on this system. The
|
||||
/// rule: if we detect both an iGPU and a dGPU that are usable, pin to
|
||||
/// the iGPU — that's the sweet-spot config for hybrid laptops (Optimus,
|
||||
/// Mux-less Ryzen, etc.) where the dGPU otherwise hijacks WebKit
|
||||
/// rendering and wastes battery. Single-GPU or no-GPU systems return
|
||||
/// None → caller leaves env alone and the system default wins (correct
|
||||
/// behavior when there's nothing to choose between).
|
||||
pub fn auto_resolves_to<'a>(detected: &'a [DetectedGpu]) -> Option<&'a DetectedGpu> {
|
||||
let integrated = detected
|
||||
.iter()
|
||||
.find(|g| g.kind == GpuKind::Integrated && g.is_usable);
|
||||
let has_discrete = detected
|
||||
.iter()
|
||||
.any(|g| g.kind == GpuKind::Discrete && g.is_usable);
|
||||
if integrated.is_some() && has_discrete {
|
||||
integrated
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the env actions that pin the running process to a specific GPU
|
||||
/// stack. The caller (main.rs at startup) applies them via
|
||||
/// `std::env::set_var` / `remove_var` before WebKit initializes.
|
||||
/// Returning an empty vec means "auto with no resolution" — don't touch
|
||||
/// the environment.
|
||||
pub fn env_vars_for_preferred_gpu(
|
||||
preferred: &PreferredGpu,
|
||||
detected: &[DetectedGpu],
|
||||
) -> Vec<EnvAction> {
|
||||
match preferred {
|
||||
PreferredGpu::Auto => match auto_resolves_to(detected) {
|
||||
Some(g) => env_vars_for_gpu(g),
|
||||
None => Vec::new(),
|
||||
},
|
||||
PreferredGpu::Software => {
|
||||
// Drop to software rendering across every layer that can
|
||||
// sneak the GPU back in. Same set as scripts/dev-cpu-mode.sh.
|
||||
vec![
|
||||
EnvAction::Set("LIBGL_ALWAYS_SOFTWARE".to_string(), "1".to_string()),
|
||||
EnvAction::Set("WEBKIT_DISABLE_DMABUF_RENDERER".to_string(), "1".to_string()),
|
||||
EnvAction::Set("GSK_RENDERER".to_string(), "cairo".to_string()),
|
||||
EnvAction::Unset("__NV_PRIME_RENDER_OFFLOAD".to_string()),
|
||||
]
|
||||
}
|
||||
PreferredGpu::Kind(kind) => {
|
||||
let target = detected
|
||||
.iter()
|
||||
.find(|g| g.kind == *kind && g.is_usable);
|
||||
let Some(g) = target else {
|
||||
return Vec::new();
|
||||
};
|
||||
env_vars_for_gpu(g)
|
||||
}
|
||||
PreferredGpu::SpecificId(id) => {
|
||||
let target = detected.iter().find(|g| &g.id == id);
|
||||
let Some(g) = target else {
|
||||
return Vec::new();
|
||||
};
|
||||
env_vars_for_gpu(g)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn env_vars_for_gpu(g: &DetectedGpu) -> Vec<EnvAction> {
|
||||
let mut env: Vec<EnvAction> = Vec::new();
|
||||
if let Some(path) = &g.egl_vendor_json {
|
||||
env.push(EnvAction::Set(
|
||||
"__EGL_VENDOR_LIBRARY_FILENAMES".to_string(),
|
||||
path.clone(),
|
||||
));
|
||||
}
|
||||
match g.vendor.as_str() {
|
||||
"Intel" | "AMD" => {
|
||||
env.push(EnvAction::Set(
|
||||
"__GLX_VENDOR_LIBRARY_NAME".to_string(),
|
||||
"mesa".to_string(),
|
||||
));
|
||||
env.push(EnvAction::Set("DRI_PRIME".to_string(), "0".to_string()));
|
||||
// Kill any NVIDIA PRIME offload the session might have set.
|
||||
// Without this, on Optimus laptops the dGPU still wins even
|
||||
// with EGL/GLX pinned to Mesa — `__NV_PRIME_RENDER_OFFLOAD=1`
|
||||
// takes priority and routes WebKit composition back to
|
||||
// NVIDIA. Mirrors `unset __NV_PRIME_RENDER_OFFLOAD` in
|
||||
// scripts/dev-igpu-mode.sh.
|
||||
env.push(EnvAction::Unset(
|
||||
"__NV_PRIME_RENDER_OFFLOAD".to_string(),
|
||||
));
|
||||
}
|
||||
"NVIDIA" => {
|
||||
env.push(EnvAction::Set(
|
||||
"__NV_PRIME_RENDER_OFFLOAD".to_string(),
|
||||
"1".to_string(),
|
||||
));
|
||||
env.push(EnvAction::Set(
|
||||
"__GLX_VENDOR_LIBRARY_NAME".to_string(),
|
||||
"nvidia".to_string(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
env
|
||||
}
|
||||
|
||||
/// User-facing preference. Mirrors the column in graphics_settings.db
|
||||
/// (free-form text with a small known set of values).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PreferredGpu {
|
||||
Auto,
|
||||
Software,
|
||||
Kind(GpuKind),
|
||||
SpecificId(String),
|
||||
}
|
||||
|
||||
impl PreferredGpu {
|
||||
pub fn parse(raw: &str) -> Self {
|
||||
match raw.trim().to_lowercase().as_str() {
|
||||
"" | "auto" => Self::Auto,
|
||||
"software" | "cpu" => Self::Software,
|
||||
"integrated" | "igpu" => Self::Kind(GpuKind::Integrated),
|
||||
"discrete" | "dgpu" => Self::Kind(GpuKind::Discrete),
|
||||
other => Self::SpecificId(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_storage_string(&self) -> String {
|
||||
match self {
|
||||
Self::Auto => "auto".to_string(),
|
||||
Self::Software => "software".to_string(),
|
||||
Self::Kind(GpuKind::Integrated) => "integrated".to_string(),
|
||||
Self::Kind(GpuKind::Discrete) => "discrete".to_string(),
|
||||
Self::Kind(GpuKind::Software) => "software".to_string(),
|
||||
Self::SpecificId(id) => id.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn preferred_gpu_round_trip() {
|
||||
for raw in ["auto", "software", "integrated", "discrete", "0000:01:00.0"] {
|
||||
let parsed = PreferredGpu::parse(raw);
|
||||
assert_eq!(parsed.as_storage_string(), raw);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slot_root_bus_classification() {
|
||||
assert!(slot_is_root_bus("0000:00:02.0"));
|
||||
assert!(slot_is_root_bus("0000:00:01.0"));
|
||||
assert!(!slot_is_root_bus("0000:01:00.0"));
|
||||
assert!(!slot_is_root_bus("0000:02:00.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vendor_classification() {
|
||||
assert_eq!(classify_vendor(VENDOR_INTEL), "Intel");
|
||||
assert_eq!(classify_vendor(VENDOR_AMD), "AMD");
|
||||
assert_eq!(classify_vendor(VENDOR_NVIDIA), "NVIDIA");
|
||||
assert_eq!(classify_vendor("0xdeadbeef"), "Unknown");
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ pub fn main_window_built_transparent() -> bool {
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod autoconfig_graphics;
|
||||
pub mod boot_watchdog;
|
||||
pub mod graphics_detection;
|
||||
|
||||
pub mod api;
|
||||
pub mod api_cache;
|
||||
@@ -1656,8 +1658,13 @@ pub fn run(qconnect_cli_override: Option<bool>) {
|
||||
commands_v2::v2_set_gdk_scale,
|
||||
commands_v2::v2_set_gdk_dpi_scale,
|
||||
commands_v2::v2_set_gsk_renderer,
|
||||
commands_v2::v2_set_preferred_gpu,
|
||||
commands_v2::v2_get_graphics_recommendation,
|
||||
commands_v2::v2_apply_graphics_recommendation,
|
||||
commands_v2::v2_enumerate_gpus,
|
||||
commands_v2::v2_mark_boot_succeeded,
|
||||
commands_v2::v2_get_crash_flags,
|
||||
commands_v2::v2_clear_crash_flag,
|
||||
commands_v2::v2_clear_cache,
|
||||
commands_v2::v2_clear_artist_cache,
|
||||
commands_v2::v2_get_vector_store_stats,
|
||||
|
||||
+78
-4
@@ -288,8 +288,14 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
// Developer settings: force_dmabuf override (from Settings > Developer Mode)
|
||||
let dev_force_dmabuf =
|
||||
// ---- Read remaining risky settings (no env vars set yet) ----
|
||||
let preferred_gpu_raw_intended = graphics_db
|
||||
.as_ref()
|
||||
.map(|s| s.preferred_gpu.clone())
|
||||
.unwrap_or_else(|| "auto".to_string());
|
||||
let force_x11_db = graphics_db.as_ref().map(|s| s.force_x11).unwrap_or(false);
|
||||
|
||||
let dev_force_dmabuf_intended =
|
||||
match qbz_nix_lib::config::developer_settings::DeveloperSettingsStore::new() {
|
||||
Ok(store) => match store.get_settings() {
|
||||
Ok(settings) => settings.force_dmabuf,
|
||||
@@ -309,6 +315,74 @@ fn main() {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Boot watchdog: revert risky settings if previous boot crashed ----
|
||||
// If a prior launch wrote a pending marker that never got cleared,
|
||||
// those settings crashed WebKit before first paint. The watchdog
|
||||
// returns the resolved values to actually use this boot (usually
|
||||
// the intended ones, with revert flags set for any knob that
|
||||
// matched the prior crash). It also writes a fresh pending marker
|
||||
// for THIS boot; the frontend's first-paint hook clears it.
|
||||
let watchdog_resolution = qbz_nix_lib::boot_watchdog::before_webkit_init(
|
||||
qbz_nix_lib::boot_watchdog::BootAttempt {
|
||||
hardware_acceleration: hardware_accel,
|
||||
force_dmabuf: dev_force_dmabuf_intended,
|
||||
preferred_gpu: preferred_gpu_raw_intended.clone(),
|
||||
force_x11: force_x11_db,
|
||||
},
|
||||
);
|
||||
let hardware_accel = watchdog_resolution.hardware_acceleration;
|
||||
let dev_force_dmabuf = watchdog_resolution.force_dmabuf;
|
||||
let preferred_gpu_raw = watchdog_resolution.preferred_gpu.clone();
|
||||
for msg in &watchdog_resolution.recovery_messages {
|
||||
eprintln!("[QBZ] {}", msg);
|
||||
qbz_nix_lib::logging::log_startup(&format!("[QBZ] {}", msg));
|
||||
}
|
||||
|
||||
// ---- Apply preferred_gpu env vars (using watchdog-resolved value) ----
|
||||
let preferred_gpu_choice =
|
||||
qbz_nix_lib::graphics_detection::PreferredGpu::parse(&preferred_gpu_raw);
|
||||
if !matches!(
|
||||
preferred_gpu_choice,
|
||||
qbz_nix_lib::graphics_detection::PreferredGpu::Auto
|
||||
) {
|
||||
let detected = qbz_nix_lib::graphics_detection::enumerate_gpus();
|
||||
let env_pairs = qbz_nix_lib::graphics_detection::env_vars_for_preferred_gpu(
|
||||
&preferred_gpu_choice,
|
||||
&detected,
|
||||
);
|
||||
if env_pairs.is_empty() {
|
||||
qbz_nix_lib::logging::log_startup(&format!(
|
||||
"[QBZ] preferred_gpu={:?} could not be resolved (no matching detected GPU); falling back to system default",
|
||||
preferred_gpu_raw
|
||||
));
|
||||
} else {
|
||||
qbz_nix_lib::logging::log_startup(&format!(
|
||||
"[QBZ] preferred_gpu={:?}: applying GPU pin env vars",
|
||||
preferred_gpu_raw
|
||||
));
|
||||
for action in &env_pairs {
|
||||
match action {
|
||||
qbz_nix_lib::graphics_detection::EnvAction::Set(name, value) => {
|
||||
qbz_nix_lib::logging::log_startup(&format!(
|
||||
"[QBZ] set {}={}",
|
||||
name, value
|
||||
));
|
||||
std::env::set_var(name, value);
|
||||
}
|
||||
qbz_nix_lib::graphics_detection::EnvAction::Unset(name) => {
|
||||
qbz_nix_lib::logging::log_startup(&format!(
|
||||
"[QBZ] unset {}",
|
||||
name
|
||||
));
|
||||
std::env::remove_var(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Apply dev_force_dmabuf env var (using watchdog-resolved value) ----
|
||||
if dev_force_dmabuf {
|
||||
std::env::set_var("QBZ_FORCE_DMABUF", "1");
|
||||
qbz_nix_lib::logging::log_startup(
|
||||
@@ -321,8 +395,8 @@ fn main() {
|
||||
let force_dmabuf = std::env::var("QBZ_FORCE_DMABUF").as_deref() == Ok("1");
|
||||
let disable_dmabuf = std::env::var("QBZ_DISABLE_DMABUF").as_deref() == Ok("1");
|
||||
|
||||
// Force X11: persistent setting from DB, env var overrides (crash recovery)
|
||||
let force_x11_db = graphics_db.as_ref().map(|s| s.force_x11).unwrap_or(false);
|
||||
// Force X11: persistent setting from DB (read above for watchdog),
|
||||
// env var overrides (crash recovery).
|
||||
let force_x11 = match std::env::var("QBZ_FORCE_X11").as_deref() {
|
||||
Ok("1") => {
|
||||
qbz_nix_lib::logging::log_startup("[QBZ] Env override: QBZ_FORCE_X11=1");
|
||||
|
||||
@@ -389,6 +389,49 @@
|
||||
let gdkScale = $state('');
|
||||
let gdkDpiScale = $state('');
|
||||
let gskRenderer = $state('');
|
||||
let preferredGpu = $state('auto');
|
||||
|
||||
// Rendering-GPU detection for the Settings > Graphics dropdown. The
|
||||
// backend cross-references PCI devices visible under /sys/class/drm/
|
||||
// with EGL vendor JSONs available to the running process; the result
|
||||
// lists every GPU the user can practically pick (auto + each usable
|
||||
// discrete/integrated + software fallback). Loaded on mount.
|
||||
type DetectedGpu = {
|
||||
id: string;
|
||||
vendor: string;
|
||||
name: string;
|
||||
pci_id: string | null;
|
||||
kind: 'integrated' | 'discrete' | 'software';
|
||||
egl_vendor_json: string | null;
|
||||
is_usable: boolean;
|
||||
};
|
||||
let detectedGpus = $state<DetectedGpu[]>([]);
|
||||
// Which GPU the backend currently resolves "Auto" to on this system.
|
||||
// Non-null only on hybrid setups (iGPU + dGPU) where Auto pins to the
|
||||
// iGPU; single-GPU and no-GPU systems get null and the dropdown shows
|
||||
// a plain "Automatic" label without a sub-name.
|
||||
let autoResolvedGpuId = $state<string | null>(null);
|
||||
type GpuList = {
|
||||
gpus: DetectedGpu[];
|
||||
auto_resolved_id: string | null;
|
||||
};
|
||||
|
||||
// Sticky crash recovery flags from the boot watchdog. If a previous
|
||||
// launch crashed during graphics init, the offending knob was
|
||||
// auto-reverted and one of these flags is true. Settings > Graphics
|
||||
// renders a banner so the user can retry or keep the safe setting.
|
||||
type CrashFlags = {
|
||||
hardware_acceleration_disabled: boolean;
|
||||
hardware_acceleration_consecutive_failures: number;
|
||||
force_dmabuf_disabled: boolean;
|
||||
force_dmabuf_consecutive_failures: number;
|
||||
preferred_gpu_disabled: boolean;
|
||||
preferred_gpu_consecutive_failures: number;
|
||||
hardware_acceleration_locked: boolean;
|
||||
force_dmabuf_locked: boolean;
|
||||
preferred_gpu_locked: boolean;
|
||||
};
|
||||
let crashFlags = $state<CrashFlags | null>(null);
|
||||
|
||||
// Graphics recommendation surfaced in the Graphics tab. Populated on
|
||||
// mount via v2_get_graphics_recommendation (matrix lives in
|
||||
@@ -1836,6 +1879,7 @@
|
||||
gdkDpiScale = settings.gdk_dpi_scale || '';
|
||||
gskRenderer = settings.gsk_renderer || '';
|
||||
hardwareAcceleration = settings.hardware_acceleration;
|
||||
preferredGpu = settings.preferred_gpu || 'auto';
|
||||
}).catch(() => {});
|
||||
|
||||
// Load graphics startup status (for fallback warning)
|
||||
@@ -1856,6 +1900,21 @@
|
||||
.catch((err) => {
|
||||
console.warn('[Settings] graphics recommendation unavailable:', err);
|
||||
});
|
||||
invoke<GpuList>('v2_enumerate_gpus')
|
||||
.then((payload) => {
|
||||
detectedGpus = payload.gpus;
|
||||
autoResolvedGpuId = payload.auto_resolved_id;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('[Settings] gpu enumeration unavailable:', err);
|
||||
});
|
||||
invoke<CrashFlags>('v2_get_crash_flags')
|
||||
.then((flags) => {
|
||||
crashFlags = flags;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('[Settings] crash flags unavailable:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to offline state changes
|
||||
@@ -4113,6 +4172,67 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePreferredGpuChange(value: string) {
|
||||
try {
|
||||
await invoke('v2_set_preferred_gpu', { value });
|
||||
preferredGpu = value;
|
||||
showToast($t('settings.graphics.restartRequired'), 'info');
|
||||
} catch (err) {
|
||||
console.error('Failed to set preferred_gpu:', err);
|
||||
showToast(String(err), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClearCrashFlag(flag: 'hardware_acceleration' | 'force_dmabuf' | 'preferred_gpu') {
|
||||
try {
|
||||
await invoke('v2_clear_crash_flag', { flag });
|
||||
const flags = await invoke<CrashFlags>('v2_get_crash_flags');
|
||||
crashFlags = flags;
|
||||
// If the user just cleared the lockout, also re-apply the
|
||||
// setting they probably wanted. Hardware acceleration: the user
|
||||
// explicitly wants it back ON; same for force_dmabuf and a
|
||||
// non-auto preferred_gpu. We don't auto-re-enable here because
|
||||
// the watchdog already wrote the safer value to DB; the user
|
||||
// needs to re-toggle to express intent. Toast hints what to do.
|
||||
showToast($t('settings.graphics.recovery.cleared'), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to clear crash flag:', err);
|
||||
showToast(String(err), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
const preferredGpuOptions = $derived.by(() => {
|
||||
const autoTarget = autoResolvedGpuId
|
||||
? detectedGpus.find((g) => g.id === autoResolvedGpuId)
|
||||
: null;
|
||||
const autoLabel = autoTarget
|
||||
? $t('settings.graphics.gpu.autoWithTarget', { values: { name: autoTarget.name } })
|
||||
: $t('settings.graphics.gpu.auto');
|
||||
const opts: { value: string; label: string }[] = [
|
||||
{ value: 'auto', label: autoLabel },
|
||||
];
|
||||
let hasIntegrated = false;
|
||||
let hasDiscrete = false;
|
||||
for (const g of detectedGpus) {
|
||||
if (!g.is_usable) continue;
|
||||
if (g.kind === 'integrated' && !hasIntegrated) {
|
||||
opts.push({ value: 'integrated', label: $t('settings.graphics.gpu.integrated', { values: { name: g.name } }) });
|
||||
hasIntegrated = true;
|
||||
}
|
||||
if (g.kind === 'discrete' && !hasDiscrete) {
|
||||
opts.push({ value: 'discrete', label: $t('settings.graphics.gpu.discrete', { values: { name: g.name } }) });
|
||||
hasDiscrete = true;
|
||||
}
|
||||
}
|
||||
opts.push({ value: 'software', label: $t('settings.graphics.gpu.software') });
|
||||
return opts;
|
||||
});
|
||||
|
||||
function getPreferredGpuDisplayValue(): string {
|
||||
const match = preferredGpuOptions.find((o) => o.value === preferredGpu);
|
||||
return match ? match.label : preferredGpu;
|
||||
}
|
||||
|
||||
// Whether the auto-detected recommendation diverges from any of the
|
||||
// four persisted graphics settings. The DMA-BUF axis is inverted:
|
||||
// recommendation `disable_dmabuf=true` maps to `force_dmabuf=false`
|
||||
@@ -5391,6 +5511,47 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if crashFlags && (crashFlags.hardware_acceleration_disabled || crashFlags.force_dmabuf_disabled || crashFlags.preferred_gpu_disabled)}
|
||||
<div class="crash-recovery-banner" class:locked={crashFlags.hardware_acceleration_locked || crashFlags.force_dmabuf_locked || crashFlags.preferred_gpu_locked}>
|
||||
<TriangleAlert size={16} />
|
||||
<div class="crash-recovery-body">
|
||||
<span class="crash-recovery-title">
|
||||
{#if crashFlags.hardware_acceleration_locked || crashFlags.force_dmabuf_locked || crashFlags.preferred_gpu_locked}
|
||||
{$t('settings.graphics.recovery.titleLocked')}
|
||||
{:else}
|
||||
{$t('settings.graphics.recovery.title')}
|
||||
{/if}
|
||||
</span>
|
||||
<ul class="crash-recovery-list">
|
||||
{#if crashFlags.hardware_acceleration_disabled}
|
||||
<li>
|
||||
<span>{$t('settings.graphics.recovery.hwAccelDisabled')}</span>
|
||||
<button class="link-btn" onclick={() => handleClearCrashFlag('hardware_acceleration')}>
|
||||
{$t('settings.graphics.recovery.tryAgain')}
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if crashFlags.force_dmabuf_disabled}
|
||||
<li>
|
||||
<span>{$t('settings.graphics.recovery.dmabufDisabled')}</span>
|
||||
<button class="link-btn" onclick={() => handleClearCrashFlag('force_dmabuf')}>
|
||||
{$t('settings.graphics.recovery.tryAgain')}
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if crashFlags.preferred_gpu_disabled}
|
||||
<li>
|
||||
<span>{$t('settings.graphics.recovery.gpuPinDisabled')}</span>
|
||||
<button class="link-btn" onclick={() => handleClearCrashFlag('preferred_gpu')}>
|
||||
{$t('settings.graphics.recovery.tryAgain')}
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if graphicsUsingFallback}
|
||||
<div class="composition-warning fallback-warning">
|
||||
<TriangleAlert size={14} />
|
||||
@@ -5436,6 +5597,23 @@
|
||||
<Toggle enabled={hardwareAcceleration} onchange={(v) => handleHardwareAccelerationChange(v)} />
|
||||
</div>
|
||||
|
||||
{#if preferredGpuOptions.length > 1}
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.graphics.gpu.label')}</span>
|
||||
<span class="setting-desc">{$t('settings.graphics.gpu.desc')}</span>
|
||||
</div>
|
||||
<Dropdown
|
||||
value={getPreferredGpuDisplayValue()}
|
||||
options={preferredGpuOptions.map((o) => o.label)}
|
||||
onchange={(displayLabel) => {
|
||||
const match = preferredGpuOptions.find((o) => o.label === displayLabel);
|
||||
if (match) void handlePreferredGpuChange(match.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">{$t('settings.graphics.forceDmabuf')}</span>
|
||||
@@ -7246,6 +7424,83 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Crash-recovery banner. Yellow when a single setting was reverted
|
||||
(recoverable), red when one or more knobs hit lockout state after
|
||||
2+ consecutive crashes — the user has to explicitly clear the flag
|
||||
to re-enable. */
|
||||
.crash-recovery-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.crash-recovery-banner.locked {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
}
|
||||
|
||||
.crash-recovery-banner :global(svg) {
|
||||
flex-shrink: 0;
|
||||
color: #f59e0b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.crash-recovery-banner.locked :global(svg) {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.crash-recovery-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.crash-recovery-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.crash-recovery-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.crash-recovery-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
background: transparent;
|
||||
color: var(--accent-primary, #7c3aed);
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.link-btn:hover {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background: var(--accent-primary, #7c3aed);
|
||||
color: var(--btn-primary-text, #fff);
|
||||
|
||||
@@ -2830,6 +2830,25 @@
|
||||
"dismiss": "Verwerfen",
|
||||
"redetect": "Empfohlene Einstellungen erkennen",
|
||||
"noChanges": "Die Einstellungen entsprechen bereits der Empfehlung für dieses System."
|
||||
},
|
||||
"restartRequired": "QBZ neu starten, damit die Änderung wirksam wird.",
|
||||
"gpu": {
|
||||
"label": "Rendering-GPU",
|
||||
"desc": "WebKit an eine bestimmte GPU binden. Neustart erforderlich. Auto überlässt die Wahl dem System.",
|
||||
"auto": "Automatisch (System-Standard)",
|
||||
"autoWithTarget": "Automatisch ({name})",
|
||||
"integrated": "Integriert — {name}",
|
||||
"discrete": "Dediziert — {name}",
|
||||
"software": "Software (CPU, llvmpipe)"
|
||||
},
|
||||
"recovery": {
|
||||
"title": "Letzter Start während Grafik-Init abgestürzt",
|
||||
"titleLocked": "Wegen wiederholter Grafikabstürze deaktiviert",
|
||||
"hwAccelDisabled": "Hardware-Beschleunigung wurde zurückgesetzt.",
|
||||
"dmabufDisabled": "DMA-BUF-Renderer wurde zurückgesetzt.",
|
||||
"gpuPinDisabled": "GPU-Auswahl wurde auf Automatisch zurückgesetzt.",
|
||||
"tryAgain": "Erneut versuchen",
|
||||
"cleared": "Absturzmarkierung gelöscht. Aktiviere die Einstellung erneut, wenn du es nochmal versuchen möchtest."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2836,6 +2836,25 @@
|
||||
"dismiss": "Dismiss",
|
||||
"redetect": "Detect recommended settings",
|
||||
"noChanges": "Settings already match the recommendation for this system."
|
||||
},
|
||||
"restartRequired": "Restart QBZ for the change to take effect.",
|
||||
"gpu": {
|
||||
"label": "Rendering GPU",
|
||||
"desc": "Pin WebKit to a specific GPU stack. Restart required. Auto lets the system pick.",
|
||||
"auto": "Auto (system default)",
|
||||
"autoWithTarget": "Auto ({name})",
|
||||
"integrated": "Integrated — {name}",
|
||||
"discrete": "Discrete — {name}",
|
||||
"software": "Software (CPU, llvmpipe)"
|
||||
},
|
||||
"recovery": {
|
||||
"title": "Last launch crashed during graphics init",
|
||||
"titleLocked": "Disabled after repeated graphics crashes",
|
||||
"hwAccelDisabled": "Hardware acceleration was reverted.",
|
||||
"dmabufDisabled": "DMA-BUF renderer was reverted.",
|
||||
"gpuPinDisabled": "Rendering GPU preference was reset to Auto.",
|
||||
"tryAgain": "Try again",
|
||||
"cleared": "Crash flag cleared. Re-enable the setting in Settings if you want to retry."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2836,6 +2836,25 @@
|
||||
"dismiss": "Descartar",
|
||||
"redetect": "Detectar configuracion recomendada",
|
||||
"noChanges": "La configuracion actual ya coincide con la recomendacion para este sistema."
|
||||
},
|
||||
"restartRequired": "Reinicia QBZ para que el cambio surta efecto.",
|
||||
"gpu": {
|
||||
"label": "GPU de renderizado",
|
||||
"desc": "Fija WebKit a una stack GPU especifica. Reinicio requerido. Auto deja que el sistema decida.",
|
||||
"auto": "Auto (predeterminado del sistema)",
|
||||
"autoWithTarget": "Auto ({name})",
|
||||
"integrated": "Integrada — {name}",
|
||||
"discrete": "Dedicada — {name}",
|
||||
"software": "Software (CPU, llvmpipe)"
|
||||
},
|
||||
"recovery": {
|
||||
"title": "El arranque anterior fallo durante el init de graficos",
|
||||
"titleLocked": "Deshabilitado tras varios fallos de graficos",
|
||||
"hwAccelDisabled": "Se revertio la aceleracion por hardware.",
|
||||
"dmabufDisabled": "Se revertio el renderizador DMA-BUF.",
|
||||
"gpuPinDisabled": "La preferencia de GPU se reseteo a Auto.",
|
||||
"tryAgain": "Reintentar",
|
||||
"cleared": "Marca de fallo limpiada. Reactiva el setting en Settings si quieres reintentar."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2830,6 +2830,25 @@
|
||||
"dismiss": "Ignorer",
|
||||
"redetect": "Détecter les paramètres recommandés",
|
||||
"noChanges": "Les paramètres correspondent déjà à la recommandation pour ce système."
|
||||
},
|
||||
"restartRequired": "Redémarrez QBZ pour que le changement prenne effet.",
|
||||
"gpu": {
|
||||
"label": "GPU de rendu",
|
||||
"desc": "Forcer WebKit sur une pile GPU spécifique. Redémarrage requis. Auto laisse le système décider.",
|
||||
"auto": "Auto (par défaut)",
|
||||
"autoWithTarget": "Auto ({name})",
|
||||
"integrated": "Intégré — {name}",
|
||||
"discrete": "Dédié — {name}",
|
||||
"software": "Logiciel (CPU, llvmpipe)"
|
||||
},
|
||||
"recovery": {
|
||||
"title": "Le démarrage précédent a planté pendant l'init graphique",
|
||||
"titleLocked": "Désactivé après plusieurs plantages graphiques",
|
||||
"hwAccelDisabled": "L'accélération matérielle a été désactivée.",
|
||||
"dmabufDisabled": "Le moteur DMA-BUF a été désactivé.",
|
||||
"gpuPinDisabled": "La préférence GPU a été réinitialisée sur Auto.",
|
||||
"tryAgain": "Réessayer",
|
||||
"cleared": "Indicateur de plantage effacé. Réactivez le paramètre dans Settings pour réessayer."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2830,6 +2830,25 @@
|
||||
"dismiss": "Descartar",
|
||||
"redetect": "Detectar configuracoes recomendadas",
|
||||
"noChanges": "As configuracoes ja correspondem a recomendacao para este sistema."
|
||||
},
|
||||
"restartRequired": "Reinicie o QBZ para que a mudanca tenha efeito.",
|
||||
"gpu": {
|
||||
"label": "GPU de renderizacao",
|
||||
"desc": "Fixar o WebKit em uma stack GPU especifica. Necessario reiniciar. Auto deixa o sistema decidir.",
|
||||
"auto": "Auto (padrao do sistema)",
|
||||
"autoWithTarget": "Auto ({name})",
|
||||
"integrated": "Integrada — {name}",
|
||||
"discrete": "Dedicada — {name}",
|
||||
"software": "Software (CPU, llvmpipe)"
|
||||
},
|
||||
"recovery": {
|
||||
"title": "O ultimo arranque travou durante o init grafico",
|
||||
"titleLocked": "Desativado apos travamentos graficos repetidos",
|
||||
"hwAccelDisabled": "A aceleracao de hardware foi revertida.",
|
||||
"dmabufDisabled": "O renderer DMA-BUF foi revertido.",
|
||||
"gpuPinDisabled": "A preferencia de GPU foi resetada para Auto.",
|
||||
"tryAgain": "Tentar novamente",
|
||||
"cleared": "Sinalizador de travamento limpo. Reative a configuracao em Settings se quiser tentar novamente."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4734,6 +4734,16 @@
|
||||
// Bootstrap app (theme, mouse nav, Last.fm restore)
|
||||
const { cleanup: cleanupBootstrap } = bootstrapApp();
|
||||
|
||||
// Tell the boot watchdog that the UI mounted successfully. This
|
||||
// clears the pending-graphics-init marker the backend wrote at
|
||||
// startup; if we never reach this line (WebKit crashed during
|
||||
// graphics init) the next launch sees the stale marker and
|
||||
// auto-reverts the offending risky setting. Idempotent and safe to
|
||||
// call on every mount — backend just removes the file if present.
|
||||
void invoke('v2_mark_boot_succeeded').catch((err) => {
|
||||
console.warn('[BootWatchdog] mark_boot_succeeded failed:', err);
|
||||
});
|
||||
|
||||
void initDiscordRpc();
|
||||
|
||||
// Window-title preference: load from localStorage and subscribe so that
|
||||
|
||||
Reference in new issue
Block a user