amdgpu_top_tui 0.11.1

TUI library for amdgpu_top
Documentation
use cursive::align::HAlign;
use cursive::views::{LinearLayout, TextView, Panel, ResizedView};
use cursive::view::SizeConstraint;

use libamdgpu_top::AMDGPU::{GPU_INFO, MetricsInfo};
use libamdgpu_top::{AppDeviceInfo, DevicePath, Sampling};

use crate::{ToggleOptions, view::*};

use libamdgpu_top::app::{AppAmdgpuTop, AppAmdgpuTopStat, AppOption};

const WIDE_TERM_COLS: u16 = 150;

pub const TOGGLE_HELP: &str = concat!(
    " (g)rbm g(r)bm2 (v)ram_usage (a)ctivity (f)dinfo se(n)sor (m)etrics \n",
    " (P): sort_by_pid (V): sort_by_vram (G): sort_by_gfx (M): sort_by_media \n",
    " (R): reverse sort (T): switch theme (light/dark) \n",
    " (h)igh_freq (q)uit \n",
);

#[derive(Clone)]
pub(crate) struct AppLayout {
    pub no_pc: bool,
    pub index: usize,
    pub grbm_view: PerfCounterView,
    pub grbm2_view: PerfCounterView,
    pub vram_usage_view: VramUsageView,
    pub activity_view: ActivityView,
    pub fdinfo_view: AppTextView,
    pub xdna_fdinfo_view: AppTextView,
    pub sensors_view: AppTextView,
    pub gpu_metrics_view: AppTextView,
    pub ecc_view: AppTextView,
}

impl AppLayout {
    pub fn new(no_pc: bool, index: usize) -> Self {
        Self {
            no_pc,
            index,
            grbm_view: PerfCounterView::reserve(index),
            grbm2_view: PerfCounterView::reserve(index),
            vram_usage_view: VramUsageView::new(index),
            activity_view: ActivityView::new(index),
            fdinfo_view: Default::default(),
            xdna_fdinfo_view: Default::default(),
            sensors_view: Default::default(),
            gpu_metrics_view: Default::default(),
            ecc_view: Default::default(),
        }
    }

    pub fn new_with_app(
        app_amdgpu_top: &AppAmdgpuTop,
        no_pc: bool,
        index: usize,
    ) -> Self {
        let grbm_view = PerfCounterView::new(&app_amdgpu_top.stat.grbm, index);
        let grbm2_view = PerfCounterView::new(&app_amdgpu_top.stat.grbm2, index);

        Self {
            no_pc,
            index,
            grbm_view,
            grbm2_view,
            vram_usage_view: VramUsageView::new(index),
            activity_view: ActivityView::new(index),
            fdinfo_view: Default::default(),
            xdna_fdinfo_view: Default::default(),
            sensors_view: Default::default(),
            gpu_metrics_view: Default::default(),
            ecc_view: Default::default(),
        }
    }

