amdguid 1.0.12

AMDGPU fan control service
Documentation
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;

use amdgpu::pidfile::ports::{Output, OutputType};
use amdgpu::pidfile::Pid;
use egui::{ColorImage, ImageData, TextureHandle, Ui};
use epi::Frame;
use image::{GenericImageView, ImageBuffer, ImageFormat};
use parking_lot::Mutex;

use crate::widgets::outputs_settings::OutputsSettings;
use crate::widgets::{CoolingPerformance, EditTempConfig, EditUsageConfig};

#[derive(Default)]
pub enum ChangeState {
    #[default]
    New,
    Reloading,
    Success,
    Failure(String),
}

pub struct FanService {
    pub pid: Pid,
    pub reload: ChangeState,
}

impl FanService {
    pub fn new(pid: Pid) -> FanService {
        Self {
            pid,
            reload: Default::default(),
        }
    }
}

pub struct FanServices(pub Vec<FanService>);

impl std::ops::Deref for FanServices {
    type Target = Vec<FanService>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl FanServices {
    pub fn list_changed(&self, other: &[Pid]) -> bool {
        if self.0.len() != other.len() {
            return true;
        }
        let c = self
            .0
            .iter()
            .fold(HashMap::with_capacity(other.len()), |mut h, service| {
                h.insert(service.pid.0, true);
                h
            });
        !other.iter().all(|s| c.contains_key(&s.0))
    }
}

impl From<Vec<Pid>> for FanServices {
    fn from(v: Vec<Pid>) -> Self {
        Self(v.into_iter().map(FanService::new).collect())
    }
}

#[derive(Clone, Copy, Debug)]
pub enum Page {
    TempConfig,
    UsageConfig,
    Monitoring,
    Outputs,
    Settings,
}

impl Default for Page {
    fn default() -> Self {
        Self::TempConfig
    }
}

pub type FanConfig = Arc<Mutex<amdgpu_config::fan::Config>>;

#[cfg(not(debug_assertions))]
static RELOAD_PID_LIST_DELAY: u8 = 18;
#[cfg(debug_assertions)]
static RELOAD_PID_LIST_DELAY: u8 = 80;

pub struct StatefulConfig {
    pub config: FanConfig,
    pub state: ChangeState,
    pub textures: HashMap<OutputType, TextureHandle>,
}

impl StatefulConfig {
    pub fn new(config: FanConfig) -> Self {
        let textures = HashMap::with_capacity(40);

        Self {
            config,
            state: ChangeState::New,
            textures,
        }
    }

    pub fn load_textures(&mut self, ui: &mut Ui) {
        if !self.textures.is_empty() {
            return;
        }

        // 80x80
        let image = {
            let bytes = include_bytes!("../assets/icons/ports2.png");
            image::load_from_memory_with_format(bytes, ImageFormat::Png).unwrap()
        };

        let ctx = ui.ctx();

        for ty in OutputType::all() {
            let (offset_x, offset_y) = ty.to_coords();
            let mut img = ImageBuffer::new(80, 80);
            for x in 0..80 {
                for y in 0..80 {
                    img.put_pixel(x, y, image.get_pixel(x + offset_x, y + offset_y));
                }
            }

            let size = [img.width() as _, img.height() as _];
            let pixels = img.as_flat_samples();
            let id = ctx.load_texture(
                ty.name(),
                ImageData::Color(ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())),
            );
            self.textures.insert(ty, id);
        }
    }
}

pub struct AmdGui {
    pub page: Page,
    pid_files: SocketState<FanServices>,
    outputs: SocketState<BTreeMap<String, Vec<Output>>>,
    cooling_performance: CoolingPerformance,
    temp_config: EditTempConfig,
    usage_config: EditUsageConfig,
    outputs_settings: OutputsSettings,
    config: StatefulConfig,
    reload_pid_list_delay: u8,
}

impl epi::App for AmdGui {
    fn update(&mut self, _ctx: &epi::egui::Context, _frame: &Frame) {}

    fn name(&self) -> &str {
        "AMD GUI"
    }
}

