feat: show OpenCL info, move Vulkan to software tab (#553)

* feat: fetch OpenCL info

* feat: show opencl info in gui

* chore: move vulkan info to software page, refactor

* refactor: unify hw info page into one component

* chore: update generated-sources

* feat: show more information

* fix: disable dynamic feature in OpenCL

* Revert "fix: disable dynamic feature in OpenCL"

This reverts commit 18c9d93ed7.

* fix: add dependency

* fix: set correct dependencies

* fix: opencl package name

* fix: fedora package name

* fix: add rhel repo

* chore: update readme
This commit is contained in:
Ilya Zlobintsev
2025-04-23 22:11:56 +03:00
committed by GitHub
parent 980f383df6
commit 96bb7b7172
28 changed files with 690 additions and 352 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Update repos
run: sudo apt update
- name: Install dependencies
run: sudo apt install -y libgtk-4-dev pkg-config libvulkan-dev libdrm-dev curl fuse3 libfuse-dev
run: sudo apt install -y libgtk-4-dev pkg-config libvulkan-dev libdrm-dev curl fuse3 libfuse-dev ocl-icd-libopencl1 ocl-icd-opencl-dev
- name: Update pci.ids
run: sudo curl -o /usr/share/hwdata/pci.ids https://pci-ids.ucw.cz/v2.2/pci.ids
- name: Build
Generated
+67 -2
View File
@@ -310,6 +310,18 @@ dependencies = [
"windows-link",
]
[[package]]
name = "cl3"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a50168bca6baeb04c8aec60adba5917221034d96275f6b199be66a3969519f1"
dependencies = [
"dlopen2",
"libc",
"opencl-sys",
"thiserror 2.0.12",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
@@ -537,6 +549,29 @@ dependencies = [
"syn",
]
[[package]]
name = "dlopen2"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6"
dependencies = [
"dlopen2_derive",
"libc",
"once_cell",
"winapi",
]
[[package]]
name = "dlopen2_derive"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "easy_fuser"
version = "0.4.0"
@@ -1308,6 +1343,7 @@ dependencies = [
"bindgen",
"bitflags 2.9.0",
"chrono",
"cl3",
"copes",
"divan",
"easy_fuser",
@@ -1671,7 +1707,7 @@ dependencies = [
"libloading",
"nvml-wrapper-sys",
"static_assertions",
"thiserror",
"thiserror 1.0.69",
"wrapcenum-derive",
]
@@ -1698,6 +1734,15 @@ version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "opencl-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edabf61284e58c345821019106a2e68f8a61eb732c0d2b214a78af24499855c"
dependencies = [
"libc",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
@@ -2338,7 +2383,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
]
[[package]]
@@ -2352,6 +2406,17 @@ dependencies = [
"syn",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread-priority"
version = "1.2.0"
+3 -2
View File
@@ -164,13 +164,14 @@ Dependencies:
- hwdata
- libdrm
- vulkan-tools
- ocl-icd
Command to install all dependencies:
- Fedora:
`sudo dnf install rust cargo make git clang gtk4-devel libdrm-devel vulkan-tools`
`sudo dnf install rust cargo make git clang gtk4-devel libdrm-devel vulkan-tools ocl-icd-devel`
- Arch:
`sudo pacman -S --needed base-devel git clang make rust gtk4 hwdata vulkan-tools`
`sudo pacman -S --needed base-devel git clang make rust gtk4 hwdata vulkan-tools ocl-icd`
Steps:
+79 -1
View File
@@ -440,6 +440,19 @@
"dest": "cargo/vendor/chrono-0.4.40",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/cl3/cl3-0.13.0.crate",
"sha256": "0a50168bca6baeb04c8aec60adba5917221034d96275f6b199be66a3969519f1",
"dest": "cargo/vendor/cl3-0.13.0"
},
{
"type": "inline",
"contents": "{\"package\": \"0a50168bca6baeb04c8aec60adba5917221034d96275f6b199be66a3969519f1\", \"files\": {}}",
"dest": "cargo/vendor/cl3-0.13.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -757,6 +770,32 @@
"dest": "cargo/vendor/divan-macros-0.1.17",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/dlopen2/dlopen2-0.7.0.crate",
"sha256": "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6",
"dest": "cargo/vendor/dlopen2-0.7.0"
},
{
"type": "inline",
"contents": "{\"package\": \"9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6\", \"files\": {}}",
"dest": "cargo/vendor/dlopen2-0.7.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/dlopen2_derive/dlopen2_derive-0.4.0.crate",
"sha256": "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54",
"dest": "cargo/vendor/dlopen2_derive-0.4.0"
},
{
"type": "inline",
"contents": "{\"package\": \"f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54\", \"files\": {}}",
"dest": "cargo/vendor/dlopen2_derive-0.4.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -2093,6 +2132,19 @@
"dest": "cargo/vendor/once_cell-1.20.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/opencl-sys/opencl-sys-0.6.0.crate",
"sha256": "6edabf61284e58c345821019106a2e68f8a61eb732c0d2b214a78af24499855c",
"dest": "cargo/vendor/opencl-sys-0.6.0"
},
{
"type": "inline",
"contents": "{\"package\": \"6edabf61284e58c345821019106a2e68f8a61eb732c0d2b214a78af24499855c\", \"files\": {}}",
"dest": "cargo/vendor/opencl-sys-0.6.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -3029,6 +3081,19 @@
"dest": "cargo/vendor/thiserror-1.0.69",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/thiserror/thiserror-2.0.12.crate",
"sha256": "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708",
"dest": "cargo/vendor/thiserror-2.0.12"
},
{
"type": "inline",
"contents": "{\"package\": \"567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708\", \"files\": {}}",
"dest": "cargo/vendor/thiserror-2.0.12",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -3042,6 +3107,19 @@
"dest": "cargo/vendor/thiserror-impl-1.0.69",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/thiserror-impl/thiserror-impl-2.0.12.crate",
"sha256": "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d",
"dest": "cargo/vendor/thiserror-impl-2.0.12"
},
{
"type": "inline",
"contents": "{\"package\": \"7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d\", \"files\": {}}",
"dest": "cargo/vendor/thiserror-impl-2.0.12",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -4062,4 +4140,4 @@
"dest": "cargo",
"dest-filename": "config"
}
]
]
+1
View File
@@ -47,6 +47,7 @@ notify = { version = "8.0.0", default-features = false }
copes = { git = "https://gitlab.com/corectrl/copes" }
libloading = "0.8.6"
tempfile = "3.17.1"
cl3 = { version = "0.13.0", features = ["static"] }
[dev-dependencies]
pretty_assertions = { workspace = true }
+1
View File
@@ -1,5 +1,6 @@
pub mod gpu_controller;
pub mod handler;
mod opencl;
mod profiles;
pub(crate) mod system;
mod vulkan;
+25
View File
@@ -17,6 +17,7 @@ pub const VENDOR_NVIDIA: &str = "10DE";
use crate::bindings::intel::IntelDrm;
use amdgpu_sysfs::gpu_handle::power_profile_mode::PowerProfileModesTable;
use anyhow::anyhow;
use anyhow::Context;
use futures::{future::LocalBoxFuture, FutureExt};
use lact_schema::{
@@ -81,6 +82,30 @@ impl CommonControllerInfo {
self.pci_slot_name
)
}
pub fn get_slot_info(&self) -> anyhow::Result<PciSlotInfo> {
let [domain, bus, dev, func] = self
.pci_slot_name
.split([':', '.'])
.map(|part| u16::from_str_radix(part, 16).context("Could not parse pci slot name part"))
.collect::<anyhow::Result<Vec<_>>>()?
.try_into()
.map_err(|err| anyhow!("Invalid pci slot name format {err:?}"))?;
Ok(PciSlotInfo {
domain,
bus,
dev,
func,
})
}
}
pub struct PciSlotInfo {
pub domain: u16,
pub bus: u16,
pub dev: u16,
pub func: u16,
}
pub(crate) fn init_controller(
+6 -2
View File
@@ -1,5 +1,7 @@
use super::{CommonControllerInfo, FanControlHandle, GpuController, VENDOR_AMD};
use crate::server::{gpu_controller::fan_control::FanCurveExt, vulkan::get_vulkan_info};
use crate::server::{
gpu_controller::fan_control::FanCurveExt, opencl::get_opencl_info, vulkan::get_vulkan_info,
};
use amdgpu_sysfs::{
error::Error,
gpu_handle::{
@@ -407,7 +409,7 @@ impl AmdGpuController {
.enumerate()
.map(|(i, value)| {
let i = u8::try_from(i).unwrap();
let enabled = enabled_states.map_or(true, |enabled| enabled.contains(&i));
let enabled = enabled_states.is_none_or(|enabled| enabled.contains(&i));
PowerState {
enabled,
min_value: None,
@@ -599,6 +601,7 @@ impl GpuController for AmdGpuController {
let vbios_version = self.get_full_vbios_version();
let link_info = self.get_link_info();
let drm_info = self.get_drm_info();
let opencl_info = get_opencl_info(&self.common);
DeviceInfo {
pci_info,
@@ -606,6 +609,7 @@ impl GpuController for AmdGpuController {
driver,
vbios_version,
link_info,
opencl_info,
drm_info,
}
})
@@ -6,7 +6,7 @@ use crate::{
drm_i915_gem_memory_class_I915_MEMORY_CLASS_DEVICE,
drm_xe_memory_class_DRM_XE_MEM_REGION_CLASS_VRAM, IntelDrm,
},
server::vulkan::get_vulkan_info,
server::{opencl::get_opencl_info, vulkan::get_vulkan_info},
};
use amdgpu_sysfs::{gpu_handle::power_profile_mode::PowerProfileModesTable, hw_mon::Temperature};
use anyhow::{anyhow, Context};
@@ -588,6 +588,7 @@ impl GpuController for IntelGpuController {
vbios_version: None,
link_info: LinkInfo::default(),
drm_info: Some(drm_info),
opencl_info: get_opencl_info(&self.common),
}
})
}
@@ -1,7 +1,9 @@
mod driver;
use super::{CommonControllerInfo, FanControlHandle, GpuController};
use crate::server::{gpu_controller::fan_control::FanCurveExt, vulkan::get_vulkan_info};
use crate::server::{
gpu_controller::fan_control::FanCurveExt, opencl::get_opencl_info, vulkan::get_vulkan_info,
};
use amdgpu_sysfs::{gpu_handle::power_profile_mode::PowerProfileModesTable, hw_mon::Temperature};
use anyhow::{anyhow, bail, Context};
use driver::DriverHandle;
@@ -322,6 +324,7 @@ impl GpuController for NvidiaGpuController {
output
}),
},
opencl_info: get_opencl_info(&self.common),
drm_info: Some(DrmInfo {
device_name: device.name().ok(),
pci_revision_id: None,
+107
View File
@@ -0,0 +1,107 @@
use super::gpu_controller::{CommonControllerInfo, PciSlotInfo};
use anyhow::anyhow;
use cl3::{
device,
ext::{
cl_device_info, CL_DEVICE_GLOBAL_MEM_SIZE, CL_DEVICE_LOCAL_MEM_SIZE,
CL_DEVICE_MAX_COMPUTE_UNITS, CL_DEVICE_MAX_WORK_GROUP_SIZE, CL_DEVICE_NAME,
CL_DEVICE_OPENCL_C_VERSION, CL_DEVICE_PCI_BUS_INFO_KHR, CL_DEVICE_TYPE_ALL,
CL_DEVICE_VERSION, CL_DRIVER_VERSION, CL_PLATFORM_NAME,
},
platform,
};
use lact_schema::OpenCLInfo;
use std::ffi::c_void;
use tracing::error;
pub fn get_opencl_info(info: &CommonControllerInfo) -> Option<OpenCLInfo> {
match try_get_opencl_info(info) {
Ok(info) => info,
Err(err) => {
error!("could not get OpenCL info: {err}");
None
}
}
}
fn try_get_opencl_info(info: &CommonControllerInfo) -> anyhow::Result<Option<OpenCLInfo>> {
let slot_info = info.get_slot_info()?;
let Some((platform, device)) = find_matching_device(&slot_info)? else {
return Ok(None);
};
let platform_name = platform::get_platform_info(platform, CL_PLATFORM_NAME)
.map_err(|err| anyhow!("Could not get platform name: {err}"))?
.to_string()
.replace('\0', "");
let device_name = get_info_string(device, CL_DEVICE_NAME)?;
let version = get_info_string(device, CL_DEVICE_VERSION)?;
let driver_version = get_info_string(device, CL_DRIVER_VERSION)?;
let c_version = get_info_string(device, CL_DEVICE_OPENCL_C_VERSION)?;
let compute_units = device::get_device_info(device, CL_DEVICE_MAX_COMPUTE_UNITS)
.map_err(|err| anyhow!("Could not get device cu count: {err}"))?
.to_uint();
let workgroup_size = device::get_device_info(device, CL_DEVICE_MAX_WORK_GROUP_SIZE)
.map_err(|err| anyhow!("Could not get device cu count: {err}"))?
.to_size();
let global_memory = device::get_device_info(device, CL_DEVICE_GLOBAL_MEM_SIZE)
.map_err(|err| anyhow!("Could not get device memory: {err}"))?
.to_ulong();
let local_memory = device::get_device_info(device, CL_DEVICE_LOCAL_MEM_SIZE)
.map_err(|err| anyhow!("Could not get device memory: {err}"))?
.to_ulong();
Ok(Some(OpenCLInfo {
platform_name,
device_name,
version,
driver_version,
c_version,
workgroup_size,
compute_units,
global_memory,
local_memory,
}))
}
fn find_matching_device(
slot_info: &PciSlotInfo,
) -> anyhow::Result<Option<(*mut c_void, *mut c_void)>> {
let platforms = platform::get_platform_ids()
.map_err(|err| anyhow!("Could not get platform list: {err}"))?;
for platform in platforms {
let devices = device::get_device_ids(platform, CL_DEVICE_TYPE_ALL)
.map_err(|err| anyhow!("Could not get device list: {err}"))?;
for device in devices {
let raw_bus_info = device::get_device_info(device, CL_DEVICE_PCI_BUS_INFO_KHR)
.map_err(|err| anyhow!("Could not get bus info: {err}"))?
.to_vec_uchar();
let bus_info = device::get_device_pci_bus_info_khr(&raw_bus_info);
if bus_info.pci_bus == u32::from(slot_info.bus)
&& bus_info.pci_device == u32::from(slot_info.dev)
&& bus_info.pci_domain == u32::from(slot_info.domain)
&& bus_info.pci_function == u32::from(slot_info.func)
{
return Ok(Some((platform, device)));
}
}
}
Ok(None)
}
fn get_info_string(device: *mut c_void, param: cl_device_info) -> anyhow::Result<String> {
let mut string = device::get_device_info(device, param)
.map_err(|err| anyhow!("Could not fetch property {param:0x}: {err}"))?
.to_string();
string.pop();
Ok(string)
}
+18 -2
View File
@@ -35,7 +35,7 @@ use msg::AppMsg;
use pages::{
info_page::InformationPage,
oc_page::{OcPage, OcPageMsg},
software_page::SoftwarePage,
software_page::{SoftwarePage, SoftwarePageMsg},
thermals_page::ThermalsPage,
PageUpdate,
};
@@ -86,7 +86,7 @@ impl AsyncComponent for AppModel {
#[root]
gtk::ApplicationWindow::builder()
.titlebar(&gtk::HeaderBar::new())
.default_height(900)
.default_height(800)
.default_width(60)
.icon_name(APP_ID)
.title("LACT")
@@ -460,6 +460,8 @@ impl AppModel {
update,
initial: true,
});
self.software_page
.emit(SoftwarePageMsg::DeviceInfo(info.clone()));
let vram_clock_ratio = info
.drm_info
@@ -1097,3 +1099,17 @@ async fn create_connection() -> anyhow::Result<(DaemonClient, Option<anyhow::Err
}
}
}
fn format_friendly_size(bytes: u64) -> String {
const NAMES: &[&str] = &["bytes", "KiB", "MiB", "GiB"];
let mut size = bytes as f64;
let mut i = 0;
while size > 2048.0 && i < NAMES.len() - 1 {
size /= 1024.0;
i += 1;
}
format!("{size:.1$} {}", NAMES[i], (size.fract() != 0.0) as usize)
}
+32
View File
@@ -23,6 +23,38 @@ impl InfoRow {
}
}
pub struct InfoRowItem {
pub name: String,
pub value: String,
pub note: Option<&'static str>,
}
#[relm4::factory(pub)]
impl relm4::factory::FactoryComponent for InfoRowItem {
type Init = Self;
type ParentWidget = gtk::Box;
type CommandOutput = ();
type Input = ();
type Output = ();
fn init_model(
init: Self::Init,
_index: &Self::Index,
_sender: relm4::FactorySender<Self>,
) -> Self {
init
}
view! {
InfoRow {
set_selectable: true,
set_name: self.name.clone(),
set_value: self.value.clone(),
set_info_text: self.note.unwrap_or_default(),
}
}
}
impl Default for InfoRow {
fn default() -> Self {
Object::builder().build()
+1 -1
View File
@@ -59,7 +59,7 @@ mod imp {
#[local_ref]
obj {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
set_spacing: 10,
set_margin_horizontal: 5,
#[local_ref]
+80 -51
View File
@@ -1,24 +1,21 @@
mod hardware_info;
mod vulkan_info;
use self::hardware_info::HardwareInfoSection;
use super::{values_grid, PageUpdate};
use crate::app::page_section::PageSection;
use super::PageUpdate;
use crate::app::{info_row::InfoRow, page_section::PageSection};
use gtk::prelude::*;
use relm4::{Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt};
use vulkan_info::VulkanInfoFrame;
use lact_schema::{DeviceInfo, DeviceStats};
use relm4::{prelude::FactoryVecDeque, ComponentParts, ComponentSender, RelmWidgetExt};
use std::sync::Arc;
pub struct InformationPage {
hardware_info: relm4::Controller<HardwareInfoSection>,
vulkan_info: VulkanInfoFrame,
values_list: FactoryVecDeque<InfoRowItem>,
device_info: Option<Arc<DeviceInfo>>,
device_stats: Option<Arc<DeviceStats>>,
}
#[relm4::component(pub)]
impl Component for InformationPage {
impl relm4::SimpleComponent for InformationPage {
type Init = ();
type Input = PageUpdate;
type Output = ();
type CommandOutput = ();
view! {
gtk::ScrolledWindow {
@@ -29,23 +26,12 @@ impl Component for InformationPage {
set_spacing: 15,
set_margin_horizontal: 20,
model.hardware_info.widget(),
#[name = "vulkan_section"]
PageSection::new("Vulkan Information") -> PageSection {
set_spacing: 10,
set_margin_start: 15,
append = &model.vulkan_info.container.clone(),
PageSection::new("Hardware Information") {
append = &model.values_list.widget().clone() -> gtk::Box {
set_spacing: 10,
set_orientation: gtk::Orientation::Vertical,
}
},
#[name = "vulkan_unavailable_label"]
gtk::Label {
set_label: "Vulkan is not available on this GPU",
set_visible: false,
set_margin_horizontal: 10,
set_halign: gtk::Align::Start,
}
}
}
}
@@ -55,12 +41,10 @@ impl Component for InformationPage {
root: Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let hardware_info = HardwareInfoSection::builder().launch(()).detach();
let vulkan_info = VulkanInfoFrame::new();
let model = Self {
hardware_info,
vulkan_info,
values_list: FactoryVecDeque::builder().launch_default().detach(),
device_info: None,
device_stats: None,
};
let widgets = view_output!();
@@ -68,27 +52,72 @@ impl Component for InformationPage {
ComponentParts { model, widgets }
}
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
_sender: ComponentSender<Self>,
_root: &Self::Root,
) {
self.hardware_info.emit(msg.clone());
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
PageUpdate::Info(gpu_info) => {
if let Some(vulkan_info) = &gpu_info.vulkan_info {
self.vulkan_info.set_info(vulkan_info);
self.vulkan_info.container.show();
widgets.vulkan_unavailable_label.hide();
} else {
self.vulkan_info.container.hide();
widgets.vulkan_unavailable_label.show();
PageUpdate::Info(device_info) => {
self.device_info = Some(device_info);
}
PageUpdate::Stats(device_stats) => {
self.device_stats = Some(device_stats);
}
}
self.update_items();
}
}
impl InformationPage {
fn update_items(&mut self) {
self.values_list.guard().clear();
if let Some(info) = &self.device_info {
let mut values_list = self.values_list.guard();
for (name, value) in info.info_elements(self.device_stats.as_deref()) {
if let Some(value) = value {
let note = if name == "Card Model" && !value.starts_with("Unknown ") {
Some("The card displayed here may be of a sibling model, e.g. XT vs XTX variety. This is normal, as such models often use the same device ID, and it is not possible to differentiate between them.")
} else {
None
};
values_list.push_back(InfoRowItem {
name: format!("{name}:"),
value,
note,
});
}
}
PageUpdate::Stats(_stats) => {}
}
}
}
struct InfoRowItem {
name: String,
value: String,
note: Option<&'static str>,
}
#[relm4::factory]
impl relm4::factory::FactoryComponent for InfoRowItem {
type Init = Self;
type ParentWidget = gtk::Box;
type CommandOutput = ();
type Input = ();
type Output = ();
fn init_model(
init: Self::Init,
_index: &Self::Index,
_sender: relm4::FactorySender<Self>,
) -> Self {
init
}
view! {
InfoRow {
set_selectable: true,
set_name: self.name.clone(),
set_value: self.value.clone(),
set_info_text: self.note.unwrap_or_default(),
}
}
}
@@ -1,116 +0,0 @@
use crate::app::{info_row::InfoRow, page_section::PageSection, pages::PageUpdate};
use gtk::prelude::{BoxExt, OrientableExt, WidgetExt};
use lact_schema::{DeviceInfo, DeviceStats};
use relm4::RelmWidgetExt;
use relm4::{prelude::FactoryVecDeque, ComponentParts, ComponentSender};
use std::sync::Arc;
pub struct HardwareInfoSection {
values_list: FactoryVecDeque<InfoRowItem>,
device_info: Option<Arc<DeviceInfo>>,
device_stats: Option<Arc<DeviceStats>>,
}
#[relm4::component(pub)]
impl relm4::SimpleComponent for HardwareInfoSection {
type Init = ();
type Input = PageUpdate;
type Output = ();
view! {
PageSection::new("Hardware Information") {
set_margin_start: 15,
append = &model.values_list.widget().clone() -> gtk::Box {
set_spacing: 10,
set_orientation: gtk::Orientation::Vertical,
set_margin_horizontal: 5,
}
}
}
fn init(
_init: Self::Init,
root: Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self {
values_list: FactoryVecDeque::builder().launch_default().detach(),
device_info: None,
device_stats: None,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
PageUpdate::Info(device_info) => {
self.device_info = Some(device_info);
}
PageUpdate::Stats(device_stats) => {
self.device_stats = Some(device_stats);
}
}
self.update_items();
}
}
impl HardwareInfoSection {
fn update_items(&mut self) {
self.values_list.guard().clear();
if let Some(info) = &self.device_info {
let mut values_list = self.values_list.guard();
for (name, value) in info.info_elements(self.device_stats.as_deref()) {
if let Some(value) = value {
let note = if name == "Card Model" && !value.starts_with("Unknown ") {
Some("The card displayed here may be of a sibling model, e.g. XT vs XTX variety. This is normal, as such models often use the same device ID, and it is not possible to differentiate between them.")
} else {
None
};
values_list.push_back(InfoRowItem {
name: format!("{name}:"),
value,
note,
});
}
}
}
}
}
struct InfoRowItem {
name: String,
value: String,
note: Option<&'static str>,
}
#[relm4::factory]
impl relm4::factory::FactoryComponent for InfoRowItem {
type Init = Self;
type ParentWidget = gtk::Box;
type CommandOutput = ();
type Input = ();
type Output = ();
fn init_model(
init: Self::Init,
_index: &Self::Index,
_sender: relm4::FactorySender<Self>,
) -> Self {
init
}
view! {
InfoRow {
set_selectable: true,
set_name: self.name.clone(),
set_value: self.value.clone(),
set_info_text: self.note.unwrap_or_default(),
}
}
}
@@ -1,124 +0,0 @@
mod feature_window;
use super::values_grid;
use crate::app::pages::{label_row, values_row};
use feature_window::{VulkanFeature, VulkanFeaturesWindow};
use glib::clone;
use gtk::prelude::*;
use gtk::*;
use lact_client::schema::VulkanInfo;
use relm4::{Component, ComponentController};
use std::cell::RefCell;
use std::rc::Rc;
use tracing::trace;
#[derive(Clone, Debug)]
pub struct VulkanInfoFrame {
pub container: Box,
device_name_label: Label,
version_label: Label,
driver_name_label: Label,
driver_version_label: Label,
features: Rc<RefCell<Vec<VulkanFeature>>>,
extensions: Rc<RefCell<Vec<VulkanFeature>>>,
}
impl VulkanInfoFrame {
pub fn new() -> Self {
let container = Box::new(Orientation::Vertical, 0);
let features: Rc<RefCell<Vec<VulkanFeature>>> = Rc::default();
let extensions: Rc<RefCell<Vec<VulkanFeature>>> = Rc::default();
let grid = values_grid();
grid.set_margin_start(0);
grid.set_margin_end(0);
let device_name_label = label_row("Device name", &grid, 0, 0, true);
let version_label = label_row("Vulkan version:", &grid, 1, 0, true);
let driver_name_label = label_row("Driver name:", &grid, 2, 0, true);
let driver_version_label = label_row("Driver version:", &grid, 3, 0, true);
let show_features_button = Button::builder().label("Show").halign(Align::End).build();
show_features_button.connect_clicked(clone!(
#[strong]
features,
move |_| {
show_features_window("Vulkan features", features.clone());
}
));
values_row("Features:", &grid, &show_features_button, 4, 0);
let show_extensions_button = Button::builder().label("Show").halign(Align::End).build();
show_extensions_button.connect_clicked(clone!(
#[strong]
extensions,
move |_| {
show_features_window("Vulkan extensions", extensions.clone());
}
));
values_row("Extensions:", &grid, &show_extensions_button, 5, 0);
container.append(&grid);
Self {
container,
device_name_label,
version_label,
driver_name_label,
driver_version_label,
features,
extensions,
}
}
pub fn set_info(&self, vulkan_info: &VulkanInfo) {
trace!("setting vulkan info: {:?}", vulkan_info);
self.device_name_label
.set_markup(&format!("<b>{}</b>", vulkan_info.device_name));
self.version_label
.set_markup(&format!("<b>{}</b>", vulkan_info.api_version));
self.driver_name_label.set_markup(&format!(
"<b>{}</b>",
vulkan_info.driver.name.as_deref().unwrap_or_default(),
));
self.driver_version_label.set_markup(&format!(
"<b>{}</b>",
vulkan_info.driver.info.as_deref().unwrap_or_default(),
));
let mut features = self.features.borrow_mut();
features.clear();
for (name, supported) in &vulkan_info.features {
let feature = VulkanFeature {
name: name.to_string(),
supported: *supported,
};
features.push(feature);
}
let mut extensions = self.extensions.borrow_mut();
extensions.clear();
for (name, supported) in &vulkan_info.extensions {
let extension = VulkanFeature {
name: name.to_string(),
supported: *supported,
};
extensions.push(extension);
}
}
}
fn show_features_window(title: &str, values: Rc<RefCell<Vec<VulkanFeature>>>) {
let features = values.borrow().iter().cloned().collect();
let mut window_controller = VulkanFeaturesWindow::builder()
.launch((features, title.to_owned()))
.detach();
window_controller.detach_runtime();
window_controller.widget().present();
}
@@ -19,8 +19,6 @@ impl relm4::SimpleComponent for GpuStatsSection {
view! {
PageSection::new("Statistics") {
set_spacing: 10,
append = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
+218 -20
View File
@@ -1,30 +1,190 @@
use crate::{app::info_row::InfoRow, GUI_VERSION};
use gtk::prelude::*;
use lact_client::schema::{SystemInfo, GIT_COMMIT};
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
use std::{fmt::Write, rc::Rc};
mod vulkan;
pub struct SoftwarePage {}
use crate::{
app::{format_friendly_size, info_row::InfoRow, page_section::PageSection},
GUI_VERSION,
};
use gtk::prelude::*;
use indexmap::IndexMap;
use lact_client::schema::{SystemInfo, GIT_COMMIT};
use lact_schema::DeviceInfo;
use relm4::{Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt};
use std::{fmt::Write, rc::Rc, sync::Arc};
use vulkan::feature_window::{VulkanFeature, VulkanFeaturesWindow};
pub struct SoftwarePage {
device_info: Option<Arc<DeviceInfo>>,
}
#[derive(Debug)]
pub enum SoftwarePageMsg {
DeviceInfo(Arc<DeviceInfo>),
ShowVulkanFeatures,
ShowVulkanExtensions,
}
#[relm4::component(pub)]
impl SimpleComponent for SoftwarePage {
impl relm4::SimpleComponent for SoftwarePage {
type Init = (Rc<SystemInfo>, bool);
type Input = ();
type Input = SoftwarePageMsg;
type Output = ();
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
set_margin_start: 5,
set_margin_end: 5,
set_margin_top: 5,
set_margin_bottom: 5,
gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
append = &InfoRow::new_selectable("LACT Daemon:", &daemon_version),
append = &InfoRow::new_selectable("LACT GUI:", &gui_version),
append = &InfoRow::new_selectable("Kernel Version:", &system_info.kernel_version),
}
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 15,
set_margin_horizontal: 20,
PageSection::new("System") {
append = &InfoRow::new_selectable("LACT Daemon:", &daemon_version),
append = &InfoRow::new_selectable("LACT GUI:", &gui_version),
append = &InfoRow::new_selectable("Kernel Version:", &system_info.kernel_version),
},
match model.device_info.as_ref().and_then(|info| info.vulkan_info.as_ref()) {
Some(info) => {
PageSection::new("Vulkan") {
append = &InfoRow {
set_name: "Device Name:",
#[watch]
set_value: info.device_name.as_str(),
set_selectable: true,
},
append = &InfoRow {
set_name: "API Version:",
#[watch]
set_value: info.api_version.as_str(),
set_selectable: true,
},
append = &InfoRow {
set_name: "Driver Name:",
#[watch]
set_value: info.driver.name.as_deref().unwrap_or_default(),
set_selectable: true,
},
append = &InfoRow {
set_name: "Driver Version:",
#[watch]
set_value: info.driver.info.as_deref().unwrap_or_default(),
set_selectable: true,
},
append = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
append = &gtk::Label {
set_halign: gtk::Align::Start,
set_hexpand: true,
set_label: "Features:"
},
append = &gtk::Button {
connect_clicked => SoftwarePageMsg::ShowVulkanFeatures,
set_label: "Show",
}
},
append = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_hexpand: true,
append = &gtk::Label {
set_halign: gtk::Align::Start,
set_hexpand: true,
set_label: "Extensions:"
},
append = &gtk::Button {
connect_clicked => SoftwarePageMsg::ShowVulkanExtensions,
set_label: "Show",
}
},
}
}
None => {
PageSection::new("Vulkan") {
append = &gtk::Label {
set_label: "Vulkan device not found",
set_halign: gtk::Align::Start,
},
}
}
},
match model.device_info.as_ref().and_then(|info| info.opencl_info.as_ref()) {
Some(info) => {
PageSection::new("OpenCL") {
append = &InfoRow {
set_name: "Platform Name:",
#[watch]
set_value: info.platform_name.as_str(),
set_selectable: true,
},
append = &InfoRow {
set_name: "Device Name:",
#[watch]
set_value: info.device_name.as_str(),
set_selectable: true,
},
append = &InfoRow {
set_name: "Version:",
#[watch]
set_value: info.version.as_str(),
set_selectable: true,
},
append = &InfoRow {
set_name: "Driver Version:",
#[watch]
set_value: info.driver_version.as_str(),
set_selectable: true,
},
append = &InfoRow {
set_name: "OpenCL C Version:",
#[watch]
set_value: info.c_version.as_str(),
set_selectable: true,
},
append = &InfoRow {
set_name: "Compute Units:",
#[watch]
set_value: info.compute_units.to_string(),
set_selectable: true,
},
append = &InfoRow {
set_name: "Workgroup Size:",
#[watch]
set_value: info.workgroup_size.to_string(),
set_selectable: true,
},
append = &InfoRow {
set_name: "Global Memory:",
#[watch]
set_value: format_friendly_size(info.global_memory),
set_selectable: true,
},
append = &InfoRow {
set_name: "Local Memory:",
#[watch]
set_value: format_friendly_size(info.local_memory),
set_selectable: true,
},
}
}
None => {
PageSection::new("OpenCL") {
append = &gtk::Label {
set_label: "OpenCL device not found",
set_halign: gtk::Align::Start,
},
}
}
},
}
},
}
fn init(
@@ -32,7 +192,7 @@ impl SimpleComponent for SoftwarePage {
root: Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self {};
let model = Self { device_info: None };
let mut daemon_version = format!("{}-{}", system_info.version, system_info.profile);
if embedded {
@@ -53,4 +213,42 @@ impl SimpleComponent for SoftwarePage {
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
SoftwarePageMsg::DeviceInfo(info) => {
self.device_info = Some(info);
}
SoftwarePageMsg::ShowVulkanFeatures => {
if let Some(info) = &self.device_info {
if let Some(vulkan_info) = &info.vulkan_info {
show_features_window("Vulkan Features", &vulkan_info.features);
}
}
}
SoftwarePageMsg::ShowVulkanExtensions => {
if let Some(info) = &self.device_info {
if let Some(vulkan_info) = &info.vulkan_info {
show_features_window("Vulkan Extensions", &vulkan_info.extensions);
}
}
}
}
}
}
fn show_features_window(title: &str, values: &IndexMap<String, bool>) {
let values = values
.into_iter()
.map(|(name, &supported)| VulkanFeature {
name: name.clone(),
supported,
})
.collect();
let mut window_controller = VulkanFeaturesWindow::builder()
.launch((values, title.to_owned()))
.detach();
window_controller.detach_runtime();
window_controller.widget().present();
}
@@ -0,0 +1 @@
pub mod feature_window;
+14
View File
@@ -97,6 +97,7 @@ pub struct GpuPciInfo {
pub struct DeviceInfo {
pub pci_info: Option<GpuPciInfo>,
pub vulkan_info: Option<VulkanInfo>,
pub opencl_info: Option<OpenCLInfo>,
pub driver: String,
pub vbios_version: Option<String>,
pub link_info: LinkInfo,
@@ -411,6 +412,19 @@ pub struct VulkanDriverInfo {
pub driver_version: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct OpenCLInfo {
pub platform_name: String,
pub device_name: String,
pub version: String,
pub driver_version: String,
pub c_version: String,
pub compute_units: u32,
pub workgroup_size: usize,
pub global_memory: u64,
pub local_memory: u64,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PciInfo {
+2
View File
@@ -1,2 +1,4 @@
FROM quay.io/rockylinux/rockylinux:8
RUN dnf install curl --allowerasing -y # Fix for curl-minimal conflict
RUN dnf install -y dnf-plugins-core rocky-repos
RUN dnf config-manager --enable devel
+2
View File
@@ -1,2 +1,4 @@
FROM quay.io/rockylinux/rockylinux:9
RUN dnf install curl --allowerasing -y # Fix for curl-minimal conflict
RUN dnf install -y dnf-plugins-core rocky-repos
RUN dnf config-manager --enable devel
+9 -9
View File
@@ -12,16 +12,16 @@ metadata:
conflicts: [ lact, lact-libadwaita ]
depends:
all: [ hwdata, vulkan-tools ]
debian-12+ubuntu-2204+ubuntu-2404: [ libdrm-amdgpu1, libdrm2 ]
fedora-40+fedora-41+fedora-42+rhel-8+rhel-9: [ libdrm ]
arch: [ libdrm ]
opensuse-tumbleweed: [ libdrm ]
debian-12+ubuntu-2204+ubuntu-2404: [ libdrm-amdgpu1, libdrm2, ocl-icd-opencl-dev ]
fedora-40+fedora-41+fedora-42+rhel-8+rhel-9: [ libdrm, ocl-icd-devel ]
arch: [ libdrm, ocl-icd ]
opensuse-tumbleweed: [ libdrm, ocl-icd-devel ]
build_depends:
all: [ curl, make, clang, git, vulkan-tools ]
debian-12+ubuntu-2204+ubuntu-2404: [ pkg-config, build-essential, libdrm-dev, dbus ]
fedora-40+fedora-41+fedora-42+rhel-8+rhel-9: [ gcc, libdrm-devel, dbus ]
arch: [ libdrm, dbus ]
opensuse-tumbleweed: [ libdrm-devel ]
debian-12+ubuntu-2204+ubuntu-2404: [ pkg-config, build-essential, libdrm-dev, dbus, ocl-icd-opencl-dev ]
fedora-40+fedora-41+fedora-42+rhel-8+rhel-9: [ gcc, libdrm-devel, dbus, ocl-icd-devel ]
arch: [ libdrm, dbus, ocl-icd ]
opensuse-tumbleweed: [ libdrm-devel, ocl-icd-devel ]
all_images: true
env:
RUSTUP_URL: https://sh.rustup.rs
@@ -29,7 +29,7 @@ configure:
steps:
- cmd: curl -o /tmp/install_rust.sh $RUSTUP_URL
- cmd: sh /tmp/install_rust.sh -y
- cmd: bash -c "source $HOME/.cargo/env && rustup toolchain install 1.82.0"
- cmd: bash -c "source $HOME/.cargo/env && rustup toolchain install 1.85.1"
- cmd: pacman -Syu --noconfirm
pkg: true
build:
+7 -7
View File
@@ -12,14 +12,14 @@ metadata:
conflicts: [ lact, lact-headless ]
depends:
all: [ hwdata, vulkan-tools ]
fedora-40+fedora-41+fedora-42: [ gtk4, libdrm, libadwaita ]
arch: [ gtk4, libadwaita ]
opensuse-tumbleweed: [ libdrm, libadwaita ]
fedora-40+fedora-41+fedora-42: [ gtk4, libdrm, libadwaita ocl-icd-devel ]
arch: [ gtk4, libadwaita, ocl-icd ]
opensuse-tumbleweed: [ libdrm, libadwaita, ocl-icd-devel ]
build_depends:
all: [ curl, make, clang, git, vulkan-tools ]
fedora-40+fedora-41+fedora-42: [ gtk4-devel, gcc, libdrm-devel, libadwaita-devel, dbus ]
arch: [ gtk4, libadwaita, dbus ]
opensuse-tumbleweed: [ libdrm-devel, libadwaita-devel ]
fedora-40+fedora-41+fedora-42: [ gtk4-devel, gcc, libdrm-devel, libadwaita-devel, dbus, ocl-icd-devel ]
arch: [ gtk4, libadwaita, dbus, ocl-icd ]
opensuse-tumbleweed: [ libdrm-devel, libadwaita-devel, ocl-icd-devel ]
all_images: true
env:
RUSTUP_URL: https://sh.rustup.rs
@@ -27,7 +27,7 @@ configure:
steps:
- cmd: curl -o /tmp/install_rust.sh $RUSTUP_URL
- cmd: sh /tmp/install_rust.sh -y
- cmd: bash -c "source $HOME/.cargo/env && rustup toolchain install 1.82.0"
- cmd: bash -c "source $HOME/.cargo/env && rustup toolchain install 1.85.1"
- cmd: pacman -Syu --noconfirm
pkg: true
build:
+9 -9
View File
@@ -12,16 +12,16 @@ metadata:
conflicts: [ lact-headless, lact-libadwaita ]
depends:
all: [ hwdata, vulkan-tools ]
debian-12+ubuntu-2204+ubuntu-2404: [ libgtk-4-1, libdrm2, libdrm-amdgpu1 ]
fedora-40+fedora-41+fedora-42: [ gtk4, libdrm ]
arch: [ gtk4 ]
opensuse-tumbleweed: [ gtk4 ]
debian-12+ubuntu-2204+ubuntu-2404: [ libgtk-4-1, libdrm2, libdrm-amdgpu1, ocl-icd-opencl-dev ]
fedora-40+fedora-41+fedora-42: [ gtk4, libdrm, ocl-icd-devel ]
arch: [ gtk4, ocl-icd ]
opensuse-tumbleweed: [ gtk4, ocl-icd-devel ]
build_depends:
all: [ curl, make, clang, git, vulkan-tools ]
debian-12+ubuntu-2204+ubuntu-2404: [ libgtk-4-dev, pkg-config, build-essential, libdrm-dev, dbus ]
fedora-40+fedora-41+fedora-42: [ gtk4-devel, gcc, libdrm-devel, dbus ]
arch: [ gtk4, dbus ]
opensuse-tumbleweed: [ gtk4-devel, libdrm-devel ]
debian-12+ubuntu-2204+ubuntu-2404: [ libgtk-4-dev, pkg-config, build-essential, libdrm-dev, dbus, ocl-icd-opencl-dev ]
fedora-40+fedora-41+fedora-42: [ gtk4-devel, gcc, libdrm-devel, dbus, ocl-icd-devel ]
arch: [ gtk4, dbus, ocl-icd ]
opensuse-tumbleweed: [ gtk4-devel, libdrm-devel, ocl-icd-devel ]
all_images: true
env:
RUSTUP_URL: https://sh.rustup.rs
@@ -29,7 +29,7 @@ configure:
steps:
- cmd: curl -o /tmp/install_rust.sh $RUSTUP_URL
- cmd: sh /tmp/install_rust.sh -y
- cmd: bash -c "source $HOME/.cargo/env && rustup toolchain install 1.82.0"
- cmd: bash -c "source $HOME/.cargo/env && rustup toolchain install 1.85.1"
- cmd: pacman -Syu --noconfirm
pkg: true
build:
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.82.0"
channel = "1.85.1"