    pub fn view(
        &self,
        title: &str,
        info_bar: String,
        stat: &AppAmdgpuTopStat,
        xdna_device_path: &Option<DevicePath>,
    ) -> ResizedView<LinearLayout> {
        let is_wide_term = termsize::get().map(|s| s.cols >= WIDE_TERM_COLS).unwrap_or_default();
        let mut layout = LinearLayout::vertical()
            .child(
                Panel::new(
                    TextView::new(info_bar).center()
                )
                .title(title)
                .title_position(HAlign::Center)
            );

        if !self.no_pc {
            let grbm_view = self.grbm_view.resized_panel(&stat.grbm);
            let grbm2_view = self.grbm2_view.resized_panel(&stat.grbm2);

            if is_wide_term {
                layout.add_child(
                    LinearLayout::horizontal()
                        .child(grbm_view)
                        .child(grbm2_view)
                );
            } else {
                layout.add_child(grbm_view);
                layout.add_child(grbm2_view);
            }
        }

        {
            let vram_usage_view = self.vram_usage_view.resized_panel(&stat.vram_usage);
            let activity_view = self.activity_view.resized_panel(&stat.activity);

            if is_wide_term {
                layout.add_child(
                    LinearLayout::horizontal()
                        .child(vram_usage_view)
                        .child(activity_view)
                );
            } else {
                layout.add_child(vram_usage_view);
                layout.add_child(activity_view);
            }
        }

        layout.add_child(self.fdinfo_view.text.resized_panel(AppTextView::FDINFO_TITLE, self.index));

        {
            let sensors_view = stat.sensors.as_ref().map(|_| self.sensors_view.text.resized_panel("Sensors", self.index));
            let metrics_view = stat.metrics.as_ref().map(|m| {
                let title = match m.get_header() {
                    Some(v) => format!("GPU Metrics v{}.{}", v.format_revision, v.content_revision),
                    None => "GPU Metrics".to_string(),
                };

                self.gpu_metrics_view.text.resized_panel_with_name(
                    &title,
                    AppTextView::gpu_metrics_name(self.index),
                )
            });

            if is_wide_term {
                let mut h_layout = LinearLayout::horizontal();

                if let Some(sensors_view) = sensors_view {
                    h_layout.add_child(sensors_view);
                }

                if let Some(metrics_view) = metrics_view {
                    h_layout.add_child(metrics_view);
                }

                layout.add_child(h_layout);
            } else {
                if let Some(sensors_view) = sensors_view {
                    layout.add_child(sensors_view);
                }

                if let Some(metrics_view) = metrics_view {
                    layout.add_child(metrics_view);
                }
            }
        }

        if stat.memory_error_count.is_some() {
            layout.add_child(self.ecc_view.text.resized_panel("ECC Error Count", self.index));
        }

        if let Some(xdna_device_path) = xdna_device_path {
            let title = format!("XDNA fdinfo - {}", xdna_device_path.device_name);
            layout.add_child(self.xdna_fdinfo_view.text.resized_panel(&title, self.index));
        }

        layout.add_child(TextView::new(TOGGLE_HELP));

        ResizedView::new(SizeConstraint::Free, SizeConstraint::Full, layout)
    }
}

#[derive(Clone)]
pub(crate) struct SuspendedTuiApp {
    pub device_path: DevicePath,
    pub no_pc: bool,
    pub index: usize,
    pub layout: AppLayout,
}

impl SuspendedTuiApp {
    pub fn new(device_path: DevicePath, no_pc: bool, index: usize) -> Self {
        Self {
            device_path,
            no_pc,
            index,
            layout: AppLayout::new(no_pc, index),
        }
    }

    pub fn to_tui_app(&self) -> Option<TuiApp> {
        let amdgpu_dev = self.device_path.init().ok()?;
        let app_amdgpu_top = AppAmdgpuTop::new(
            amdgpu_dev,
            self.device_path.clone(),
            &AppOption { pcie_bw: true },
        )?;

        Some(TuiApp {
            app_amdgpu_top,
            no_pc: self.no_pc,
            index: self.index,
            layout: self.layout.clone(),
        })
    }

    pub fn label(&self) -> String {
        format!("#{:<2} {} (Suspended)", self.index, self.device_path.menu_entry())
    }
}

pub(crate) struct TuiApp {
    pub app_amdgpu_top: AppAmdgpuTop,
    pub no_pc: bool,
    pub index: usize,
    pub layout: AppLayout,
}

impl TuiApp {
    pub fn new_with_app(
        app_amdgpu_top: AppAmdgpuTop,
        no_pc: bool,
        index: usize,
    ) -> Self {
        let layout = AppLayout::new_with_app(&app_amdgpu_top, no_pc, index);

        Self {
            app_amdgpu_top,
            no_pc,
            index,
            layout,
        }
    }

    pub fn view(&self, title: &str) -> ResizedView<LinearLayout> {
        self.layout.view(
            title,
            self.app_amdgpu_top.device_info.info_bar(),
            &self.app_amdgpu_top.stat,
            &self.app_amdgpu_top.xdna_device_path,
        )
    }

