diff --git a/src-tauri/src/autoconfig_graphics.rs b/src-tauri/src/autoconfig_graphics.rs index 707151f19..4968a06e1 100644 --- a/src-tauri/src/autoconfig_graphics.rs +++ b/src-tauri/src/autoconfig_graphics.rs @@ -3,30 +3,39 @@ //! Detects the current GPU, display server, desktop environment, and compositor, //! then recommends and applies optimal graphics settings. //! -//! Usage: `qbz --autoconfig-graphics` +//! Two entry points: +//! - `run()` — CLI prompt invoked via `qbz --autoconfig-graphics` +//! - `detect_environment()` / `compute_recommendation()` / `apply_recommendation()` +//! — public helpers consumed by V2 commands that surface the recommendation +//! inside the Settings UI (issue #315 follow-up) use crate::config::graphics_settings::GraphicsSettingsStore; +use serde::Serialize; use std::io::{self, BufRead, Write}; /// Detected environment information -struct Environment { - display_server: String, - gpu_nvidia: bool, - gpu_amd: bool, - gpu_intel: bool, - gpu_name: String, - desktop: String, - is_vm: bool, +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct Environment { + pub display_server: String, + pub gpu_nvidia: bool, + pub gpu_amd: bool, + pub gpu_intel: bool, + pub gpu_name: String, + pub desktop: String, + pub is_vm: bool, } /// Recommended configuration -struct Recommendation { - hardware_acceleration: bool, - force_x11: bool, - gsk_renderer: Option, - disable_dmabuf: bool, - disable_blur_background: bool, - rationale: Vec, +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct Recommendation { + pub hardware_acceleration: bool, + pub force_x11: bool, + pub gsk_renderer: Option, + pub disable_dmabuf: bool, + pub disable_blur_background: bool, + pub rationale: Vec, } /// Run the autoconfig-graphics CLI tool. @@ -57,7 +66,7 @@ pub fn run() { } } -fn detect_environment() -> Environment { +pub fn detect_environment() -> Environment { let display_server = detect_display_server(); let gpu_nvidia = is_nvidia_gpu(); let gpu_amd = is_amd_gpu(); @@ -200,7 +209,7 @@ fn print_environment(env: &Environment) { eprintln!(); } -fn compute_recommendation(env: &Environment) -> Recommendation { +pub fn compute_recommendation(env: &Environment) -> Recommendation { let mut rationale = Vec::new(); // VM: software rendering, no blur @@ -361,9 +370,40 @@ fn print_recommendation(rec: &Recommendation) { } fn apply_recommendation(rec: &Recommendation) { + match write_recommendation(rec) { + Ok(()) => { + if rec.disable_blur_background { + eprintln!( + "[QBZ AutoConfig] Note: blur background will be disabled. You can toggle this in Settings > Appearance." + ); + } + eprintln!(); + eprintln!("[QBZ AutoConfig] Configuration applied successfully."); + eprintln!("[QBZ AutoConfig] Restart QBZ to take effect."); + } + Err(errors) => { + eprintln!(); + eprintln!("[QBZ AutoConfig] Some settings could not be applied:"); + for e in &errors { + eprintln!(" - {}", e); + } + } + } +} + +/// Apply the recommendation to the persistence layer. Shared between the CLI +/// prompt (`apply_recommendation`) and the V2 command surface. Returns the +/// list of write errors so the caller can surface them appropriately (stderr +/// for CLI, frontend toast for the Settings UI). +/// +/// DMA-BUF semantics after the 1.2.13 opt-in flip: +/// - `rec.disable_dmabuf = true` → force_dmabuf = false (matches default; +/// runtime keeps DMA-BUF off). +/// - `rec.disable_dmabuf = false` → force_dmabuf = true (user opts in via +/// this recommendation; runtime turns DMA-BUF on). +pub fn write_recommendation(rec: &Recommendation) -> Result<(), Vec> { let mut errors = Vec::new(); - // Apply graphics settings to DB match GraphicsSettingsStore::new() { Ok(store) => { if let Err(e) = store.set_hardware_acceleration(rec.hardware_acceleration) { @@ -381,41 +421,21 @@ fn apply_recommendation(rec: &Recommendation) { } } - // Apply developer settings (force_dmabuf) - if rec.disable_dmabuf { - // Note: we don't set force_dmabuf=true here because the default behavior - // already disables DMA-BUF for NVIDIA+Wayland. We only reset it to ensure - // it's not forced ON. - match crate::config::developer_settings::DeveloperSettingsStore::new() { - Ok(store) => { - if let Err(e) = store.set_force_dmabuf(false) { - errors.push(format!("force_dmabuf: {}", e)); - } - } - Err(e) => { - errors.push(format!("developer settings store: {}", e)); + let desired_force_dmabuf = !rec.disable_dmabuf; + match crate::config::developer_settings::DeveloperSettingsStore::new() { + Ok(store) => { + if let Err(e) = store.set_force_dmabuf(desired_force_dmabuf) { + errors.push(format!("force_dmabuf: {}", e)); } } - } - - // Apply blur background setting to localStorage config file - // Note: This is stored in localStorage by the frontend, so we write a hint - // that the frontend will read on next startup - if rec.disable_blur_background { - eprintln!( - "[QBZ AutoConfig] Note: blur background will be disabled. You can toggle this in Settings > Appearance." - ); + Err(e) => { + errors.push(format!("developer settings store: {}", e)); + } } if errors.is_empty() { - eprintln!(); - eprintln!("[QBZ AutoConfig] Configuration applied successfully."); - eprintln!("[QBZ AutoConfig] Restart QBZ to take effect."); + Ok(()) } else { - eprintln!(); - eprintln!("[QBZ AutoConfig] Some settings could not be applied:"); - for e in &errors { - eprintln!(" - {}", e); - } + Err(errors) } } diff --git a/src-tauri/src/commands_v2/graphics_recommendation.rs b/src-tauri/src/commands_v2/graphics_recommendation.rs new file mode 100644 index 000000000..78dce84d9 --- /dev/null +++ b/src-tauri/src/commands_v2/graphics_recommendation.rs @@ -0,0 +1,49 @@ +//! V2 commands that expose the graphics auto-config recommendation to the +//! Settings UI. The detection + decision logic lives in +//! `autoconfig_graphics.rs` (shared with the `qbz --autoconfig-graphics` CLI +//! tool); this module is just the Tauri surface that the Graphics tab uses +//! to render the "Detected / Recommended" banner. + +use crate::autoconfig_graphics::{ + compute_recommendation, detect_environment, write_recommendation, Environment, Recommendation, +}; +use serde::Serialize; + +/// Payload returned to the Settings UI. Splits environment from +/// recommendation so the frontend can show a human-readable detection +/// line and then compare the recommendation to the user's current +/// persisted settings to decide whether to surface the banner at all. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct GraphicsRecommendationPayload { + pub environment: Environment, + pub recommendation: Recommendation, +} + +/// Compute the recommendation for the current host. Cheap — runs the +/// same detection that the CLI tool runs and synthesizes the matrix +/// decision. The Settings UI calls this on Graphics-tab mount. +#[tauri::command] +pub fn v2_get_graphics_recommendation() -> GraphicsRecommendationPayload { + let environment = detect_environment(); + let recommendation = compute_recommendation(&environment); + GraphicsRecommendationPayload { + environment, + recommendation, + } +} + +/// Write the current recommendation to the persistence layer. +/// `force_dmabuf` is derived from `disable_dmabuf` per the 1.2.13 +/// opt-in semantics (see `write_recommendation` in autoconfig_graphics). +/// Returns the list of per-field errors so the UI can surface them. +/// Empty `Ok(Vec)` means "everything written, restart required". +#[tauri::command] +pub fn v2_apply_graphics_recommendation() -> Result, Vec> { + let environment = detect_environment(); + let recommendation = compute_recommendation(&environment); + match write_recommendation(&recommendation) { + Ok(()) => Ok(Vec::new()), + Err(errors) => Err(errors), + } +} diff --git a/src-tauri/src/commands_v2/mod.rs b/src-tauri/src/commands_v2/mod.rs index 675edb51d..33b6f1ae8 100644 --- a/src-tauri/src/commands_v2/mod.rs +++ b/src-tauri/src/commands_v2/mod.rs @@ -23,6 +23,9 @@ pub struct V2PlaylistSuggestionsInput { pub(crate) mod helpers; pub use helpers::*; +mod graphics_recommendation; +pub use graphics_recommendation::*; + mod runtime; pub use runtime::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cf56269b2..cb9a787b8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1656,6 +1656,8 @@ pub fn run(qconnect_cli_override: Option) { commands_v2::v2_set_gdk_scale, commands_v2::v2_set_gdk_dpi_scale, commands_v2::v2_set_gsk_renderer, + commands_v2::v2_get_graphics_recommendation, + commands_v2::v2_apply_graphics_recommendation, commands_v2::v2_clear_cache, commands_v2::v2_clear_artist_cache, commands_v2::v2_get_vector_store_stats, diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index d736ee7bc..35eb91f5a 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -389,6 +389,34 @@ let gdkScale = $state(''); let gdkDpiScale = $state(''); let gskRenderer = $state(''); + + // Graphics recommendation surfaced in the Graphics tab. Populated on + // mount via v2_get_graphics_recommendation (matrix lives in + // autoconfig_graphics.rs). The banner fires only when at least one + // recommended value differs from the currently persisted setting; the + // "Apply" button writes the entire recommendation via + // v2_apply_graphics_recommendation. + type GraphicsRecommendation = { + environment: { + display_server: string; + gpu_nvidia: boolean; + gpu_amd: boolean; + gpu_intel: boolean; + gpu_name: string; + desktop: string; + is_vm: boolean; + }; + recommendation: { + hardware_acceleration: boolean; + force_x11: boolean; + gsk_renderer: string | null; + disable_dmabuf: boolean; + disable_blur_background: boolean; + rationale: string[]; + }; + }; + let graphicsRecommendation = $state(null); + // Graphics startup status (for showing degraded mode warning) let graphicsUsingFallback = $state(false); let graphicsIsWayland = $state(false); @@ -1809,6 +1837,17 @@ graphicsHwAccelEnabled = status.hardware_accel_enabled; }).catch(() => {}); + // Linux-only: recommendation banner in the Graphics tab + if (platform === 'linux') { + invoke('v2_get_graphics_recommendation') + .then((payload) => { + graphicsRecommendation = payload; + }) + .catch((err) => { + console.warn('[Settings] graphics recommendation unavailable:', err); + }); + } + // Subscribe to offline state changes const unsubscribeOffline = subscribeOffline(() => { offlineStatus = getOfflineStatus(); @@ -4064,6 +4103,53 @@ } } + // 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` + // (1.2.13 opt-in semantics), so the two booleans differ iff + // `disable_dmabuf === force_dmabuf` (same value means mismatch given + // the inversion). + const recommendationDiffers = $derived.by(() => { + if (!graphicsRecommendation) return false; + const rec = graphicsRecommendation.recommendation; + if (rec.hardware_acceleration !== hardwareAcceleration) return true; + if (rec.force_x11 !== forceX11) return true; + if (rec.disable_dmabuf === forceDmabuf) return true; + if ((rec.gsk_renderer ?? '') !== gskRenderer) return true; + return false; + }); + + async function handleApplyGraphicsRecommendation() { + try { + const errors = await invoke('v2_apply_graphics_recommendation'); + if (errors && errors.length > 0) { + console.error('Failed to apply some graphics settings:', errors); + showToast(errors.join('; '), 'error'); + return; + } + // Refresh local state from DB so the toggles snap to the new values + const gs = await invoke('v2_get_graphics_settings'); + forceX11 = gs.force_x11; + gskRenderer = gs.gsk_renderer || ''; + hardwareAcceleration = gs.hardware_acceleration; + const ds = await invoke('v2_get_developer_settings'); + forceDmabuf = ds.force_dmabuf; + showToast($t('settings.graphics.recommendation.applied'), 'success'); + } catch (err) { + console.error('Failed to apply graphics recommendation:', err); + showToast(String(err), 'error'); + } + } + + function buildEnvironmentSummary(env: GraphicsRecommendation['environment']): string { + const parts: string[] = []; + if (env.gpu_name) parts.push(env.gpu_name); + if (env.display_server) parts.push(env.display_server); + if (env.desktop && env.desktop !== 'Unknown') parts.push(env.desktop); + if (env.is_vm) parts.push('VM'); + return parts.join(' / '); + } + const GSK_RENDERER_KEYS = ['', 'gl', 'ngl', 'vulkan', 'cairo'] as const; function getGskRendererOptions(): string[] { @@ -5217,6 +5303,29 @@