impl AmdGui {
    pub fn new_with_config(config: FanConfig) -> Self {
        Self {
            page: Default::default(),
            pid_files: SocketState::NotAvailable,
            outputs: SocketState::NotAvailable,
            cooling_performance: CoolingPerformance::new(100, config.clone()),
            temp_config: EditTempConfig::new(config.clone()),
            usage_config: EditUsageConfig::new(config.clone()),
            outputs_settings: OutputsSettings::default(),
            config: StatefulConfig::new(config),
            reload_pid_list_delay: 0,
        }
    }

    pub fn ui(&mut self, ui: &mut Ui) {
        self.config.load_textures(ui);

        match self.page {
            Page::TempConfig => {
                if let SocketState::Connected(pid_files) = &mut self.pid_files {
                    self.temp_config.draw(ui, pid_files, &mut self.config);
                } else {
                    ui.label("Not available");
                }
            }
            Page::UsageConfig => {
                if let SocketState::Connected(pid_files) = &mut self.pid_files {
                    self.usage_config.draw(ui, pid_files, &mut self.config);
                } else {
                    ui.label("Not available");
                }
            }
            Page::Monitoring => {
                if let SocketState::Connected(pid_files) = &mut self.pid_files {
                    self.cooling_performance.draw(ui, pid_files);
                } else {
                    ui.label("Not available");
                }
            }
            Page::Settings => {}
            Page::Outputs => {
                if let SocketState::Connected(outputs) = &self.outputs {
                    self.outputs_settings.draw(ui, &mut self.config, outputs);
                } else {
                    ui.label("Not available");
                }
            }
        }
    }

    pub fn tick(&mut self) {
        self.cooling_performance.tick();
        let can_decrease = self.reload_pid_list_delay > 0;

        if can_decrease {
            self.reload_pid_list_delay -= 1;
            return;
        }

        self.reload_pid_list_delay = RELOAD_PID_LIST_DELAY;

        {
            use amdgpu::pidfile::helper_cmd::{send_command, Command, Response};

            match send_command(Command::FanServices) {
                Ok(Response::Services(services))
                    if self
                        .pid_files
                        .connected()
                        .map(|c| c.list_changed(&services))
                        .unwrap_or(true) =>
                {
                    self.pid_files = SocketState::Connected(FanServices::from(services));
                }
                Ok(Response::Services(_services)) => {
                    // SKIP
                }
                Ok(res) => {
                    tracing::warn!("Unexpected response {:?} while loading fan services", res);
                }
                Err(e) => {
                    self.pid_files = SocketState::NotAvailable;
                    tracing::warn!("Failed to load amd fan services pid list. {:?}", e);
                }
            }
        }

        {
            use amdgpu::pidfile::ports::{send_command, Command, Response};

            match send_command(Command::Ports) {
                Ok(Response::NoOp) => {}
                Ok(Response::Ports(outputs)) => {
                    let mut names = outputs.iter().fold(
                        Vec::with_capacity(outputs.len()),
                        |mut set, output| {
                            set.push(output.card.clone());
                            set
                        },
                    );
                    names.sort();

                    let mut tree = BTreeMap::new();
                    names.into_iter().for_each(|name| {
                        tree.insert(name, Vec::with_capacity(6));
                    });

                    self.outputs = SocketState::Connected(outputs.into_iter().fold(
                        tree,
                        |mut agg, output| {
                            let v = agg
                                .entry(output.card.clone())
                                .or_insert_with(|| Vec::with_capacity(6));
                            v.push(output);
                            v.sort_by(|a, b| {
                                format!(
                                    "{}{}{}",
                                    a.port_type,
                                    a.port_name.as_deref().unwrap_or_default(),
                                    a.port_number,
                                )
                                .cmp(&format!(
                                    "{}{}{}",
                                    b.port_type,
                                    b.port_name.as_deref().unwrap_or_default(),
                                    b.port_number,
                                ))
                            });
                            agg
                        },
                    ));
                }
                Err(e) => {
                    if matches!(self.page, Page::Outputs) {
                        self.page = Page::TempConfig;
                    }
                    self.outputs = SocketState::NotAvailable;
                    tracing::warn!("Failed to load amd fan services pid list. {:?}", e);
                }
            }
        }
    }
}

pub enum SocketState<Content> {
    NotAvailable,
    Connected(Content),
}

impl<C> SocketState<C> {
    pub fn connected(&self) -> Option<&C> {
        match self {
            Self::Connected(c) => Some(c),
            _ => None,
        }
    }
}