    pub fn update(&mut self, flags: &ToggleOptions, sample: &Sampling) {
        self.app_amdgpu_top.update(sample.to_duration());

        let _ = self.layout.fdinfo_view.print_fdinfo(
            &mut self.app_amdgpu_top.stat.fdinfo,
            flags.fdinfo_sort,
            flags.reverse_sort,
        );

        if self.app_amdgpu_top.xdna_device_path.is_some() {
            let _ = self.layout.xdna_fdinfo_view.print_xdna_fdinfo(&mut self.app_amdgpu_top.stat.xdna_fdinfo);
        }

        self.layout.vram_usage_view.set_value(&self.app_amdgpu_top.stat.vram_usage);
        self.layout.activity_view.set_value(&self.app_amdgpu_top.stat.activity);

        if let Some(sensors) = &self.app_amdgpu_top.stat.sensors {
            let _ = self.layout.sensors_view.print_sensors(sensors);
        }

        {
            if let Some(arc_pcie_bw) = &self.app_amdgpu_top.stat.arc_pcie_bw {
                let lock = arc_pcie_bw.try_lock();
                if let Ok(pcie_bw) = &lock {
                    let _ = self.layout.sensors_view.print_pcie_bw(pcie_bw);
                }
            }
        }

        if let Some(ecc) = &self.app_amdgpu_top.stat.memory_error_count {
            let _ = self.layout.ecc_view.print_memory_error_count(ecc);
        }

        if let Some(metrics) = &self.app_amdgpu_top.stat.metrics {
            let _ = self.layout.gpu_metrics_view.print_gpu_metrics(metrics);
        } else {
            self.layout.gpu_metrics_view.text.clear();
        }

        if !self.no_pc {
            self.layout.grbm_view.set_value(&self.app_amdgpu_top.stat.grbm);
            self.layout.grbm2_view.set_value(&self.app_amdgpu_top.stat.grbm2);
        }

        self.layout.sensors_view.text.set();
        self.layout.fdinfo_view.text.set();
        self.layout.xdna_fdinfo_view.text.set();
        self.layout.ecc_view.text.set();
        self.layout.gpu_metrics_view.text.set();
    }

    pub fn label(&self) -> String {
        format!("#{:<2} {}", self.index, self.app_amdgpu_top.device_path.menu_entry())
    }
}

pub trait ListNameInfoBar {
    fn info_bar(&self) -> String;
}

impl ListNameInfoBar for AppDeviceInfo {
    fn info_bar(&self) -> String {
        format!(
            concat!(
                "{mark_name} ({pci}, {did:#06X}:{rid:#04X})\n",
                "{asic}\n{gpu_type}, {chip_class},{gfx_ver} {num_cu} CU, {min_gpu_clk}-{max_gpu_clk} MHz\n",
                "{vram_type} {vram_bus_width}-bit, {vram_size} MiB, ",
                "{min_memory_clk}-{max_memory_clk} MHz",
            ),
            mark_name = self.marketing_name,
            pci = self.pci_bus,
            did = self.ext_info.device_id(),
            rid = self.ext_info.pci_rev_id(),
            asic = self.ext_info.get_asic_name(),
            gpu_type = if self.ext_info.is_apu() { "APU" } else { "dGPU" },
            chip_class = self.ext_info.get_chip_class(),
            gfx_ver = match &self.gfx_target_version {
                Some(ver) => format!(" {ver},"),
                None => String::new(),
            },
            num_cu = self.ext_info.cu_active_number(),
            min_gpu_clk = self.min_gpu_clk,
            max_gpu_clk = self.max_gpu_clk,
            vram_type = self.ext_info.get_vram_type(),
            vram_bus_width = self.ext_info.vram_bit_width,
            vram_size = self.memory_info.vram.total_heap_size >> 20,
            min_memory_clk = self.min_mem_clk,
            max_memory_clk = self.max_mem_clk,
        )
    }
}