feat(shell): per-GPU selector, ambient scene in immersive, glass carousels/viewall
Smoke round 5 (owner feedback). Checkpoint.
- Preferred GPU selector (Settings > Renderer, Linux/wgpu): lists each detected
GPU by name ("Auto" + e.g. "NVIDIA … (discrete)" / "Intel … (integrated)"),
persisted (ui_prefs.gpu_power by device name), applied at startup via the wgpu
adapter PowerPreference (WGPU_POWER_PREF env still wins). Fixes the iGPU running
hot under the shader background on hybrid laptops — pick the discrete GPU.
New gpu_adapters() enumerates+dedups adapters; gpu_power_from_prefs /
_options / _seed_index / _value_for_index.
- Ambient metaball added to the IMMERSIVE scene picker (mode 7, wgpu-only opt-in);
the non-shader atmosphere stays the fallback for GL/software tiers.
- Glass treatment on the surfaces that were still opaque: carousel < > nav
buttons (6 carousels), "Filter by category" pill, "Filter by genre" buttons,
and the PlaylistBrowseView (View-all) header band + category radio row.
- i18n: "Preferred GPU" + description + discrete/integrated + restart toast (8
locales).
Base: 76ea5a82. Prior checkpoint: a0f5cf2b.
cc
This commit is contained in:
23 files changed
+341
-19
No files matched your search
@@ -4594,3 +4594,19 @@ msgstr "Dynamischer Hintergrund"
|
||||
|
||||
msgid "Animated album-art background behind the whole app. High resource use — GPU accelerated."
|
||||
msgstr "Animierter Album-Cover-Hintergrund hinter der gesamten App. Hoher Ressourcenverbrauch — GPU-beschleunigt."
|
||||
|
||||
# Preferred GPU (Renderer settings)
|
||||
msgid "Preferred GPU"
|
||||
msgstr "Bevorzugte GPU"
|
||||
|
||||
msgid "Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)"
|
||||
msgstr "Welche GPU die App rendert. Auf einem Hybrid-Laptop verlagert die Wahl der dedizierten GPU die Last des dynamischen Hintergrunds von der integrierten weg (kühler, aber höherer Stromverbrauch). Auto wird empfohlen (Neustart erforderlich)"
|
||||
|
||||
msgid "discrete"
|
||||
msgstr "dediziert"
|
||||
|
||||
msgid "integrated"
|
||||
msgstr "integriert"
|
||||
|
||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||
msgstr "Bevorzugte GPU geändert — QBZ neu starten, um die Änderung zu übernehmen"
|
||||
@@ -372,3 +372,19 @@ msgstr "Dynamic background"
|
||||
|
||||
msgid "Animated album-art background behind the whole app. High resource use — GPU accelerated."
|
||||
msgstr "Animated album-art background behind the whole app. High resource use — GPU accelerated."
|
||||
|
||||
# Preferred GPU (Renderer settings)
|
||||
msgid "Preferred GPU"
|
||||
msgstr "Preferred GPU"
|
||||
|
||||
msgid "Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)"
|
||||
msgstr "Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)"
|
||||
|
||||
msgid "discrete"
|
||||
msgstr "discrete"
|
||||
|
||||
msgid "integrated"
|
||||
msgstr "integrated"
|
||||
|
||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||
msgstr "Preferred GPU changed — restart QBZ to apply"
|
||||
@@ -4624,3 +4624,19 @@ msgstr "Fondo dinámico"
|
||||
|
||||
msgid "Animated album-art background behind the whole app. High resource use — GPU accelerated."
|
||||
msgstr "Fondo animado con la portada del álbum detrás de toda la app. Alto uso de recursos — acelerado por GPU."
|
||||
|
||||
# Preferred GPU (Renderer settings)
|
||||
msgid "Preferred GPU"
|
||||
msgstr "GPU preferida"
|
||||
|
||||
msgid "Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)"
|
||||
msgstr "Qué GPU renderiza la app. En una laptop híbrida, elegir la GPU dedicada mueve la carga del fondo dinámico fuera de la integrada (más fresca, pero con mayor consumo). Se recomienda Auto (requiere reinicio)"
|
||||
|
||||
msgid "discrete"
|
||||
msgstr "dedicada"
|
||||
|
||||
msgid "integrated"
|
||||
msgstr "integrada"
|
||||
|
||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||
msgstr "GPU preferida cambiada — reinicia QBZ para aplicar"
|
||||
@@ -4594,3 +4594,19 @@ msgstr "Arrière-plan dynamique"
|
||||
|
||||
msgid "Animated album-art background behind the whole app. High resource use — GPU accelerated."
|
||||
msgstr "Arrière-plan animé à partir de la pochette derrière toute l'application. Consommation de ressources élevée — accéléré par le GPU."
|
||||
|
||||
# Preferred GPU (Renderer settings)
|
||||
msgid "Preferred GPU"
|
||||
msgstr "GPU préféré"
|
||||
|
||||
msgid "Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)"
|
||||
msgstr "Quel GPU affiche l'application. Sur un portable hybride, choisir le GPU dédié déplace la charge de l'arrière-plan dynamique hors du GPU intégré (plus frais, mais plus gourmand en énergie). Auto est recommandé (redémarrage requis)"
|
||||
|
||||
msgid "discrete"
|
||||
msgstr "dédié"
|
||||
|
||||
msgid "integrated"
|
||||
msgstr "intégré"
|
||||
|
||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||
msgstr "GPU préféré modifié — redémarrez QBZ pour appliquer"
|
||||
@@ -4582,3 +4582,19 @@ msgstr "ダイナミック背景"
|
||||
|
||||
msgid "Animated album-art background behind the whole app. High resource use — GPU accelerated."
|
||||
msgstr "アプリ全体の背後にアルバムアートワークのアニメーション背景を表示します。リソース使用量が高く、GPUアクセラレーションを使用します。"
|
||||
|
||||
# Preferred GPU (Renderer settings)
|
||||
msgid "Preferred GPU"
|
||||
msgstr "優先GPU"
|
||||
|
||||
msgid "Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)"
|
||||
msgstr "どのGPUでアプリをレンダリングするかを選びます。ハイブリッド構成のノートPCでは、ディスクリートGPUを選ぶとダイナミック背景の負荷を内蔵GPUから移せます(温度は下がりますが消費電力は増えます)。自動を推奨します(再起動が必要)"
|
||||
|
||||
msgid "discrete"
|
||||
msgstr "ディスクリート"
|
||||
|
||||
msgid "integrated"
|
||||
msgstr "内蔵"
|
||||
|
||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||
msgstr "優先GPUを変更しました — 適用するには QBZ を再起動してください"
|
||||
@@ -4606,3 +4606,19 @@ msgstr "Dynamische achtergrond"
|
||||
|
||||
msgid "Animated album-art background behind the whole app. High resource use — GPU accelerated."
|
||||
msgstr "Geanimeerde albumhoes-achtergrond achter de hele app. Hoog verbruik van systeembronnen — GPU-versneld."
|
||||
|
||||
# Preferred GPU (Renderer settings)
|
||||
msgid "Preferred GPU"
|
||||
msgstr "Voorkeurs-GPU"
|
||||
|
||||
msgid "Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)"
|
||||
msgstr "Welke GPU de app rendert. Op een hybride laptop verplaatst het kiezen van de dedicated GPU de belasting van de dynamische achtergrond weg van de geïntegreerde GPU (koeler, maar meer stroomverbruik). Automatisch wordt aanbevolen (vereist herstart)"
|
||||
|
||||
msgid "discrete"
|
||||
msgstr "dedicated"
|
||||
|
||||
msgid "integrated"
|
||||
msgstr "geïntegreerd"
|
||||
|
||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||
msgstr "Voorkeurs-GPU gewijzigd — herstart QBZ om toe te passen"
|
||||
@@ -4594,3 +4594,19 @@ msgstr "Fundo dinâmico"
|
||||
|
||||
msgid "Animated album-art background behind the whole app. High resource use — GPU accelerated."
|
||||
msgstr "Fundo animado com a capa do álbum atrás de toda a aplicação. Uso elevado de recursos — acelerado por GPU."
|
||||
|
||||
# Preferred GPU (Renderer settings)
|
||||
msgid "Preferred GPU"
|
||||
msgstr "GPU preferida"
|
||||
|
||||
msgid "Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)"
|
||||
msgstr "Qual GPU renderiza o app. Em um laptop híbrido, escolher a GPU dedicada transfere a carga do fundo dinâmico para fora da integrada (mais fria, mas com maior consumo). Auto é recomendado (requer reinicialização)"
|
||||
|
||||
msgid "discrete"
|
||||
msgstr "dedicada"
|
||||
|
||||
msgid "integrated"
|
||||
msgstr "integrada"
|
||||
|
||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||
msgstr "GPU preferida alterada — reinicie o QBZ para aplicar"
|
||||
@@ -4608,3 +4608,19 @@ msgstr "Динамический фон"
|
||||
|
||||
msgid "Animated album-art background behind the whole app. High resource use — GPU accelerated."
|
||||
msgstr "Анимированный фон из обложки альбома за всем приложением. Высокое потребление ресурсов — аппаратное ускорение GPU."
|
||||
|
||||
# Preferred GPU (Renderer settings)
|
||||
msgid "Preferred GPU"
|
||||
msgstr "Предпочитаемый GPU"
|
||||
|
||||
msgid "Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)"
|
||||
msgstr "Какой GPU отрисовывает приложение. На гибридном ноутбуке выбор дискретного GPU переносит нагрузку динамического фона со встроенного (меньше нагрев, но выше энергопотребление). Рекомендуется Авто (требуется перезапуск)"
|
||||
|
||||
msgid "discrete"
|
||||
msgstr "дискретный"
|
||||
|
||||
msgid "integrated"
|
||||
msgstr "встроенный"
|
||||
|
||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||
msgstr "Предпочитаемый GPU изменён — перезапустите QBZ, чтобы применить"
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { SlimItem, ShellState } from "../state.slint";
|
||||
import { SlimItem, ShellState, AppearanceState } from "../state.slint";
|
||||
import { ArtistCard } from "ArtistCard.slint";
|
||||
import { ArtistGridCard } from "ArtistGridCard.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
@@ -20,7 +20,9 @@ component NavButton inherits Rectangle {
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
background: ta.has-hover && root.enabled
|
||||
? Theme.surface-hover
|
||||
: Theme.surface-elevated;
|
||||
: (ShellState.app-background-active
|
||||
? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha)
|
||||
: Theme.surface-elevated);
|
||||
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
|
||||
@@ -108,7 +108,7 @@ export component GenreButton inherits Rectangle {
|
||||
border-radius: 6px;
|
||||
background: root.active
|
||||
? Theme.accent
|
||||
: (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
|
||||
: (ta.has-hover ? Theme.surface-hover : (ShellState.app-background-active ? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha) : Theme.surface-elevated));
|
||||
|
||||
genre-row := HorizontalLayout {
|
||||
padding-left: 12px;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { DiscoverSection, ShellState } from "../state.slint";
|
||||
import { DiscoverSection, ShellState, AppearanceState } from "../state.slint";
|
||||
import { AlbumCard } from "AlbumCard.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { QbzSelect } from "../primitives/QbzSelect.slint";
|
||||
@@ -23,7 +23,9 @@ component NavButton inherits Rectangle {
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
background: ta.has-hover && root.enabled
|
||||
? Theme.surface-hover
|
||||
: Theme.surface-elevated;
|
||||
: (ShellState.app-background-active
|
||||
? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha)
|
||||
: Theme.surface-elevated);
|
||||
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
|
||||
@@ -86,7 +86,7 @@ component GenreButton inherits Rectangle {
|
||||
border-radius: 6px;
|
||||
background: root.active
|
||||
? Theme.accent
|
||||
: (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
|
||||
: (ta.has-hover ? Theme.surface-hover : (ShellState.app-background-active ? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha) : Theme.surface-elevated));
|
||||
|
||||
genre-row := HorizontalLayout {
|
||||
padding-left: 12px;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { PinnedItem, ShellState } from "../state.slint";
|
||||
import { PinnedItem, ShellState, AppearanceState } from "../state.slint";
|
||||
import { AlbumCard } from "AlbumCard.slint";
|
||||
import { ArtistGridCard } from "ArtistGridCard.slint";
|
||||
import { PlaylistCard } from "PlaylistCard.slint";
|
||||
@@ -24,7 +24,9 @@ component NavButton inherits Rectangle {
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
background: ta.has-hover && root.enabled
|
||||
? Theme.surface-hover
|
||||
: Theme.surface-elevated;
|
||||
: (ShellState.app-background-active
|
||||
? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha)
|
||||
: Theme.surface-elevated);
|
||||
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { PlaylistBrowseState, PlaylistBrowseActions, GenreFilterState, GenreFilterActions, NavState, ShellState } from "../state.slint";
|
||||
import { PlaylistBrowseState, PlaylistBrowseActions, GenreFilterState, GenreFilterActions, NavState, ShellState, AppearanceState } from "../state.slint";
|
||||
import { BrowseSearch, GenreButton, ViewModeToggle } from "BrowseHeaderTools.slint";
|
||||
import { GenreFilterPopup } from "GenreFilterPopup.slint";
|
||||
import { PlaylistCard } from "PlaylistCard.slint";
|
||||
@@ -90,7 +90,7 @@ export component PlaylistBrowseView inherits Rectangle {
|
||||
// RIGHT at the 32px gutter. Only the list below scrolls.
|
||||
Rectangle {
|
||||
height: 56px;
|
||||
background: Theme.surface-main;
|
||||
background: ShellState.app-background-active ? Theme.surface-main.with-alpha(AppearanceState.app-background-bar-alpha) : Theme.surface-main;
|
||||
|
||||
// Left cluster — nav buttons then the title, inline.
|
||||
HorizontalLayout {
|
||||
@@ -154,7 +154,7 @@ export component PlaylistBrowseView inherits Rectangle {
|
||||
// row; overflow scrolls horizontally inside its own Flickable.
|
||||
if PlaylistBrowseState.tags.length > 0: Rectangle {
|
||||
height: 36px;
|
||||
background: Theme.surface-main;
|
||||
background: ShellState.app-background-active ? Theme.surface-main.with-alpha(AppearanceState.app-background-bar-alpha) : Theme.surface-main;
|
||||
Flickable {
|
||||
x: 32px;
|
||||
width: parent.width - 64px;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { SearchPlaylistItem, ShellState } from "../state.slint";
|
||||
import { SearchPlaylistItem, ShellState, AppearanceState } from "../state.slint";
|
||||
import { PlaylistCard } from "PlaylistCard.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
@@ -19,7 +19,9 @@ component NavButton inherits Rectangle {
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
background: ta.has-hover && root.enabled
|
||||
? Theme.surface-hover
|
||||
: Theme.surface-elevated;
|
||||
: (ShellState.app-background-active
|
||||
? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha)
|
||||
: Theme.surface-elevated);
|
||||
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { HomeState, HomeActions, PlaylistTagItem } from "../state.slint";
|
||||
import { HomeState, HomeActions, PlaylistTagItem, ShellState, AppearanceState } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
// One checkbox row in the open list: a square check + the localized tag name.
|
||||
@@ -74,7 +74,7 @@ export component PlaylistTagFilter inherits Rectangle {
|
||||
height: 30px;
|
||||
border-radius: 6px;
|
||||
border-width: 0px;
|
||||
background: ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
||||
background: ta.has-hover ? Theme.surface-hover : (ShellState.app-background-active ? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha) : Theme.surface-elevated);
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: 10px;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { RadioStationItem } from "../state.slint";
|
||||
import { RadioStationItem, ShellState, AppearanceState } from "../state.slint";
|
||||
import { RadioCard } from "RadioCard.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
@@ -21,7 +21,9 @@ component NavButton inherits Rectangle {
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
background: ta.has-hover && root.enabled
|
||||
? Theme.surface-hover
|
||||
: Theme.surface-elevated;
|
||||
: (ShellState.app-background-active
|
||||
? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha)
|
||||
: Theme.surface-elevated);
|
||||
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { SlimItem } from "../state.slint";
|
||||
import { SlimItem, ShellState, AppearanceState } from "../state.slint";
|
||||
import { SlimCard } from "SlimCard.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
|
||||
@@ -23,7 +23,9 @@ component NavButton inherits Rectangle {
|
||||
opacity: root.enabled ? 1.0 : 0.4;
|
||||
background: ta.has-hover && root.enabled
|
||||
? Theme.surface-hover
|
||||
: Theme.surface-elevated;
|
||||
: (ShellState.app-background-active
|
||||
? Theme.surface-elevated.with-alpha(AppearanceState.app-background-surface-alpha)
|
||||
: Theme.surface-elevated);
|
||||
|
||||
QbzIcon {
|
||||
source: root.icon;
|
||||
|
||||
@@ -671,6 +671,19 @@ component ImmersiveViewMenuPopup inherits PopupWindow {
|
||||
root.picked();
|
||||
}
|
||||
}
|
||||
// Ambient metaball (mode 7) — the same album-colored amoeba as the
|
||||
// app-wide dynamic background, offered here as an opt-in immersive
|
||||
// scene for strong GPUs; the non-shader atmosphere stays the default
|
||||
// fallback for the GL/software tiers (shader-scenes-available gates it).
|
||||
if ImmersiveState.shader-scenes-available: ImmersiveMenuRow {
|
||||
icon: @image-url("../assets/icons/audio-lines.svg");
|
||||
label: @tr("Ambient");
|
||||
active: root.is-focus && root.sh == 7;
|
||||
clicked => {
|
||||
ImmersiveState.view-mode = 0; ImmersiveState.shader-mode = 7;
|
||||
root.picked();
|
||||
}
|
||||
}
|
||||
if ImmersiveState.shader-scenes-available: ImmersiveMenuRow {
|
||||
icon: @image-url("../assets/icons/chart-no-axes-column.svg");
|
||||
label: @tr("Spectral Ribbon");
|
||||
|
||||
@@ -1007,6 +1007,19 @@ export component AppearanceSettings inherits VerticalLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
if AppearanceState.renderer-setting-visible: SettingRow {
|
||||
label: @tr("Preferred GPU");
|
||||
description: @tr("Which GPU renders the app. On a hybrid laptop, picking the discrete GPU moves the dynamic-background load off the integrated one (cooler, but more power). Auto is recommended (requires restart)");
|
||||
QbzSelect {
|
||||
menu-width: 260px;
|
||||
options: AppearanceState.gpu-power-modes;
|
||||
current-index: AppearanceState.gpu-power-index;
|
||||
selected(i) => {
|
||||
AppearanceState.gpu-power-index = i;
|
||||
AppearanceState.appearance-select("gpu-power", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 40px; }
|
||||
}
|
||||
@@ -3215,6 +3215,10 @@ export global AppearanceState {
|
||||
in-out property <bool> renderer-setting-visible: false;
|
||||
in-out property <[string]> renderer-options: [@tr("Auto (recommended)"), @tr("GPU"), @tr("GPU (compatibility)"), @tr("Software")];
|
||||
in-out property <int> renderer-index: 0;
|
||||
// Preferred GPU: options built in Rust from the detected adapters
|
||||
// ("Auto" + each GPU by name); index 0 = Auto.
|
||||
in-out property <[string]> gpu-power-modes: [@tr("Auto (recommended)")];
|
||||
in-out property <int> gpu-power-index: 0;
|
||||
|
||||
// --- INTERFACE SIZE ----------------------------------------------------
|
||||
// Window scale-factor preset (XS 0.8 / Small 0.9 / Default 1.0 /
|
||||
|
||||
@@ -301,6 +301,14 @@ fn seed_tray_appearance(w: &AppWindow, tray: &tray_settings::TraySettings) {
|
||||
appearance.set_renderer_index(crate::ui_prefs::renderer_index(
|
||||
&crate::ui_prefs::load().renderer,
|
||||
));
|
||||
// Preferred GPU row (Linux only, shares the renderer-row visibility) —
|
||||
// options built from the real detected GPUs; index resolved from the
|
||||
// persisted device name / legacy key.
|
||||
appearance
|
||||
.set_gpu_power_modes(slint::ModelRc::new(slint::VecModel::from(gpu_power_options())));
|
||||
appearance.set_gpu_power_index(gpu_power_seed_index(
|
||||
&crate::ui_prefs::load().gpu_power,
|
||||
));
|
||||
// Interface-size row (all platforms). Same choke point as the renderer
|
||||
// row so the dropdown always reflects the persisted value.
|
||||
appearance.set_ui_scale_index(crate::ui_prefs::ui_scale_index(
|
||||
@@ -4129,6 +4137,9 @@ fn reseed_i18n_labels(window: &AppWindow) {
|
||||
t("Ambient"),
|
||||
t("Blurred art"),
|
||||
])));
|
||||
// Preferred-GPU labels ("Auto…" + the (discrete)/(integrated) tags) are
|
||||
// translated; the device names are not. Rebuild from the cached adapters.
|
||||
state.set_gpu_power_modes(ModelRc::new(VecModel::from(gpu_power_options())));
|
||||
state.set_wc_positions(ModelRc::new(VecModel::from(vec![t("Left"), t("Right")])));
|
||||
state.set_wc_styles(ModelRc::new(VecModel::from(vec![
|
||||
t("Rectangular"),
|
||||
@@ -6352,6 +6363,94 @@ fn probe_gpu_topology() -> GpuTopology {
|
||||
topo
|
||||
}
|
||||
|
||||
/// A real (non-CPU) GPU adapter, for the "Preferred GPU" Settings dropdown.
|
||||
#[derive(Clone)]
|
||||
struct GpuAdapterInfo {
|
||||
name: String,
|
||||
discrete: bool,
|
||||
}
|
||||
static GPU_ADAPTERS: std::sync::OnceLock<Vec<GpuAdapterInfo>> = std::sync::OnceLock::new();
|
||||
|
||||
/// Enumerate the machine's real GPUs (integrated + discrete), deduped by name
|
||||
/// (enumerate_adapters returns one per backend, so a single physical GPU shows
|
||||
/// up under Vulkan AND GL). Cached — the wgpu instance is created once.
|
||||
fn gpu_adapters() -> &'static Vec<GpuAdapterInfo> {
|
||||
GPU_ADAPTERS.get_or_init(|| {
|
||||
use slint::wgpu_28::wgpu;
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::all(),
|
||||
..Default::default()
|
||||
});
|
||||
let adapters =
|
||||
poll_ready(instance.enumerate_adapters(wgpu::Backends::all())).unwrap_or_default();
|
||||
let mut out: Vec<GpuAdapterInfo> = Vec::new();
|
||||
for adapter in &adapters {
|
||||
let info = adapter.get_info();
|
||||
let discrete = match info.device_type {
|
||||
wgpu::DeviceType::DiscreteGpu => true,
|
||||
wgpu::DeviceType::IntegratedGpu => false,
|
||||
_ => continue, // skip CPU / virtual / other
|
||||
};
|
||||
if !out.iter().any(|a| a.name == info.name) {
|
||||
out.push(GpuAdapterInfo {
|
||||
name: info.name.clone(),
|
||||
discrete,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
})
|
||||
}
|
||||
|
||||
/// Dropdown labels for the "Preferred GPU" setting: "Auto" + one per detected
|
||||
/// GPU, tagged (discrete)/(integrated). Built in Rust so it reflects the real
|
||||
/// hardware; index 0 = Auto, index i>0 = `gpu_adapters()[i-1]`.
|
||||
fn gpu_power_options() -> Vec<slint::SharedString> {
|
||||
let mut opts: Vec<slint::SharedString> = vec![qbz_i18n::t("Auto (recommended)").into()];
|
||||
for a in gpu_adapters() {
|
||||
let tag = if a.discrete {
|
||||
qbz_i18n::t("discrete")
|
||||
} else {
|
||||
qbz_i18n::t("integrated")
|
||||
};
|
||||
opts.push(format!("{} ({})", a.name, tag).into());
|
||||
}
|
||||
opts
|
||||
}
|
||||
|
||||
/// Persisted `gpu_power` -> the dropdown index in the CURRENT enumeration.
|
||||
/// Accepts a device name, the legacy type keys ("integrated"/"discrete"), or
|
||||
/// "auto". Falls back to 0 (Auto) when the stored device is no longer present.
|
||||
fn gpu_power_seed_index(persisted: &str) -> i32 {
|
||||
let adapters = gpu_adapters();
|
||||
match persisted {
|
||||
"" | "auto" => 0,
|
||||
"integrated" => adapters
|
||||
.iter()
|
||||
.position(|a| !a.discrete)
|
||||
.map_or(0, |i| i as i32 + 1),
|
||||
"discrete" => adapters
|
||||
.iter()
|
||||
.position(|a| a.discrete)
|
||||
.map_or(0, |i| i as i32 + 1),
|
||||
name => adapters
|
||||
.iter()
|
||||
.position(|a| a.name == name)
|
||||
.map_or(0, |i| i as i32 + 1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Dropdown index -> the value to persist in `gpu_power` (0 = "auto", else the
|
||||
/// selected adapter's name).
|
||||
fn gpu_power_value_for_index(index: i32) -> String {
|
||||
if index <= 0 {
|
||||
return "auto".to_string();
|
||||
}
|
||||
gpu_adapters()
|
||||
.get((index - 1) as usize)
|
||||
.map_or_else(|| "auto".to_string(), |a| a.name.clone())
|
||||
}
|
||||
|
||||
/// True when /sys/class/power_supply exposes a SYSTEM battery. Peripheral
|
||||
/// batteries (wireless mice etc.) report `scope=Device` — exclude them, or
|
||||
/// every desktop with a wireless mouse would classify as a laptop.
|
||||
@@ -6401,6 +6500,33 @@ fn default_wgpu_power_preference() -> slint::wgpu_28::wgpu::PowerPreference {
|
||||
}
|
||||
}
|
||||
|
||||
/// Persisted "Preferred GPU" setting -> wgpu adapter PowerPreference override.
|
||||
/// `None` = "auto" (fall through to `default_wgpu_power_preference`). The
|
||||
/// WGPU_POWER_PREF env still wins over this (it is checked first in the caller).
|
||||
/// On a hybrid laptop "discrete" (HighPerformance) moves the render off the
|
||||
/// integrated GPU — the fix for the iGPU running hot under the shader background.
|
||||
fn gpu_power_from_prefs() -> Option<slint::wgpu_28::wgpu::PowerPreference> {
|
||||
use slint::wgpu_28::wgpu::PowerPreference;
|
||||
let pref = crate::ui_prefs::load().gpu_power;
|
||||
match pref.as_str() {
|
||||
"" | "auto" => None,
|
||||
// Legacy type keys.
|
||||
"integrated" => Some(PowerPreference::LowPower),
|
||||
"discrete" => Some(PowerPreference::HighPerformance),
|
||||
// A specific device name: pick the power preference that steers wgpu to
|
||||
// that adapter's class (exact on the usual 1 iGPU + 1 dGPU laptop; a
|
||||
// same-class tie resolves to whichever wgpu prefers). Unknown name (GPU
|
||||
// removed) → None = auto.
|
||||
name => gpu_adapters().iter().find(|a| a.name == name).map(|a| {
|
||||
if a.discrete {
|
||||
PowerPreference::HighPerformance
|
||||
} else {
|
||||
PowerPreference::LowPower
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decide + activate the Slint backend, returning whether the GPU (wgpu) renderer was
|
||||
/// selected. `false` => femtovg-GL or the pure software renderer is active and the caller
|
||||
/// must skip the wgpu shader underlay (neither exposes a WGPU28 GraphicsAPI). See the big
|
||||
@@ -6484,7 +6610,10 @@ fn select_slint_backend() -> Result<bool, slint::PlatformError> {
|
||||
// default_wgpu_power_preference). WGPU_POWER_PREF still wins if
|
||||
// set (same as WGPUSettings::default()).
|
||||
let mut wgpu_settings = slint::wgpu_28::WGPUSettings::default();
|
||||
// Resolution order: WGPU_POWER_PREF env (debug) > persisted
|
||||
// "Preferred GPU" setting (Settings > Renderer) > auto default.
|
||||
wgpu_settings.power_preference = slint::wgpu_28::wgpu::PowerPreference::from_env()
|
||||
.or_else(gpu_power_from_prefs)
|
||||
.unwrap_or_else(default_wgpu_power_preference);
|
||||
// Alternate-adapter rung: the previous start died on the adapter
|
||||
// this preference picks, so flip it (even over WGPU_POWER_PREF —
|
||||
@@ -9882,6 +10011,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
qbz_i18n::t("Renderer changed — restart QBZ to apply"),
|
||||
);
|
||||
}
|
||||
"gpu-power" => {
|
||||
// 0 = Auto, i>0 = a specific detected GPU (by name). Applied at
|
||||
// startup by gpu_power_from_prefs (wgpu adapter power preference),
|
||||
// so it takes effect on the next launch.
|
||||
let mut prefs = crate::ui_prefs::load();
|
||||
prefs.gpu_power = gpu_power_value_for_index(index);
|
||||
crate::ui_prefs::save(&prefs);
|
||||
crate::toast::info_weak(
|
||||
&theme_weak,
|
||||
qbz_i18n::t("Preferred GPU changed — restart QBZ to apply"),
|
||||
);
|
||||
}
|
||||
"ui-scale" => {
|
||||
// 0 = Extra small, 1 = Small, 2 = Default, 3 = Large,
|
||||
// 4 = Extra large. Startup-time choice — SLINT_SCALE_FACTOR is
|
||||
|
||||
@@ -560,6 +560,12 @@ pub struct UiPrefs {
|
||||
/// window comes up, the next start reverts this to "auto".
|
||||
#[serde(default = "default_renderer")]
|
||||
pub renderer: String,
|
||||
/// Preferred GPU adapter: "auto" | "integrated" | "discrete". Drives the
|
||||
/// wgpu PowerPreference at startup (WGPU_POWER_PREF env still wins). On a
|
||||
/// hybrid laptop "discrete" moves the render off the integrated GPU
|
||||
/// (thermals); requires restart. See main.rs `gpu_power_from_prefs`.
|
||||
#[serde(default = "default_gpu_power")]
|
||||
pub gpu_power: String,
|
||||
/// App version that AUTO-degraded `renderer` (the ladder persisted "gl"
|
||||
/// or "software" after failed starts). Empty = `renderer` is the user's
|
||||
/// own choice. A NEW build re-probes "auto" once (vendored renderer
|
||||
@@ -617,6 +623,10 @@ fn default_wc_position() -> String {
|
||||
"right".to_string()
|
||||
}
|
||||
|
||||
fn default_gpu_power() -> String {
|
||||
"auto".to_string()
|
||||
}
|
||||
|
||||
fn default_renderer() -> String {
|
||||
"auto".to_string()
|
||||
}
|
||||
@@ -778,6 +788,7 @@ impl Default for UiPrefs {
|
||||
window_y: default_window_pos(),
|
||||
window_maximized: false,
|
||||
renderer: default_renderer(),
|
||||
gpu_power: default_gpu_power(),
|
||||
renderer_auto_degraded: String::new(),
|
||||
renderer_wgpu_alt: String::new(),
|
||||
ui_scale: default_ui_scale(),
|
||||
|
||||
Reference in new issue
Block a user