refactor: make info page a relm component (#404)

* refactor: drop RootStack

* refactor: make InformationPage a relm component
This commit is contained in:
Ilya Zlobintsev 2024-11-07 22:35:23 +02:00 committed by GitHub
parent 41f26c7f34
commit 23ab0e86e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 240 additions and 232 deletions

View File

@ -23,7 +23,6 @@ use lact_schema::{
use libdrm_amdgpu_sys::AMDGPU::ThrottlerBit;
use pciid_parser::Database;
use std::{
borrow::Cow,
cell::RefCell,
cmp,
collections::{HashMap, HashSet},
@ -551,7 +550,7 @@ impl GpuController for AmdGpuController {
}
}
});
let pci_info = self.pci_info.as_ref().map(Cow::Borrowed);
let pci_info = self.pci_info.clone();
let driver = self.handle.get_driver().to_owned();
let vbios_version = self.get_full_vbios_version();
let link_info = self.get_link_info();

View File

@ -21,7 +21,6 @@ use nvml_wrapper::{
Device, Nvml,
};
use std::{
borrow::Cow,
cell::RefCell,
collections::HashMap,
fmt::Write,
@ -292,7 +291,7 @@ impl GpuController for NvidiaGpuController {
};
DeviceInfo {
pci_info: Some(Cow::Borrowed(&self.pci_info)),
pci_info: Some(self.pci_info.clone()),
vulkan_info,
driver: format!(
"nvidia {}",

View File

@ -306,7 +306,7 @@ impl<'a> Handler {
.collect()
}
pub fn get_device_info(&'a self, id: &str) -> anyhow::Result<DeviceInfo<'a>> {
pub fn get_device_info(&'a self, id: &str) -> anyhow::Result<DeviceInfo> {
Ok(self.controller_by_id(id)?.get_info())
}

View File

@ -5,7 +5,7 @@ mod header;
mod info_row;
mod msg;
mod page_section;
mod root_stack;
mod pages;
use crate::{APP_ID, GUI_VERSION};
use anyhow::{anyhow, Context};
@ -30,12 +30,15 @@ use lact_schema::{
FanOptions, GIT_COMMIT,
};
use msg::AppMsg;
use pages::{
info_page::InformationPage, oc_page::OcPage, software_page::SoftwarePage,
thermals_page::ThermalsPage, PageUpdate,
};
use relm4::{
actions::{RelmAction, RelmActionGroup},
prelude::{AsyncComponent, AsyncComponentParts},
tokio, AsyncComponentSender, Component, ComponentController,
};
use root_stack::RootStack;
use std::{os::unix::net::UnixStream, rc::Rc, sync::atomic::AtomicBool, time::Duration};
use tracing::{debug, error, info, trace, warn};
@ -44,7 +47,12 @@ const STATS_POLL_INTERVAL_MS: u64 = 250;
pub struct AppModel {
daemon_client: DaemonClient,
graphs_window: GraphsWindow,
root_stack: RootStack,
info_page: relm4::Controller<InformationPage>,
oc_page: OcPage,
thermals_page: ThermalsPage,
software_page: relm4::Controller<SoftwarePage>,
header: relm4::Controller<Header>,
apply_revealer: relm4::Controller<ApplyRevealer>,
stats_task_handle: Option<glib::JoinHandle<()>>,
@ -72,7 +80,19 @@ impl AsyncComponent for AppModel {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
model.root_stack.container.clone(),
#[name = "root_stack"]
gtk::Stack {
set_vexpand: true,
set_margin_top: 15,
set_margin_start: 30,
set_margin_end: 30,
add_titled[Some("info_page"), "Information"] = model.info_page.widget(),
add_titled[Some("oc_page"), "OC"] = &model.oc_page.container.clone(),
add_titled[Some("thermals_page"), "Thermals"] = &model.thermals_page.container.clone(),
add_titled[Some("software_page"), "Software"] = model.software_page.widget(),
},
model.apply_revealer.widget(),
}
},
@ -145,24 +165,31 @@ impl AsyncComponent for AppModel {
sender.input(AppMsg::Error(err.into()));
}
let root_stack = RootStack::new(system_info, daemon_client.embedded);
let info_page = InformationPage::builder().launch(()).detach();
let oc_page = OcPage::new(&system_info);
let thermals_page = ThermalsPage::new(&system_info);
let software_page = SoftwarePage::builder()
.launch((system_info, daemon_client.embedded))
.detach();
let header = Header::builder()
.launch((devices, root_stack.container.clone()))
.launch(devices)
.forward(sender.input_sender(), |msg| msg);
let apply_revealer = ApplyRevealer::builder()
.launch(())
.forward(sender.input_sender(), |msg| msg);
root_stack.oc_page.clocks_frame.connect_clocks_reset(clone!(
oc_page.clocks_frame.connect_clocks_reset(clone!(
#[strong]
sender,
move || {
sender.input(AppMsg::ResetClocks);
}
));
root_stack.thermals_page.connect_reset_pmfw(clone!(
thermals_page.connect_reset_pmfw(clone!(
#[strong]
sender,
move || {
@ -170,7 +197,7 @@ impl AsyncComponent for AppModel {
}
));
if let Some(ref button) = root_stack.oc_page.enable_overclocking_button {
if let Some(ref button) = oc_page.enable_overclocking_button {
button.connect_clicked(clone!(
#[strong]
sender,
@ -190,7 +217,10 @@ impl AsyncComponent for AppModel {
let model = AppModel {
daemon_client,
graphs_window,
root_stack,
info_page,
oc_page,
thermals_page,
software_page,
apply_revealer,
header,
stats_task_handle: None,
@ -202,6 +232,9 @@ impl AsyncComponent for AppModel {
show_embedded_info(&root, err);
}
model
.header
.emit(HeaderMsg::Stack(widgets.root_stack.clone()));
sender.input(AppMsg::ReloadProfiles);
AsyncComponentParts { model, widgets }
@ -260,9 +293,10 @@ impl AppModel {
sender.input(AppMsg::ReloadProfiles);
}
AppMsg::Stats(stats) => {
self.root_stack.info_page.set_stats(&stats);
self.root_stack.thermals_page.set_stats(&stats, false);
self.root_stack.oc_page.set_stats(&stats, false);
self.info_page.emit(PageUpdate::Stats(stats.clone()));
self.thermals_page.set_stats(&stats, false);
self.oc_page.set_stats(&stats, false);
self.graphs_window.set_stats(&stats);
}
AppMsg::ApplyChanges => {
@ -355,10 +389,11 @@ impl AppModel {
.get_device_info(&gpu_id)
.await
.context("Could not fetch info")?;
let info = info_buf.inner()?;
let info = Rc::new(info_buf.inner()?);
self.root_stack.info_page.set_info(&info);
self.root_stack.oc_page.set_info(&info);
self.info_page.emit(PageUpdate::Info(info.clone()));
self.oc_page.set_info(&info);
let vram_clock_ratio = info
.drm_info
@ -369,7 +404,7 @@ impl AppModel {
self.update_gpu_data(gpu_id, sender).await?;
self.root_stack.thermals_page.set_info(&info);
self.thermals_page.set_info(&info);
self.graphs_window.clear();
@ -393,10 +428,12 @@ impl AppModel {
.await
.context("Could not fetch stats")?
.inner()?;
let stats = Rc::new(stats);
self.root_stack.oc_page.set_stats(&stats, true);
self.root_stack.thermals_page.set_stats(&stats, true);
self.root_stack.info_page.set_stats(&stats);
self.oc_page.set_stats(&stats, true);
self.thermals_page.set_stats(&stats, true);
self.info_page.emit(PageUpdate::Stats(stats));
let maybe_clocks_table = match self.daemon_client.get_device_clocks_info(&gpu_id).await {
Ok(clocks_buf) => match clocks_buf.inner() {
@ -411,7 +448,7 @@ impl AppModel {
None
}
};
self.root_stack.oc_page.set_clocks_table(maybe_clocks_table);
self.oc_page.set_clocks_table(maybe_clocks_table);
let maybe_modes_table = match self
.daemon_client
@ -430,8 +467,7 @@ impl AppModel {
None
}
};
self.root_stack
.oc_page
self.oc_page
.performance_frame
.set_power_profile_modes(maybe_modes_table);
@ -442,8 +478,7 @@ impl AppModel {
.and_then(|states| states.inner())
{
Ok(power_states) => {
self.root_stack
.oc_page
self.oc_page
.power_states_frame
.set_power_states(power_states);
}
@ -460,13 +495,10 @@ impl AppModel {
}
);
self.root_stack
.thermals_page
self.thermals_page
.connect_settings_changed(show_revealer.clone());
self.root_stack
.oc_page
.connect_settings_changed(show_revealer);
self.oc_page.connect_settings_changed(show_revealer);
self.apply_revealer
.sender()
@ -492,7 +524,7 @@ impl AppModel {
debug!("applying settings on gpu {gpu_id}");
if let Some(cap) = self.root_stack.oc_page.get_power_cap() {
if let Some(cap) = self.oc_page.get_power_cap() {
self.daemon_client
.set_power_cap(&gpu_id, Some(cap))
.await
@ -514,7 +546,7 @@ impl AppModel {
.await
.context("Could not commit config")?;
if let Some(level) = self.root_stack.oc_page.get_performance_level() {
if let Some(level) = self.oc_page.get_performance_level() {
self.daemon_client
.set_performance_level(&gpu_id, level)
.await
@ -525,12 +557,10 @@ impl AppModel {
.context("Could not commit config")?;
let mode_index = self
.root_stack
.oc_page
.performance_frame
.get_selected_power_profile_mode();
let custom_heuristics = self
.root_stack
.oc_page
.performance_frame
.get_power_profile_mode_custom_heuristics();
@ -545,7 +575,7 @@ impl AppModel {
.context("Could not commit config")?;
}
if let Some(thermals_settings) = self.root_stack.thermals_page.get_thermals_settings() {
if let Some(thermals_settings) = self.thermals_page.get_thermals_settings() {
debug!("applying thermal settings: {thermals_settings:?}");
let opts = FanOptions {
id: &gpu_id,
@ -568,7 +598,7 @@ impl AppModel {
.context("Could not commit config")?;
}
let clocks_settings = self.root_stack.oc_page.clocks_frame.get_settings();
let clocks_settings = self.oc_page.clocks_frame.get_settings();
let mut clocks_commands = Vec::new();
debug!("applying clocks settings {clocks_settings:#?}");
@ -601,7 +631,7 @@ impl AppModel {
clocks_commands.push(SetClocksCommand::VoltageOffset(offset));
}
let enabled_power_states = self.root_stack.oc_page.get_enabled_power_states();
let enabled_power_states = self.oc_page.get_enabled_power_states();
for (kind, states) in enabled_power_states {
if !states.is_empty() {
@ -896,7 +926,7 @@ fn start_stats_update_loop(
.and_then(|buffer| buffer.inner())
{
Ok(stats) => {
sender.input(AppMsg::Stats(stats));
sender.input(AppMsg::Stats(Rc::new(stats)));
}
Err(err) => {
error!("could not fetch stats: {err:#}");

View File

@ -17,10 +17,12 @@ pub struct Header {
gpu_selector: TypedListView<GpuListItem, gtk::SingleSelection>,
profile_selector: TypedListView<ProfileListItem, gtk::SingleSelection>,
selector_label: String,
stack: Option<Stack>,
}
#[derive(Debug)]
pub enum HeaderMsg {
Stack(Stack),
Profiles(ProfilesInfo),
SelectProfile,
SelectGpu,
@ -30,7 +32,7 @@ pub enum HeaderMsg {
#[relm4::component(pub)]
impl Component for Header {
type Init = (Vec<DeviceListEntry>, gtk::Stack);
type Init = Vec<DeviceListEntry>;
type Input = HeaderMsg;
type Output = AppMsg;
type CommandOutput = ();
@ -41,7 +43,8 @@ impl Component for Header {
#[wrap(Some)]
set_title_widget = &StackSwitcher {
set_stack: Some(&stack),
#[watch]
set_stack: model.stack.as_ref(),
},
#[name = "menu_button"]
@ -137,7 +140,7 @@ impl Component for Header {
}
fn init(
(variants, stack): Self::Init,
variants: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
@ -171,6 +174,7 @@ impl Component for Header {
gpu_selector,
profile_selector,
selector_label: String::new(),
stack: None,
};
let gpu_selector = &model.gpu_selector.view;
@ -208,6 +212,9 @@ impl Component for Header {
_root: &Self::Root,
) {
match msg {
HeaderMsg::Stack(stack) => {
self.stack = Some(stack);
}
HeaderMsg::Profiles(profiles_info) => {
let selected_index = match &profiles_info.current_profile {
Some(profile) => {

View File

@ -7,7 +7,7 @@ use std::rc::Rc;
pub enum AppMsg {
Error(Rc<anyhow::Error>),
ReloadData { full: bool },
Stats(DeviceStats),
Stats(Rc<DeviceStats>),
ApplyChanges,
RevertChanges,
ResetClocks,

49
lact-gui/src/app/pages.rs Normal file
View File

@ -0,0 +1,49 @@
pub mod info_page;
pub mod oc_adjustment;
pub mod oc_page;
pub mod software_page;
pub mod thermals_page;
use std::rc::Rc;
use gtk::{prelude::*, *};
use lact_schema::{DeviceInfo, DeviceStats};
#[derive(Debug)]
pub enum PageUpdate {
Info(Rc<DeviceInfo>),
Stats(Rc<DeviceStats>),
}
fn values_row<W: IsA<Widget>>(
title: &str,
parent: &Grid,
value_child: &W,
row: i32,
column_offset: i32,
) {
let title_label = Label::builder().label(title).halign(Align::Start).build();
parent.attach(&title_label, column_offset, row, 1, 1);
parent.attach(value_child, column_offset + 1, row, 1, 1);
}
fn label_row(title: &str, parent: &Grid, row: i32, column_offset: i32, selectable: bool) -> Label {
let value_label = Label::builder()
.halign(Align::End)
.hexpand(true)
.selectable(selectable)
.build();
values_row(title, parent, &value_label, row, column_offset);
value_label
}
fn values_grid() -> Grid {
Grid::builder()
.margin_start(10)
.margin_end(5)
.row_spacing(10)
.column_spacing(10)
.build()
}

View File

@ -0,0 +1,96 @@
mod hardware_info;
mod vulkan_info;
use self::hardware_info::HardwareInfoSection;
use super::{values_grid, PageUpdate};
use crate::app::page_section::PageSection;
use gtk::prelude::*;
use relm4::{Component, ComponentParts, ComponentSender, RelmWidgetExt};
use vulkan_info::VulkanInfoFrame;
pub struct InformationPage {
hardware_info: HardwareInfoSection,
vulkan_info: VulkanInfoFrame,
}
#[relm4::component(pub)]
impl Component for InformationPage {
type Init = ();
type Input = PageUpdate;
type Output = ();
type CommandOutput = ();
view! {
gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 15,
set_margin_horizontal: 20,
model.hardware_info.clone(),
#[name = "vulkan_section"]
PageSection::new("Vulkan Information") -> PageSection {
set_spacing: 10,
set_margin_start: 15,
append = &model.vulkan_info.container.clone(),
},
#[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,
}
}
}
}
fn init(
_init: Self::Init,
root: Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let hardware_info = HardwareInfoSection::new();
let vulkan_info = VulkanInfoFrame::new();
let model = Self {
hardware_info,
vulkan_info,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
_sender: ComponentSender<Self>,
_root: &Self::Root,
) {
match msg {
PageUpdate::Info(gpu_info) => {
self.hardware_info.set_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::Stats(stats) => {
self.hardware_info.set_stats(&stats);
}
}
}
}

View File

@ -22,7 +22,7 @@ impl HardwareInfoSection {
.drm_info
.as_ref()
.and_then(|drm| drm.device_name.as_deref())
.or_else(|| pci_info.device_pci_info.model.as_deref())
.or(pci_info.device_pci_info.model.as_deref())
.unwrap_or("Unknown")
.to_owned();

View File

@ -105,7 +105,7 @@ impl SimpleComponent for VulkanFeaturesWindow {
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct VulkanFeature {
pub name: String,
pub supported: bool,

View File

@ -1,7 +1,7 @@
mod feature_window;
use super::values_grid;
use crate::app::root_stack::{label_row, values_row};
use crate::app::pages::{label_row, values_row};
use feature_window::{VulkanFeature, VulkanFeaturesWindow};
use glib::clone;
use gtk::prelude::*;
@ -12,7 +12,7 @@ use std::cell::RefCell;
use std::rc::Rc;
use tracing::trace;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct VulkanInfoFrame {
pub container: Box,
device_name_label: Label,

View File

@ -1,7 +1,7 @@
mod adjustment_row;
use crate::app::page_section::PageSection;
use crate::app::root_stack::oc_adjustment::OcAdjustment;
use crate::app::pages::oc_adjustment::OcAdjustment;
use adjustment_row::AdjustmentRow;
use amdgpu_sysfs::gpu_handle::overdrive::{ClocksTable as _, ClocksTableGen as AmdClocksTable};
use glib::clone;

View File

@ -56,7 +56,7 @@ impl AdjustmentRow {
}
mod imp {
use crate::app::root_stack::oc_adjustment::OcAdjustment;
use crate::app::pages::oc_adjustment::OcAdjustment;
use glib::{clone, subclass::InitializingObject};
use gtk::{
glib::{self, Properties},

View File

@ -29,7 +29,7 @@ impl Default for PowerCapSection {
}
mod imp {
use crate::app::{page_section::PageSection, root_stack::oc_adjustment::OcAdjustment};
use crate::app::{page_section::PageSection, pages::oc_adjustment::OcAdjustment};
use gtk::{
glib::{self, clone, subclass::InitializingObject, types::StaticTypeExt, Properties},
prelude::{ButtonExt, ObjectExt},

View File

@ -66,7 +66,7 @@ impl Default for PowerStatesFrame {
}
mod imp {
use crate::app::root_stack::oc_page::power_states::power_states_list::PowerStatesList;
use crate::app::pages::oc_page::power_states::power_states_list::PowerStatesList;
use gtk::{
glib::{self, subclass::InitializingObject, types::StaticTypeExt, Properties},
prelude::ObjectExt,

View File

@ -1,4 +1,4 @@
use crate::app::root_stack::oc_page::power_states::power_state_row::PowerStateRow;
use crate::app::pages::oc_page::power_states::power_state_row::PowerStateRow;
use gtk::{
gio,
glib::{

View File

@ -1,7 +1,7 @@
mod point_adjustment;
use self::point_adjustment::PointAdjustment;
use crate::app::root_stack::oc_adjustment::OcAdjustment;
use crate::app::pages::oc_adjustment::OcAdjustment;
use glib::clone;
use gtk::graphene::Point;
use gtk::gsk::Transform;

View File

@ -1,4 +1,4 @@
use crate::app::root_stack::oc_adjustment::OcAdjustment;
use crate::app::pages::oc_adjustment::OcAdjustment;
use amdgpu_sysfs::gpu_handle::fan_control::FanInfo;
use gtk::{
glib::clone,

View File

@ -1,80 +0,0 @@
mod hardware_info;
mod vulkan_info;
use self::hardware_info::HardwareInfoSection;
use super::values_grid;
use crate::app::page_section::PageSection;
use gtk::prelude::*;
use gtk::*;
use lact_client::schema::{DeviceInfo, DeviceStats};
use vulkan_info::VulkanInfoFrame;
#[derive(Clone)]
pub struct InformationPage {
pub container: ScrolledWindow,
hardware_info: HardwareInfoSection,
vulkan_info_frame: VulkanInfoFrame,
vulkan_unavailable_label: Label,
}
impl InformationPage {
pub fn new() -> Self {
let vbox = Box::builder()
.orientation(Orientation::Vertical)
.spacing(15)
.margin_start(20)
.margin_end(20)
.build();
let hardware_info = HardwareInfoSection::new();
vbox.append(&hardware_info);
let vulkan_container = PageSection::new("Vulkan Information");
vulkan_container.set_spacing(10);
vulkan_container.set_margin_start(15);
let vulkan_info_frame = VulkanInfoFrame::new();
vulkan_container.append(&vulkan_info_frame.container);
let vulkan_unavailable_label = Label::builder()
.label("Vulkan is not available on this GPU")
.visible(false)
.margin_start(10)
.margin_end(10)
.halign(Align::Start)
.build();
vulkan_container.append(&vulkan_unavailable_label);
vbox.append(&vulkan_container);
let container = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never)
.child(&vbox)
.build();
Self {
container,
hardware_info,
vulkan_info_frame,
vulkan_unavailable_label,
}
}
pub fn set_info(&self, gpu_info: &DeviceInfo) {
self.hardware_info.set_info(gpu_info);
if let Some(vulkan_info) = &gpu_info.vulkan_info {
self.vulkan_info_frame.set_info(vulkan_info);
self.vulkan_info_frame.container.show();
self.vulkan_unavailable_label.hide();
} else {
self.vulkan_info_frame.container.hide();
self.vulkan_unavailable_label.show();
}
}
pub fn set_stats(&self, stats: &DeviceStats) {
self.hardware_info.set_stats(stats);
}
}

View File

@ -1,91 +0,0 @@
mod info_page;
mod oc_adjustment;
mod oc_page;
mod software_page;
mod thermals_page;
use gtk::{prelude::*, *};
use relm4::{Component, ComponentController};
use self::software_page::SoftwarePage;
use info_page::InformationPage;
use lact_client::schema::SystemInfo;
use oc_page::OcPage;
use thermals_page::ThermalsPage;
#[derive(Clone)]
pub struct RootStack {
pub container: Stack,
pub info_page: InformationPage,
pub thermals_page: ThermalsPage,
pub oc_page: OcPage,
}
impl RootStack {
pub fn new(system_info: SystemInfo, embedded_daemon: bool) -> Self {
let container = Stack::builder()
.vexpand(true)
.margin_top(15)
.margin_start(30)
.margin_end(30)
.build();
let info_page = InformationPage::new();
container.add_titled(&info_page.container, Some("info_page"), "Information");
let oc_page = OcPage::new(&system_info);
container.add_titled(&oc_page.container, Some("oc_page"), "OC");
let thermals_page = ThermalsPage::new(&system_info);
container.add_titled(&thermals_page.container, Some("thermals_page"), "Thermals");
let mut software_page = SoftwarePage::builder()
.launch((system_info, embedded_daemon))
.detach();
container.add_titled(software_page.widget(), Some("software_page"), "Software");
software_page.detach_runtime();
Self {
container,
info_page,
thermals_page,
oc_page,
}
}
}
fn values_row<W: IsA<Widget>>(
title: &str,
parent: &Grid,
value_child: &W,
row: i32,
column_offset: i32,
) {
let title_label = Label::builder().label(title).halign(Align::Start).build();
parent.attach(&title_label, column_offset, row, 1, 1);
parent.attach(value_child, column_offset + 1, row, 1, 1);
}
fn label_row(title: &str, parent: &Grid, row: i32, column_offset: i32, selectable: bool) -> Label {
let value_label = Label::builder()
.halign(Align::End)
.hexpand(true)
.selectable(selectable)
.build();
values_row(title, parent, &value_label, row, column_offset);
value_label
}
fn values_grid() -> Grid {
Grid::builder()
.margin_start(10)
.margin_end(5)
.row_spacing(10)
.column_spacing(10)
.build()
}

View File

@ -89,9 +89,8 @@ pub struct GpuPciInfo {
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DeviceInfo<'a> {
#[serde(borrow)]
pub pci_info: Option<Cow<'a, GpuPciInfo>>,
pub struct DeviceInfo {
pub pci_info: Option<GpuPciInfo>,
pub vulkan_info: Option<VulkanInfo>,
pub driver: String,
pub vbios_version: Option<String>,