feat(about): improve GPU names by getting info from wgpu

This commit is contained in:
Fred 2025-12-12 16:57:58 +01:00 committed by GitHub
parent fffee57149
commit 29f1386e58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 488 additions and 573 deletions

View file

@ -32,7 +32,6 @@ cosmic-settings-bluetooth-subscription = { path = "../subscriptions/bluetooth",
cosmic-settings-network-manager-subscription = { path = "../subscriptions/network-manager", optional = true }
cosmic-settings-upower-subscription = { path = "../subscriptions/upower", optional = true }
cosmic-settings-sound-subscription = { path = "../subscriptions/sound", optional = true, features = ["auto-profile-init"] }
cosmic-settings-system = { path = "../pages/system", optional = true }
cosmic-settings-wallpaper = { path = "../pages/wallpapers" }
cosmic-settings-daemon-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
derive_setters = "0.1.8"
@ -55,6 +54,7 @@ itertools = "0.14.0"
itoa = "1.0.15"
libcosmic.workspace = true
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
sysinfo = { version = "0.36.1", optional = true }
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", optional = true }
# TODO: updating notify beyond this hangs the app
notify = "6.1.1"
@ -140,7 +140,7 @@ page-accessibility = [
"dep:cosmic-settings-accessibility-subscription",
"dep:cosmic-settings-a11y-manager-subscription",
]
page-about = ["dep:cosmic-settings-system", "dep:hostname1-zbus", "dep:zbus"]
page-about = ["dep:hostname1-zbus", "dep:sysinfo", "dep:zbus"]
page-bluetooth = [
"dep:cosmic-settings-bluetooth-subscription",
"dep:zbus",

View file

@ -3,9 +3,9 @@
use cosmic_settings_page::{self as page, Section, section};
use super::info::Info;
use cosmic::widget::{editable_input, list_column, settings, text};
use cosmic::{Apply, Task};
use cosmic_settings_system::about::Info;
use slab::Slab;
use slotmap::SlotMap;
@ -119,46 +119,48 @@ impl Page {
return Task::none();
}
let hostname = &self.hostname_input;
if hostname_validator::is_valid(hostname) {
self.editing_device_name = false;
let hostname = hostname.clone();
return cosmic::Task::future(async move {
let connection = match zbus::Connection::system().await {
Ok(conn) => conn,
Err(why) => {
tracing::error!(?why, "failed to establish connection to dbus");
return Message::Error(String::from(
"failed to establish connection to dbus",
));
}
};
let hostname1 = match hostname1_zbus::Hostname1Proxy::new(&connection).await {
Ok(proxy) => proxy,
Err(why) => {
tracing::error!(?why, "failed to connect to org.freedesktop.hostname1");
return Message::Error(String::from(
"failed to connect to org.freedesktop.hostname1",
));
}
};
if let Err(why) = hostname1.set_static_hostname(&hostname, false).await {
tracing::error!(?why, "failed to set static hostname");
return Message::Error(String::from("failed to set static hostname"));
}
Message::HostnameSuccess(hostname)
})
.map(crate::app::Message::from)
.map(Into::into);
if !hostname_validator::is_valid(&self.hostname_input) {
return Task::none();
}
Task::none()
self.editing_device_name = false;
let hostname = self.hostname_input.clone();
cosmic::Task::future(async move { set_hostname(hostname).await })
.map(crate::app::Message::from)
.map(Into::into)
}
}
/// Sets the system hostname via D-Bus.
async fn set_hostname(hostname: String) -> Message {
match set_hostname_impl(&hostname).await {
Ok(()) => Message::HostnameSuccess(hostname),
Err(err) => {
tracing::error!("failed to set hostname: {}", err);
Message::Error(err)
}
}
}
/// Implementation of hostname setting that uses Result for cleaner error handling.
async fn set_hostname_impl(hostname: &str) -> Result<(), String> {
let connection = zbus::Connection::system()
.await
.map_err(|e| format!("failed to establish connection to dbus: {}", e))?;
let hostname1 = hostname1_zbus::Hostname1Proxy::new(&connection)
.await
.map_err(|e| format!("failed to connect to org.freedesktop.hostname1: {}", e))?;
hostname1
.set_static_hostname(hostname, false)
.await
.map_err(|e| format!("failed to set static hostname: {}", e))?;
Ok(())
}
fn device() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
@ -207,7 +209,7 @@ fn hardware() -> Section<crate::pages::Message> {
.view::<Page>(move |_binder, page, section| {
let desc = &section.descriptions;
let sections = settings::section()
let mut section_builder = settings::section()
.title(&section.title)
.add(settings::flex_item(
&*desc[model],
@ -222,15 +224,14 @@ fn hardware() -> Section<crate::pages::Message> {
text::body(&page.info.processor),
));
page.info
.graphics
.iter()
.fold(sections, |sections, card| {
sections.add(settings::flex_item(
&*desc[graphics],
text::body(card.as_str()),
))
})
for card in &page.info.graphics {
section_builder = section_builder.add(settings::flex_item(
&*desc[graphics],
text::body(card.as_str()),
));
}
section_builder
.add(settings::flex_item(
&*desc[disk_capacity],
text::body(&page.info.disk_capacity),

View file

@ -0,0 +1,419 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::iced_wgpu::wgpu;
use std::{collections::HashMap, collections::HashSet, ffi::OsStr, process::Command};
#[must_use]
#[derive(Clone, Debug, Default)]
pub struct Info {
pub desktop_environment: String,
pub device_name: String,
pub disk_capacity: String,
pub graphics: Vec<String>,
pub hardware_model: String,
pub memory: String,
pub operating_system: String,
pub os_architecture: String,
pub kernel_version: String,
pub processor: String,
pub windowing_system: String,
}
impl Info {
pub fn load() -> Info {
let mut info = Info::default();
info.os_architecture = architecture();
info.kernel_version = kernel_version();
info.hardware_model = hardware_model();
info.operating_system = operating_system();
info.processor = processor_name();
let mut sys = sysinfo::System::new();
let disks = sysinfo::Disks::new_with_refreshed_list();
sys.refresh_memory();
let mut total_capacity = 0;
let mut disk_set = HashSet::new();
for disk in disks.list() {
if disk_set.contains(disk.name()) {
continue;
}
disk_set.insert(disk.name());
total_capacity += disk.total_space();
}
info.disk_capacity = format_size(total_capacity);
if let Some(name) = sysinfo::System::host_name() {
info.device_name = name;
}
info.memory = format_size(sys.total_memory());
if let Ok(mut session) = std::env::var("XDG_SESSION_TYPE") {
if let Some(first) = session.get_mut(0..1) {
first.make_ascii_uppercase();
}
info.windowing_system = session;
}
// prefer XDG_SESSION_DESKTOP because the value is singular
if let Ok(mut session) = std::env::var("XDG_SESSION_DESKTOP")
.or_else(|_| std::env::var("XDG_CURRENT_DESKTOP"))
.or_else(|_| std::env::var("DESKTOP_SESSION"))
{
if let Some(first) = session.get_mut(0..1) {
first.make_ascii_uppercase();
}
info.desktop_environment = session;
}
// Use wgpu to enumerate GPUs. Works cross-platform and doesn't require external tools
let instance = wgpu::Instance::default();
let adapters = instance.enumerate_adapters(wgpu::Backends::all());
// Track seen GPUs by (vendor, device) and by name to handle different scenarios:
// - Same GPU via different backends (Vulkan/OpenGL) -> deduplicate by device ID or name
// - Multiple identical GPUs -> show all (different device IDs or backend enumeration order)
// - Backends with invalid device IDs (0x0000) -> deduplicate by name
let mut seen_devices = HashSet::new();
let mut seen_names = HashSet::new();
for adapter in adapters {
let adapter_info = adapter.get_info();
if adapter_info.device_type == wgpu::DeviceType::Cpu {
continue;
}
let mut gpu_name = if let Some(pos) = adapter_info.name.find('(') {
adapter_info.name[..pos].trim().to_string()
} else {
adapter_info.name
};
// Intel GPU quirk: wgpu sometimes reports incomplete names (just "Intel" or "Mesa Intel").
// Mesa OpenGL backend also reports Intel GPUs with "Mesa Intel(R)" prefix and device ID 0.
// These are generic/duplicate entries when the driver can't properly identify the GPU.
// Use lspci as fallback to get proper full name like "Intel Corporation HD Graphics 500"
let is_intel_quirk = gpu_name == "Intel"
|| gpu_name == "Mesa Intel"
|| (gpu_name.starts_with("Mesa Intel") && adapter_info.device == 0);
if is_intel_quirk {
if adapter_info.device != 0 {
let lspci_gpus = get_lspci_gpu_names();
if let Some(lspci_name) = lspci_gpus.get(&adapter_info.device) {
gpu_name = lspci_name.clone();
} else {
// If lspci lookup fails, skip this generic name to avoid duplicates
continue;
}
} else {
// Skip generic Intel names with no device ID (Mesa OpenGL duplicates)
continue;
}
}
// AMD parsing to append codename to specific name
let is_amd_gpu = gpu_name.starts_with("AMD");
if is_amd_gpu {
if adapter_info.device != 0 {
let lspci_gpus = get_lspci_gpu_names();
// If we can't find an lspci name for a GPU, just keep its original name
if let Some(lspci_name) = lspci_gpus.get(&adapter_info.device) {
// Codename starts after first pair of brackets
if let Some(bracket_end) = lspci_name.find(']') {
let mut code_name = lspci_name[bracket_end + 1..].to_string();
// After codename, there may be another pair of brackets.
if let Some(bracket_start) = code_name.find('[') {
code_name = code_name[..bracket_start].to_string();
}
if code_name.trim() != "Device" {
gpu_name = format!("{} ({})", gpu_name, code_name.trim());
}
}
}
} else {
// Can't identify GPU
continue;
}
}
let device_key = (adapter_info.vendor, adapter_info.device);
if adapter_info.device != 0 && seen_devices.contains(&device_key) {
continue;
}
if adapter_info.device == 0 && seen_names.contains(&gpu_name) {
continue;
}
if adapter_info.device != 0 {
seen_devices.insert(device_key);
}
seen_names.insert(gpu_name.clone());
info.graphics.push(gpu_name);
}
// NVIDIA Optimus quirk: On laptops with NVIDIA Optimus (switchable graphics),
// the dedicated NVIDIA GPU may be powered off to save battery and won't be
// enumerated by wgpu. Use lspci as a fallback to detect these inactive GPUs.
// This ensures users can see all GPUs in their system, even when not in use.
// Reference: https://wiki.archlinux.org/title/NVIDIA_Optimus
let lspci_gpus = get_all_lspci_gpus();
for (vendor, device, name) in lspci_gpus {
let device_key = (vendor, device);
if !seen_devices.contains(&device_key) {
seen_devices.insert(device_key);
info.graphics.push(name);
}
}
info
}
}
fn architecture() -> String {
read_to_string("/proc/sys/kernel/arch")
.unwrap_or_default()
.trim()
.to_string()
}
fn kernel_version() -> String {
read_to_string("/proc/version")
.and_then(|content| content.split_whitespace().nth(2).map(|s| s.to_string()))
.unwrap_or_default()
}
fn hardware_model() -> String {
const DMI_DIR: &str = "/sys/devices/virtual/dmi/id/";
const VERSION_IGNORING_PRODUCTS: &[&str] = &["Dev One"];
let sys_vendor = read_to_string(&format!("{DMI_DIR}sys_vendor"))
.map(|s| s.trim().to_string())
.unwrap_or_default();
if sys_vendor.is_empty() {
// Fallback to sysinfo if DMI information is not available
return sysinfo::Product::name().unwrap_or_default();
}
let mut model = sys_vendor.clone();
if let Some(mut name) =
read_to_string(&format!("{DMI_DIR}board_name")).map(|s| s.trim().to_string())
{
if !name.is_empty() && name != sys_vendor {
// Ensure that the name does not contain the vendor
name = name
.strip_prefix(&sys_vendor)
.map(|s| s.trim().to_string())
.unwrap_or(name);
model.push(' ');
model.push_str(&name);
if let Some(version) =
read_to_string(&format!("{DMI_DIR}board_version")).map(|s| s.trim().to_string())
{
if !version.is_empty() && !VERSION_IGNORING_PRODUCTS.contains(&name.as_str()) {
model.push_str(&format!(" ({version})"));
}
}
}
}
model
}
fn operating_system() -> String {
let content = read_to_string("/etc/os-release").unwrap_or_default();
for line in content.lines() {
if let Some(mut value) = line.strip_prefix("PRETTY_NAME=") {
value = value.strip_prefix('"').unwrap_or(value);
value = value.strip_suffix('"').unwrap_or(value);
return value.trim().to_string();
}
}
String::new()
}
fn processor_name() -> String {
if let Some(cpuinfo) = read_to_string("/proc/cpuinfo") {
for line in cpuinfo.lines() {
if let Some(info) = line.strip_prefix("model name") {
if let Some(mut info) = info.trim_start().strip_prefix(':') {
if let Some(cpu_with) = info.find(" w/ ") {
info = &info[..cpu_with];
}
return info.trim().to_string();
}
break;
}
}
}
// Fallback to sysinfo if /proc/cpuinfo is not present
let sys = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::nothing().with_cpu(sysinfo::CpuRefreshKind::everything()),
);
sys.cpus()
.first()
.map(|cpu| cpu.brand().to_string())
.unwrap_or_default()
}
fn read_to_string<P: AsRef<OsStr>>(path: P) -> Option<String> {
std::fs::read_to_string(path.as_ref()).ok()
}
fn format_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
const FACTOR: f64 = 1024.0;
if bytes == 0 {
return String::from("0 B");
}
let bytes_f64 = bytes as f64;
let exp = (bytes_f64.ln() / FACTOR.ln()).floor() as usize;
let exp = exp.min(UNITS.len() - 1);
let value = bytes_f64 / FACTOR.powi(exp as i32);
format!("{:.2} {}", value, UNITS[exp])
}
/// Get all GPUs from lspci with their vendor ID, device ID, and name.
fn get_all_lspci_gpus() -> Vec<(u32, u32, String)> {
let mut gpus = Vec::new();
let output = match Command::new("lspci").arg("-nn").output() {
Ok(output) => output,
Err(_) => return gpus,
};
let stdout = match std::str::from_utf8(&output.stdout) {
Ok(s) => s,
Err(_) => return gpus,
};
for line in stdout.lines() {
if !line.contains("VGA compatible") // Avoid "Non-VGA unclassified"
&& !line.contains("3D controller")
&& !line.contains("Display controller")
{
continue;
}
// Parse device ID from format: "00:02.0 VGA compatible controller [0300]: Intel Corporation HD Graphics 500 [8086:5a85] (rev 0b)"
// We want to extract vendor:device IDs (8086:5a85) and the name (Intel Corporation HD Graphics 500)
if let Some(ids_start) = line.rfind('[') {
if let Some(ids_end) = line.rfind(']') {
let ids = &line[ids_start + 1..ids_end];
if let Some(colon_pos) = ids.find(':') {
let vendor_id_str = &ids[..colon_pos];
let device_id_str = &ids[colon_pos + 1..];
if let (Ok(vendor_id), Ok(device_id)) = (
u32::from_str_radix(vendor_id_str, 16),
u32::from_str_radix(device_id_str, 16),
) {
if let Some(name_start) = line.find(": ") {
let full_name = line[name_start + 2..ids_start].trim();
// Look for marketing name in brackets like "GP108M [GeForce MX150]"
// We prefer the marketing name over the chip code
let gpu_name = if let Some(bracket_start) = full_name.find('[') {
if let Some(bracket_end) = full_name.find(']') {
let vendor_part = full_name[..bracket_start].trim();
let marketing_name =
full_name[bracket_start + 1..bracket_end].trim();
let vendor = vendor_part
.split_whitespace()
.next()
.unwrap_or(vendor_part);
format!("{} {}", vendor, marketing_name)
} else {
full_name.to_string()
}
} else {
full_name.to_string()
};
if !gpu_name.is_empty() {
gpus.push((vendor_id, device_id, gpu_name));
}
}
}
}
}
}
}
gpus
}
/// Get GPU names from lspci, mapping device ID to full GPU name.
/// This is used as a quirk for Intel GPUs which often report incomplete names via their OpenGL and Vulkan drivers.
/// https://www.intel.com/content/www/us/en/support/articles/000005520/graphics.html
fn get_lspci_gpu_names() -> HashMap<u32, String> {
let mut gpu_map = HashMap::new();
let output = match Command::new("lspci").arg("-nn").output() {
Ok(output) => output,
Err(_) => return gpu_map,
};
let stdout = match std::str::from_utf8(&output.stdout) {
Ok(s) => s,
Err(_) => return gpu_map,
};
for line in stdout.lines() {
// Look for any line containing VGA, 3D controller, or Display controller
if !line.contains("VGA compatible") // Avoid "Non-VGA unclassified"
&& !line.contains("3D controller")
&& !line.contains("Display controller")
{
continue;
}
// Parse device ID from format: "00:02.0 VGA compatible controller [0300]: Intel Corporation HD Graphics 500 [8086:5a85] (rev 0b)"
// Or with marketing name: "00:02.0 VGA compatible controller: Intel Corporation Alder Lake-P GT2 [Iris Xe Graphics] [8086:46a6] (rev 0c)"
// We want to extract the device ID (5a85/46a6) and the name, preferring marketing name if available
if let Some(ids_start) = line.rfind('[') {
if let Some(ids_end) = line.rfind(']') {
let ids = &line[ids_start + 1..ids_end];
// Parse vendor:device format like "8086:5a85"
if let Some(colon_pos) = ids.find(':') {
let device_id_str = &ids[colon_pos + 1..];
if let Ok(device_id) = u32::from_str_radix(device_id_str, 16) {
// Extract the GPU name between ": " and the last "["
if let Some(name_start) = line.find(": ") {
let full_name = line[name_start + 2..ids_start].trim();
let gpu_name = full_name.replace(" Corporation", "").replace(" [Intel Graphics]", "");
if !gpu_name.is_empty() {
gpu_map.insert(device_id, gpu_name);
}
}
}
}
}
}
}
gpu_map
}

View file

@ -4,6 +4,8 @@
#[cfg(feature = "page-about")]
pub mod about;
#[cfg(feature = "page-about")]
pub mod info;
#[cfg(feature = "page-users")]
pub mod users;