feat(settings): surface graphics recommendation banner in the Graphics tab

The detection + decision matrix in autoconfig_graphics.rs already knew
the right combination for every (GPU vendor x display server x desktop)
case, but the only way to consume it was the qbz --autoconfig-graphics
CLI tool which most users never run. The recommendation sat invisible
while users hunted toggles by hand.

Lifts the same logic into the Settings UI as a persistent banner at
the top of the new Graphics tab:

  - autoconfig_graphics: Environment and Recommendation structs become
    public with Serialize, the three pieces (detect_environment,
    compute_recommendation, write_recommendation) become pub so they
    can be re-used. The existing apply_recommendation CLI helper now
    delegates writes to the shared write_recommendation, keeping the
    behaviour identical (and inverting the DMA-BUF mapping so that
    rec.disable_dmabuf=false now writes force_dmabuf=true, matching
    the 1.2.13 opt-in semantics).

  - commands_v2/graphics_recommendation.rs (new): two thin Tauri
    commands — v2_get_graphics_recommendation returns the environment
    plus the computed recommendation so the frontend can compare it
    to persisted settings; v2_apply_graphics_recommendation writes
    the whole bundle in one call.

  - Settings UI: the Graphics tab loads the recommendation on mount.
    A $derived computes whether at least one recommended value
    diverges from the four persisted settings (HW accel / force_x11 /
    gsk_renderer / force_dmabuf with the inverted DMA-BUF axis).
    When it does, a violet-accent banner appears above the profile
    grid showing the detected environment, the rationale lines from
    the matrix, and an Apply button. Persistent (no dismiss) — vanishes
    the moment the persisted state matches the recommendation, no
    second copy of state to keep in sync.

  - i18n: 4 new keys per locale (title / detected / apply / applied)
    across en/es/de/fr/pt. The rationale strings stay in English
    because they're hardcoded in autoconfig_graphics.rs and reword
    them per scenario would mean a structured i18n key per case —
    not worth it for diagnostic copy that's read once and dismissed.

cc
This commit is contained in:
vicrodh committed 2026-05-13 16:46:29 -06:00
1 parent 11721594f4
commit 700d5ef2c6
10 files changed
+308 -43

No files matched your search

+63 -43
View File
@@ -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<String>,
disable_dmabuf: bool,
disable_blur_background: bool,
rationale: Vec<String>,
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct Recommendation {
pub hardware_acceleration: bool,
pub force_x11: bool,
pub gsk_renderer: Option<String>,
pub disable_dmabuf: bool,
pub disable_blur_background: bool,
pub rationale: Vec<String>,
}
/// 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<String>> {
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,14 +421,10 @@ 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.
let desired_force_dmabuf = !rec.disable_dmabuf;
match crate::config::developer_settings::DeveloperSettingsStore::new() {
Ok(store) => {
if let Err(e) = store.set_force_dmabuf(false) {
if let Err(e) = store.set_force_dmabuf(desired_force_dmabuf) {
errors.push(format!("force_dmabuf: {}", e));
}
}
@@ -396,26 +432,10 @@ fn apply_recommendation(rec: &Recommendation) {
errors.push(format!("developer settings store: {}", 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."
);
}
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)
}
}
@@ -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<String>, Vec<String>> {
let environment = detect_environment();
let recommendation = compute_recommendation(&environment);
match write_recommendation(&recommendation) {
Ok(()) => Ok(Vec::new()),
Err(errors) => Err(errors),
}
}
+3
View File
@@ -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::*;
+2
View File
@@ -1656,6 +1656,8 @@ 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_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,
@@ -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<GraphicsRecommendation | null>(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<GraphicsRecommendation>('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<string[]>('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<any>('v2_get_graphics_settings');
forceX11 = gs.force_x11;
gskRenderer = gs.gsk_renderer || '';
hardwareAcceleration = gs.hardware_acceleration;
const ds = await invoke<any>('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 @@
<h3 class="section-title">{$t('settings.graphics.title')}</h3>
<p class="section-note">{$t('settings.graphics.helpText')}</p>
{#if graphicsRecommendation && recommendationDiffers}
<div class="recommendation-banner">
<div class="recommendation-info">
<span class="recommendation-title">{$t('settings.graphics.recommendation.title')}</span>
<span class="recommendation-detected">
{$t('settings.graphics.recommendation.detected')}: {buildEnvironmentSummary(graphicsRecommendation.environment)}
</span>
{#if graphicsRecommendation.recommendation.rationale.length > 0}
<ul class="recommendation-rationale">
{#each graphicsRecommendation.recommendation.rationale as line}
<li>{line}</li>
{/each}
</ul>
{/if}
</div>
<div class="recommendation-actions">
<button class="primary-btn" onclick={handleApplyGraphicsRecommendation}>
{$t('settings.graphics.recommendation.apply')}
</button>
</div>
</div>
{/if}
{#if graphicsUsingFallback}
<div class="composition-warning fallback-warning">
<TriangleAlert size={14} />
@@ -7019,6 +7128,58 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
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);
+6
View File
@@ -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."
}
}
},
+6
View File
@@ -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."
}
}
},
+6
View File
@@ -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."
}
}
},
+6
View File
@@ -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."
}
}
},
+6
View File
@@ -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."
}
}
},