{$t('settings.graphics.title')}

{$t('settings.graphics.helpText')}

+ {#if graphicsRecommendation && recommendationDiffers} +
+
+ {$t('settings.graphics.recommendation.title')} + + {$t('settings.graphics.recommendation.detected')}: {buildEnvironmentSummary(graphicsRecommendation.environment)} + + {#if graphicsRecommendation.recommendation.rationale.length > 0} +
    + {#each graphicsRecommendation.recommendation.rationale as line} +
  • {line}
  • + {/each} +
+ {/if} +
+
+ +
+
+ {/if} + {#if graphicsUsingFallback}
@@ -7019,6 +7128,58 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz margin-bottom: 2px; } + /* Graphics recommendation banner. Shown at the top of the Graphics + section when the autoconfig recommendation diverges from the + persisted settings. Persistent (no dismiss) so the user knows the + option is available; disappears the moment recommendation == state. */ + .recommendation-banner { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 14px 16px; + margin-bottom: 16px; + border-radius: 8px; + background: var(--accent-primary-tint, rgba(124, 58, 237, 0.08)); + border: 1px solid var(--accent-primary, rgba(124, 58, 237, 0.3)); + } + + .recommendation-info { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 0; + } + + .recommendation-title { + font-weight: 600; + color: var(--text-primary); + font-size: 13px; + } + + .recommendation-detected { + font-size: 12px; + color: var(--text-secondary); + } + + .recommendation-rationale { + margin: 4px 0 0; + padding-left: 18px; + list-style: disc; + font-size: 12px; + color: var(--text-secondary); + } + + .recommendation-rationale li { + margin: 1px 0; + } + + .recommendation-actions { + display: flex; + align-items: flex-start; + flex-shrink: 0; + } + .fallback-desc { display: block; color: var(--text-secondary); diff --git a/src/lib/i18n/locales/de.json b/src/lib/i18n/locales/de.json index 109e4a671..82bf394e0 100644 --- a/src/lib/i18n/locales/de.json +++ b/src/lib/i18n/locales/de.json @@ -2821,6 +2821,12 @@ "maxPerformanceLabel": "Maximale Leistung", "maxPerformanceDesc": "Software-Rendering (Cairo) mit deaktiviertem Blur. Maximale Kompatibilitaet, niedrigste GPU-Nutzung. Verwenden bei Ruckeln oder Abstuerzen.", "applied": "Profil angewendet: {profile}. QBZ neu starten, damit es wirksam wird." + }, + "recommendation": { + "title": "Empfohlen für dein System", + "detected": "Erkannt", + "apply": "Übernehmen", + "applied": "Konfiguration übernommen. QBZ neu starten, damit es wirksam wird." } } }, diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json index a02235aed..926c0c02e 100644 --- a/src/lib/i18n/locales/en.json +++ b/src/lib/i18n/locales/en.json @@ -2827,6 +2827,12 @@ "maxPerformanceLabel": "Max Performance", "maxPerformanceDesc": "Software rendering (Cairo) with blur disabled. Maximum compatibility, lowest GPU usage. Use if experiencing stutter or crashes.", "applied": "Applied profile: {profile}. Restart QBZ to take effect." + }, + "recommendation": { + "title": "Recommended for your system", + "detected": "Detected", + "apply": "Apply", + "applied": "Configuration applied. Restart QBZ to take effect." } } }, diff --git a/src/lib/i18n/locales/es.json b/src/lib/i18n/locales/es.json index 9703bbcfc..54526ab8f 100644 --- a/src/lib/i18n/locales/es.json +++ b/src/lib/i18n/locales/es.json @@ -2827,6 +2827,12 @@ "maxPerformanceLabel": "Rendimiento maximo", "maxPerformanceDesc": "Renderizado por software (Cairo) con blur desactivado. Maxima compatibilidad, menor uso de GPU. Usar si hay tartamudeo o crashes.", "applied": "Perfil aplicado: {profile}. Reinicia QBZ para que surta efecto." + }, + "recommendation": { + "title": "Recomendado para tu sistema", + "detected": "Detectado", + "apply": "Aplicar", + "applied": "Configuracion aplicada. Reinicia QBZ para que surta efecto." } } }, diff --git a/src/lib/i18n/locales/fr.json b/src/lib/i18n/locales/fr.json index 45dfc651c..0a614e3e2 100644 --- a/src/lib/i18n/locales/fr.json +++ b/src/lib/i18n/locales/fr.json @@ -2821,6 +2821,12 @@ "maxPerformanceLabel": "Performance maximale", "maxPerformanceDesc": "Rendu logiciel (Cairo) avec flou désactivé. Compatibilité maximale, utilisation GPU minimale. À utiliser en cas de saccades ou de plantages.", "applied": "Profil appliqué : {profile}. Redémarrez QBZ pour appliquer le changement." + }, + "recommendation": { + "title": "Recommandé pour votre système", + "detected": "Détecté", + "apply": "Appliquer", + "applied": "Configuration appliquée. Redémarrez QBZ pour qu'elle prenne effet." } } }, diff --git a/src/lib/i18n/locales/pt.json b/src/lib/i18n/locales/pt.json index 49e7b224d..80f675f06 100644 --- a/src/lib/i18n/locales/pt.json +++ b/src/lib/i18n/locales/pt.json @@ -2821,6 +2821,12 @@ "maxPerformanceLabel": "Desempenho Máximo", "maxPerformanceDesc": "Renderização por software (Cairo) com desfoque desativado. Máxima compatibilidade, menor uso de GPU. Use se tiver engasgos ou travamentos.", "applied": "Perfil aplicado: {profile}. Reinicie o QBZ para aplicar." + }, + "recommendation": { + "title": "Recomendado para o seu sistema", + "detected": "Detectado", + "apply": "Aplicar", + "applied": "Configuracao aplicada. Reinicie o QBZ para que tenha efeito." } } },