use ::zbus::Connection; use ctrl_gfx::error::GfxError; use ctrl_gfx::*; use ctrl_rog_bios::CtrlRogBios; use log::{error, info, warn}; use logind_zbus::{ types::{SessionClass, SessionInfo, SessionState, SessionType}, ManagerProxy, SessionProxy, }; use rog_types::gfx_vendors::{GfxPower, GfxRequiredUserAction, GfxVendors}; use std::{io::Write, ops::Add, path::Path, time::Instant}; use std::{iter::FromIterator, thread::JoinHandle}; use std::{process::Command, thread::sleep, time::Duration}; use std::{str::FromStr, sync::mpsc}; use std::{sync::Arc, sync::Mutex}; use sysfs_class::{PciDevice, SysClass}; use system::{GraphicsDevice, PciBus}; use crate::*; const THREAD_TIMEOUT_MSG: &str = "GFX: thread time exceeded 3 minutes, exiting"; pub struct CtrlGraphics { bus: PciBus, _amd: Vec, _intel: Vec, nvidia: Vec, #[allow(dead_code)] other: Vec, config: Arc>, thread_kill: Arc>>>, } impl Reloadable for CtrlGraphics { fn reload(&mut self) -> Result<(), RogError> { self.auto_power()?; info!("GFX: Reloaded gfx mode: {:?}", self.get_gfx_mode()?); Ok(()) } } impl CtrlGraphics { pub fn new(config: Arc>) -> std::io::Result { let bus = PciBus::new()?; info!("GFX: Rescanning PCI bus"); bus.rescan()?; let devs = PciDevice::all()?; let functions = |parent: &PciDevice| -> Vec { let mut functions = Vec::new(); if let Some(parent_slot) = parent.id().split('.').next() { for func in devs.iter() { if let Some(func_slot) = func.id().split('.').next() { if func_slot == parent_slot { info!("GFX: {}: Function for {}", func.id(), parent.id()); functions.push(func.clone()); } } } } functions }; let mut amd = Vec::new(); let mut intel = Vec::new(); let mut nvidia = Vec::new(); let mut other = Vec::new(); for dev in devs.iter() { let c = dev.class()?; if 0x03 == (c >> 16) & 0xFF { match dev.vendor()? { 0x1002 => { info!("GFX: {}: AMD graphics", dev.id()); amd.push(GraphicsDevice::new(dev.id().to_owned(), functions(&dev))); } 0x10DE => { info!("GFX: {}: NVIDIA graphics", dev.id()); nvidia.push(GraphicsDevice::new(dev.id().to_owned(), functions(&dev))); } 0x8086 => { info!("GFX: {}: Intel graphics", dev.id()); intel.push(GraphicsDevice::new(dev.id().to_owned(), functions(&dev))); } vendor => { info!("GFX: {}: Other({:X}) graphics", dev.id(), vendor); other.push(GraphicsDevice::new(dev.id().to_owned(), functions(&dev))); } } } } Ok(CtrlGraphics { bus, _amd: amd, _intel: intel, nvidia, other, config, thread_kill: Arc::new(Mutex::new(None)), }) } pub fn bus(&self) -> PciBus { self.bus.clone() } pub fn devices(&self) -> Vec { self.nvidia.clone() } /// Save the selected `Vendor` mode to config fn save_gfx_mode(vendor: GfxVendors, config: Arc>) { if let Ok(mut config) = config.lock() { config.gfx_last_mode = config.gfx_mode; config.gfx_mode = vendor; config.write(); } } /// Associated method to get which vendor mode is set pub fn get_gfx_mode(&self) -> Result { if let Ok(config) = self.config.lock() { return Ok(config.gfx_mode); } // TODO: Error here Ok(GfxVendors::Hybrid) } pub(super) fn get_runtime_status() -> Result { let path = Path::new("/sys/bus/pci/devices/0000:01:00.0/power/runtime_status"); if path.exists() { let buf = std::fs::read_to_string(path).map_err(|err| { RogError::Read( "/sys/bus/pci/devices/0000:01:00.0/power/runtime_status".to_string(), err, ) })?; Ok(GfxPower::from_str(&buf)?) } else { Ok(GfxPower::Off) } } /// Some systems have a fallback service to load nouveau if nvidia fails fn toggle_fallback_service(vendor: GfxVendors) -> Result<(), RogError> { let action = if vendor == GfxVendors::Nvidia { info!("GFX: Enabling nvidia-fallback.service"); "enable" } else { info!("GFX: Disabling nvidia-fallback.service"); "disable" }; let status = Command::new("systemctl") .arg(action) .arg("nvidia-fallback.service") .status() .map_err(|err| RogError::Command("systemctl".into(), err))?; if !status.success() { // Error is ignored in case this service is removed warn!( "systemctl: {} (ignore warning if service does not exist!)", status ); } Ok(()) } /// Write the appropriate xorg config for the chosen mode fn write_xorg_conf(vendor: GfxVendors) -> Result<(), RogError> { let text = if vendor == GfxVendors::Nvidia { [PRIMARY_GPU_BEGIN, PRIMARY_GPU_NVIDIA, PRIMARY_GPU_END].concat() } else { [PRIMARY_GPU_BEGIN, PRIMARY_GPU_END].concat() }; if !Path::new(XORG_PATH).exists() { std::fs::create_dir(XORG_PATH).map_err(|err| RogError::Write(XORG_PATH.into(), err))?; } let file = XORG_PATH.to_string().add(XORG_FILE); info!("GFX: Writing {}", file); let mut file = std::fs::OpenOptions::new() .create(true) .truncate(true) .write(true) .open(&file) .map_err(|err| RogError::Write(file, err))?; file.write_all(&text) .and_then(|_| file.sync_all()) .map_err(|err| RogError::Write(MODPROBE_PATH.into(), err))?; Ok(()) } /// Creates the full modprobe.conf required for vfio pass-through fn get_vfio_conf(devices: &[GraphicsDevice]) -> Vec { let mut vifo = MODPROBE_VFIO.to_vec(); for (d_count, dev) in devices.iter().enumerate() { for (f_count, func) in dev.functions().iter().enumerate() { let vendor = func.vendor().unwrap(); let device = func.device().unwrap(); unsafe { vifo.append(format!("{:x}", vendor).as_mut_vec()); } vifo.append(&mut vec![b':']); unsafe { vifo.append(format!("{:x}", device).as_mut_vec()); } if f_count < dev.functions().len() - 1 { vifo.append(&mut vec![b',']); } } if d_count < dev.functions().len() - 1 { vifo.append(&mut vec![b',']); } } let mut conf = MODPROBE_INTEGRATED.to_vec(); conf.append(&mut vifo); conf } fn write_modprobe_conf(vendor: GfxVendors, devices: &[GraphicsDevice]) -> Result<(), RogError> { info!("GFX: Writing {}", MODPROBE_PATH); let content = match vendor { GfxVendors::Nvidia | GfxVendors::Hybrid => { let mut base = MODPROBE_BASE.to_vec(); base.append(&mut MODPROBE_DRM_MODESET.to_vec()); base } GfxVendors::Vfio => Self::get_vfio_conf(devices), GfxVendors::Integrated => MODPROBE_INTEGRATED.to_vec(), GfxVendors::Compute => MODPROBE_BASE.to_vec(), }; let mut file = std::fs::OpenOptions::new() .create(true) .truncate(true) .write(true) .open(MODPROBE_PATH) .map_err(|err| RogError::Path(MODPROBE_PATH.into(), err))?; file.write_all(&content) .and_then(|_| file.sync_all()) .map_err(|err| RogError::Write(MODPROBE_PATH.into(), err))?; Ok(()) } fn unbind_remove_nvidia(devices: &[GraphicsDevice]) -> Result<(), RogError> { // Unbind NVIDIA graphics devices and their functions let unbinds = devices.iter().map(|dev| dev.unbind()); // Remove NVIDIA graphics devices and their functions let removes = devices.iter().map(|dev| dev.remove()); Result::from_iter(unbinds.chain(removes)) .map_err(|err| RogError::Command("device unbind error".into(), err)) } fn unbind_only(devices: &[GraphicsDevice]) -> Result<(), RogError> { let unbinds = devices.iter().map(|dev| dev.unbind()); Result::from_iter(unbinds) .map_err(|err| RogError::Command("device unbind error".into(), err)) } fn do_driver_action(driver: &str, action: &str) -> Result<(), GfxError> { let mut cmd = Command::new(action); cmd.arg(driver); let mut count = 0; const MAX_TRIES: i32 = 6; loop { if count > MAX_TRIES { let msg = format!("{} {} failed for unknown reason", action, driver); error!("GFX: {}", msg); return Ok(()); //Err(RogError::Modprobe(msg)); } let output = cmd .output() .map_err(|err| GfxError::Command(format!("{:?}", cmd), err))?; if !output.status.success() { if output .stderr .ends_with("is not currently loaded\n".as_bytes()) { return Ok(()); } if output.stderr.ends_with("is builtin.\n".as_bytes()) { return Err(GfxError::VfioBuiltin); } if output.stderr.ends_with("Permission denied\n".as_bytes()) { warn!( "{} {} failed: {:?}", action, driver, String::from_utf8_lossy(&output.stderr) ); warn!("GFX: It may be safe to ignore the above error, run `lsmod |grep {}` to confirm modules loaded", driver); return Ok(()); } if String::from_utf8_lossy(&output.stderr) .contains(&format!("Module {} not found", driver)) { return Err(GfxError::MissingModule(driver.into())); } if count >= MAX_TRIES { let msg = format!( "{} {} failed: {:?}", action, driver, String::from_utf8_lossy(&output.stderr) ); return Err(GfxError::Modprobe(msg)); } } else if output.status.success() { return Ok(()); } count += 1; std::thread::sleep(std::time::Duration::from_millis(50)); } } fn do_display_manager_action(action: &str) -> Result<(), RogError> { let mut cmd = Command::new("systemctl"); cmd.arg(action); cmd.arg(DISPLAY_MANAGER); let status = cmd .status() .map_err(|err| RogError::Command(format!("{:?}", cmd), err))?; if !status.success() { let msg = format!( "systemctl {} {} failed: {:?}", action, DISPLAY_MANAGER, status ); return Err(GfxError::DisplayManagerAction(msg, status).into()); } Ok(()) } fn wait_display_manager_state(state: &str) -> Result<(), RogError> { let mut cmd = Command::new("systemctl"); cmd.arg("is-active"); cmd.arg(DISPLAY_MANAGER); let mut count = 0; while count <= 5 { let output = cmd .output() .map_err(|err| RogError::Command(format!("{:?}", cmd), err))?; if output.stdout.starts_with(state.as_bytes()) { return Ok(()); } std::thread::sleep(std::time::Duration::from_millis(500)); count += 1; } Err(GfxError::DisplayManagerTimeout(state.into()).into()) } /// Determine if we need to logout/thread. Integrated<->Vfio mode does not /// require logout. fn logout_required(&self, vendor: GfxVendors) -> GfxRequiredUserAction { if let Ok(config) = self.config.lock() { let current = config.gfx_mode; if matches!( current, GfxVendors::Integrated | GfxVendors::Vfio | GfxVendors::Compute ) && matches!( vendor, GfxVendors::Integrated | GfxVendors::Vfio | GfxVendors::Compute ) { return GfxRequiredUserAction::None; } } GfxRequiredUserAction::Logout } /// Write the config changes and add/remove drivers and devices depending /// on selected mode: /// /// Tasks: /// - write xorg config /// - write modprobe config /// - rescan for devices /// + add drivers /// + or remove drivers and devices /// /// The daemon needs direct access to this function when it detects that the pub fn do_vendor_tasks( vendor: GfxVendors, vfio_enable: bool, devices: &[GraphicsDevice], bus: &PciBus, ) -> Result<(), RogError> { // Rescan before doing remove or add drivers bus.rescan()?; // Self::write_xorg_conf(vendor)?; // Write different modprobe to enable boot control to work Self::write_modprobe_conf(vendor, devices)?; match vendor { GfxVendors::Nvidia | GfxVendors::Hybrid | GfxVendors::Compute => { if vfio_enable { for driver in VFIO_DRIVERS.iter() { Self::do_driver_action(driver, "rmmod")?; } } for driver in NVIDIA_DRIVERS.iter() { Self::do_driver_action(driver, "modprobe")?; } } GfxVendors::Vfio => { if vfio_enable { Self::do_driver_action("nouveau", "rmmod")?; for driver in NVIDIA_DRIVERS.iter() { Self::do_driver_action(driver, "rmmod")?; } Self::unbind_only(&devices)?; Self::do_driver_action("vfio-pci", "modprobe")?; } else { return Err(GfxError::VfioDisabled.into()); } } GfxVendors::Integrated => { Self::do_driver_action("nouveau", "rmmod")?; if vfio_enable { for driver in VFIO_DRIVERS.iter() { Self::do_driver_action(driver, "rmmod")?; } } for driver in NVIDIA_DRIVERS.iter() { Self::do_driver_action(driver, "rmmod")?; } Self::unbind_remove_nvidia(&devices)?; } } Ok(()) } /// Check if the user has any graphical uiser sessions that are active or online fn graphical_user_sessions_exist( connection: &Connection, sessions: &[SessionInfo], ) -> Result { for session in sessions { let session_proxy = SessionProxy::new(connection, session)?; if session_proxy.get_class()? == SessionClass::User { match session_proxy.get_type()? { SessionType::X11 | SessionType::Wayland | SessionType::MIR => { match session_proxy.get_state()? { SessionState::Online | SessionState::Active => return Ok(true), SessionState::Closing | SessionState::Invalid => {} } } _ => {} } } } Ok(false) } /// Spools until all user sessions are ended then switches to requested mode fn fire_starter( vendor: GfxVendors, devices: Vec, bus: PciBus, thread_stop: mpsc::Receiver, config: Arc>, ) -> Result { info!("GFX: display-manager thread started"); const SLEEP_PERIOD: Duration = Duration::from_millis(100); let start_time = Instant::now(); let connection = Connection::new_system()?; let manager = ManagerProxy::new(&connection)?; let mut sessions = manager.list_sessions()?; loop { let tmp = manager.list_sessions()?; if !tmp.iter().eq(&sessions) { info!("GFX thread: Sessions list changed"); sessions = tmp; } if !Self::graphical_user_sessions_exist(&connection, &sessions)? { break; } if let Ok(stop) = thread_stop.try_recv() { if stop { return Ok("Graphics mode change was cancelled".into()); } } // exit if 3 minutes pass if Instant::now().duration_since(start_time).as_secs() > 180 { warn!("{}", THREAD_TIMEOUT_MSG); return Ok(THREAD_TIMEOUT_MSG.into()); } // Don't spin at max speed sleep(SLEEP_PERIOD); } info!("GFX thread: all graphical user sessions ended, continuing"); Self::do_display_manager_action("stop")?; Self::wait_display_manager_state("inactive")?; let vfio_enable = if let Ok(config) = config.lock() { config.gfx_vfio_enable } else { false }; Self::do_vendor_tasks(vendor, vfio_enable, &devices, &bus)?; Self::do_display_manager_action("restart")?; // Save selected mode in case of reboot Self::save_gfx_mode(vendor, config); info!("GFX thread: display-manager started"); let v: &str = vendor.into(); info!("GFX thread: Graphics mode changed to {} successfully", v); Ok(format!("Graphics mode changed to {} successfully", v)) } /// Before starting a new thread the old one *must* be cancelled fn cancel_thread(&self) { if let Ok(lock) = self.thread_kill.lock() { if let Some(tx) = lock.as_ref() { // Cancel the running thread info!("GFX: Cancelling previous thread"); tx.send(true) .map_err(|err| { warn!("GFX thread: {}", err); }) .ok(); } } } /// The thread is used only in cases where a logout is required fn setup_thread(&mut self, vendor: GfxVendors) { let config = self.config.clone(); let devices = self.nvidia.clone(); let bus = self.bus.clone(); let (tx, rx) = mpsc::channel(); if let Ok(mut lock) = self.thread_kill.lock() { *lock = Some(tx); } let killer = self.thread_kill.clone(); let _join: JoinHandle<()> = std::thread::spawn(move || { Self::fire_starter(vendor, devices, bus, rx, config) .map_err(|err| { error!("GFX: {}", err); }) .ok(); // clear the tx/rx when done if let Ok(mut lock) = killer.try_lock() { *lock = None; } }); } /// Initiates a mode change by starting a thread that will wait until all /// graphical sessions are exited before performing the tasks required /// to switch modes. /// /// For manually calling (not on boot/startup) via dbus pub fn set_gfx_config( &mut self, vendor: GfxVendors, ) -> Result { if let Ok(gsync) = CtrlRogBios::get_gfx_mode() { if gsync == 1 { return Err(GfxError::GsyncModeActive.into()); } } let vfio_enable = if let Ok(config) = self.config.lock() { config.gfx_vfio_enable } else { false }; if !vfio_enable && matches!(vendor, GfxVendors::Vfio) { return Err(GfxError::VfioDisabled.into()); } // Must always cancel any thread running self.cancel_thread(); // determine which method we need here let action_required = self.logout_required(vendor); if matches!(action_required, GfxRequiredUserAction::Logout) { // Yeah need the thread to check if all users are logged out info!("GFX: mode change requires a logout to complete"); self.setup_thread(vendor); } else { // Okay cool, we can switch on/off vfio info!("GFX: mode change does not require logout"); let devices = self.nvidia.clone(); let bus = self.bus.clone(); Self::do_vendor_tasks(vendor, vfio_enable, &devices, &bus)?; info!("GFX: Graphics mode changed to {}", <&str>::from(vendor)); if matches!(vendor, GfxVendors::Compute | GfxVendors::Vfio) { loop { if let Ok(config) = self.config.try_lock() { if config.gfx_save_compute_vfio { Self::save_gfx_mode(vendor, self.config.clone()); } return Ok(action_required); } } } } // TODO: undo if failed? Save last mode, catch errors... Ok(action_required) } /// Used only on boot to set correct mode fn auto_power(&mut self) -> Result<(), RogError> { let vendor = self.get_gfx_mode()?; let devices = self.nvidia.clone(); let bus = self.bus.clone(); let vfio_enable = if let Ok(config) = self.config.try_lock() { config.gfx_vfio_enable } else { false }; Self::do_vendor_tasks(vendor, vfio_enable, &devices, &bus)?; Self::toggle_fallback_service(vendor)?; Ok(()) } }