use crate::cli::GpuCommands; use crate::hardware::gpu::{ get_amd_igpu_name, get_current_mode, get_igpu_vendor, get_nvidia_gpu_pci_addr, get_nvidia_gpu_pci_bus, kill_gpu_processes, remove_gpu, rescan_pci, unbind_gpu, }; use crate::utils::system::{ assert_root, create_file, get_active_graphical_sessions, get_display_manager, is_service_active, manage_service, rebuild_initramfs, terminate_session, }; use log::{error, info}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::Path; use std::process::Command; use zbus::Connection; use zbus::proxy; #[proxy( interface = "org.lapctl1", default_service = "org.lapctl", default_path = "/org/lapctl" )] trait Lapctl { async fn switch_gpu_integrated(&self, no_reboot: bool) -> zbus::Result<()>; async fn switch_gpu_hybrid( &self, rtd3: i32, use_nvidia_current: bool, no_reboot: bool, ) -> zbus::Result<()>; async fn switch_gpu_nvidia( &self, dm: String, force_comp: bool, coolbits: i32, use_nvidia_current: bool, wayland: bool, no_reboot: bool, ) -> zbus::Result<()>; } fn try_call_daemon(cmd: &GpuCommands) -> bool { let rt = match tokio::runtime::Runtime::new() { Ok(rt) => rt, Err(_) => return false, }; rt.block_on(async { let connection = match tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system()) .await { Ok(Ok(conn)) => conn, _ => return false, }; let proxy = match LapctlProxy::new(&connection).await { Ok(p) => p, Err(_) => return false, }; let res = match cmd { GpuCommands::Integrated { no_reboot } => { tokio::time::timeout( std::time::Duration::from_secs(5), proxy.switch_gpu_integrated(*no_reboot), ) .await } GpuCommands::Hybrid { rtd3, use_nvidia_current, no_reboot, } => { let rtd3_val = rtd3.map(|v| v as i32).unwrap_or(-1); tokio::time::timeout( std::time::Duration::from_secs(5), proxy.switch_gpu_hybrid(rtd3_val, *use_nvidia_current, *no_reboot), ) .await } GpuCommands::Nvidia { dm, force_comp, coolbits, use_nvidia_current, wayland, no_reboot, } => { let dm_val = dm.clone().unwrap_or_default(); let coolbits_val = coolbits.map(|v| v as i32).unwrap_or(-1); tokio::time::timeout( std::time::Duration::from_secs(5), proxy.switch_gpu_nvidia( dm_val, *force_comp, coolbits_val, *use_nvidia_current, *wayland, *no_reboot, ), ) .await } _ => return false, }; matches!(res, Ok(Ok(_))) }) } const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json"; const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf"; const BLACKLIST_CONTENT: &str = r#"# Automatically generated by lapctl blacklist nouveau blacklist nova_core blacklist nova_drm blacklist nvidia blacklist nvidia_drm blacklist nvidia_uvm blacklist nvidia_modeset blacklist nvidia_current blacklist nvidia_current_drm blacklist nvidia_current_uvm blacklist nvidia_current_modeset blacklist i2c_nvidia_gpu alias nouveau off alias nova_core off alias nova_drm off alias nvidia off alias nvidia_drm off alias nvidia_uvm off alias nvidia_modeset off alias nvidia_current off alias nvidia_current_drm off alias nvidia_current_uvm off alias nvidia_current_modeset off alias i2c_nvidia_gpu off "#; const UDEV_INTEGRATED_PATH: &str = "/etc/udev/rules.d/50-remove-nvidia.rules"; const UDEV_INTEGRATED: &str = r#"# Automatically generated by lapctl # Remove NVIDIA USB xHCI Host Controller devices, if present ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{power/control}="auto", ATTR{remove}="1" # Remove NVIDIA USB Type-C UCSI devices, if present ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{power/control}="auto", ATTR{remove}="1" # Remove NVIDIA Audio devices, if present ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x040300", ATTR{power/control}="auto", ATTR{remove}="1" # Remove NVIDIA VGA/3D controller devices ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x03[0-9]*", ATTR{power/control}="auto", ATTR{remove}="1" "#; const UDEV_PM_PATH: &str = "/etc/udev/rules.d/80-nvidia-pm.rules"; const UDEV_PM_CONTENT: &str = r#"# Automatically generated by lapctl # Remove NVIDIA USB xHCI Host Controller devices, if present ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{remove}="1" # Remove NVIDIA USB Type-C UCSI devices, if present ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{remove}="1" # Remove NVIDIA Audio devices, if present ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x040300", ATTR{remove}="1" # Enable runtime PM for NVIDIA VGA/3D controller devices on driver bind ACTION=="bind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="auto" ACTION=="bind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="auto" # Disable runtime PM for NVIDIA VGA/3D controller devices on driver unbind ACTION=="unbind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="on" ACTION=="unbind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="on" "#; const XORG_PATH: &str = "/etc/X11/xorg.conf"; const EXTRA_XORG_PATH: &str = "/etc/X11/xorg.conf.d/10-nvidia.conf"; const MODESET_PATH: &str = "/etc/modprobe.d/nvidia.conf"; const SDDM_XSETUP_PATH: &str = "/usr/share/sddm/scripts/Xsetup"; const LIGHTDM_SCRIPT_PATH: &str = "/etc/lightdm/nvidia.sh"; const LIGHTDM_CONFIG_PATH: &str = "/etc/lightdm/lightdm.conf.d/20-nvidia.conf"; const SDDM_XSETUP_CONTENT: &str = r#"#!/bin/sh # Xsetup - run as root before the login dialog appears "#; const EXTRA_XORG_CONTENT: &str = r#"# Automatically generated by lapctl Section "OutputClass" Identifier "nvidia" MatchDriver "nvidia-drm" Driver "nvidia" "#; const LIGHTDM_CONFIG_CONTENT: &str = r#"# Automatically generated by lapctl [Seat:*] display-setup-script=/etc/lightdm/nvidia.sh "#; const NVIDIA_XRANDR_SCRIPT: &str = r#"#!/bin/sh # Automatically generated by lapctl current="" xrandr --setprovideroutputsource "{}" NVIDIA-0 xrandr --auto for next in $(xrandr --listmonitors | grep -E " *[0-9]+:.*" | cut -d" " -f6); do [ -z "$current" ] && current=$next && continue xrandr --output "$current" --auto --output "$next" --auto --right-of "$current" current=$next done "#; // Helpers pub fn xorg_intel(bus_id: &str) -> String { format!( r#"# Automatically generated by lapctl Section "ServerLayout" Identifier "layout" Screen 0 "nvidia" Inactive "intel" EndSection Section "Device" Identifier "nvidia" Driver "nvidia" BusID "{}" EndSection Section "Screen" Identifier "nvidia" Device "nvidia" Option "AllowEmptyInitialConfiguration" EndSection Section "Device" Identifier "intel" Driver "modesetting" EndSection Section "Screen" Identifier "intel" Device "intel" EndSection "#, bus_id ) } pub fn xorg_amd(bus_id: &str) -> String { format!( r#"# Automatically generated by lapctl Section "ServerLayout" Identifier "layout" Screen 0 "nvidia" Inactive "amdgpu" EndSection Section "Device" Identifier "nvidia" Driver "nvidia" BusID "{}" EndSection Section "Screen" Identifier "nvidia" Device "nvidia" Option "AllowEmptyInitialConfiguration" EndSection Section "Device" Identifier "amdgpu" Driver "amdgpu" EndSection Section "Screen" Identifier "amd" Device "amdgpu" EndSection "#, bus_id ) } pub fn generate_xrandr_script(igpu_vendor: Option<&String>) -> String { let source_name = match igpu_vendor.map(|s| s.as_str()) { Some("intel") => "modesetting".to_string(), Some("amd") => { if let Some(amd_name) = get_amd_igpu_name() { amd_name } else { "modesetting".to_string() } } _ => "modesetting".to_string(), }; NVIDIA_XRANDR_SCRIPT.replace("{}", &source_name) } fn cleanup() { let to_remove = vec![ BLACKLIST_PATH, UDEV_INTEGRATED_PATH, UDEV_PM_PATH, XORG_PATH, EXTRA_XORG_PATH, MODESET_PATH, LIGHTDM_SCRIPT_PATH, LIGHTDM_CONFIG_PATH, "/etc/lapctl-wayland-nvidia", // legacy paths "/etc/X11/xorg.conf.d/90-nvidia.conf", "/lib/udev/rules.d/50-remove-nvidia.rules", "/lib/udev/rules.d/80-nvidia-pm.rules", ]; for file_path in to_remove { if Path::new(file_path).exists() { if let Err(e) = fs::remove_file(file_path) { error!("Failed to remove file '{}': {}", file_path, e); } else { info!("Removed file {}", file_path); } } } let backup_path = format!("{}.bak", SDDM_XSETUP_PATH); if Path::new(&backup_path).exists() { info!("Restoring Xsetup backup"); if let Ok(content) = fs::read_to_string(&backup_path) { create_file(SDDM_XSETUP_PATH, &content, true); } if let Err(e) = fs::remove_file(&backup_path) { error!("Failed to remove backup: {}", e); } else { info!("Removed file {}", backup_path); } } } // CACHING LOGIC #[derive(Serialize, Deserialize, Debug)] struct Cache { nvidia_gpu_pci_bus: String, } fn create_cache_obj(bus_id: String) -> Cache { Cache { nvidia_gpu_pci_bus: bus_id, } } fn create_cache_file() { let pci_addr = get_nvidia_gpu_pci_addr(); if pci_addr.is_none() { log::debug!("NVIDIA GPU not found, skipping cache creation"); return; } let bus_id = get_nvidia_gpu_pci_bus(); let cache = create_cache_obj(bus_id); if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() { let _ = fs::create_dir_all(parent); } if let Ok(json) = serde_json::to_string_pretty(&cache) && fs::write(CACHE_FILE_PATH, json).is_ok() { log::debug!("Created/Updated cache file {}", CACHE_FILE_PATH); } } fn read_cache_file() -> Result { if Path::new(CACHE_FILE_PATH).exists() { let content = fs::read_to_string(CACHE_FILE_PATH).map_err(|e| e.to_string())?; serde_json::from_str(&content).map_err(|e| e.to_string()) } else if get_current_mode() == "hybrid" { Ok(create_cache_obj(get_nvidia_gpu_pci_bus())) } else { Err( "No cache present. Operation requires that the system be in the hybrid Optimus mode" .into(), ) } } fn prepare_no_reboot(dm_flag: Option) -> Result, String> { let dm_name = dm_flag.or_else(get_display_manager); if let Some(ref name) = dm_name && is_service_active(name) { info!("Stopping display manager: {}", name); manage_service(name, "stop")?; return Ok(Some(name.clone())); } // If no active DM found, check for graphical sessions let sessions = get_active_graphical_sessions(); if !sessions.is_empty() { println!("No active display manager found, but graphical sessions are running."); println!("Terminating graphical sessions for no-reboot GPU switching..."); for session_id in sessions { if let Err(e) = terminate_session(&session_id) { error!("Failed to terminate session {}: {}", session_id, e); } } // Give some time for sessions to terminate std::thread::sleep(std::time::Duration::from_secs(2)); } Ok(None) } fn finalize_no_reboot(stopped_dm: Option) { if let Some(name) = stopped_dm { info!("Starting display manager: {}", name); let _ = manage_service(&name, "start"); } } fn switch_integrated(no_reboot: bool) { assert_root(); println!("Switching to integrated mode"); if !Path::new(CACHE_FILE_PATH).exists() { create_cache_file(); } cleanup(); create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false); create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false); let mut stopped_dm = None; if no_reboot { match prepare_no_reboot(None) { Ok(dm) => stopped_dm = dm, Err(e) => { error!("{}", e); return; } } if let Err(e) = kill_gpu_processes() { error!("Failed to kill GPU processes: {}", e); } // Try to unload modules let modules = ["nvidia_uvm", "nvidia_modeset", "nvidia_drm", "nvidia"]; for module in modules { let _ = Command::new("modprobe").args(["-r", module]).status(); } if let Some(pci_addr) = get_nvidia_gpu_pci_addr() { if let Err(e) = unbind_gpu(&pci_addr) { error!("Failed to unbind GPU: {}", e); } if let Err(e) = remove_gpu(&pci_addr) { error!("Failed to remove GPU: {}", e); } } } let is_debug = log::log_enabled!(log::Level::Debug); let mut dis_cmd = Command::new("systemctl"); dis_cmd.args(["disable", "nvidia-persistenced.service"]); if !is_debug { dis_cmd.stdout(std::process::Stdio::null()); dis_cmd.stderr(std::process::Stdio::null()); } match dis_cmd.status() { Ok(s) if s.success() => println!("Successfully disabled nvidia-persistenced.service"), _ => error!("An error ocurred while disabling service"), } if no_reboot { println!("Operation completed successfully"); finalize_no_reboot(stopped_dm); } else { rebuild_initramfs(); println!("Operation completed successfully"); println!("Please reboot your computer for changes to take effect!"); } } fn switch_hybrid(rtd3: Option, use_nvidia_current: bool, no_reboot: bool) { assert_root(); println!("Switching to hybrid mode"); println!( "Enable PCI-Express Runtime D3 (RTD3) Power Management: {}", rtd3.is_some() ); if no_reboot && let Err(e) = rescan_pci() { error!("Failed to rescan PCI bus: {}", e); } if !Path::new(CACHE_FILE_PATH).exists() { create_cache_file(); } cleanup(); let mut stopped_dm = None; if no_reboot { match prepare_no_reboot(None) { Ok(dm) => stopped_dm = dm, Err(e) => { error!("{}", e); return; } } if let Err(e) = kill_gpu_processes() { error!("Failed to kill GPU processes: {}", e); } } // persistenced enable let is_debug = log::log_enabled!(log::Level::Debug); let mut enable_cmd = Command::new("systemctl"); enable_cmd.args(["enable", "nvidia-persistenced.service"]); if !is_debug { enable_cmd.stdout(std::process::Stdio::null()); enable_cmd.stderr(std::process::Stdio::null()); } match enable_cmd.status() { Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"), _ => error!("An error ocurred while enabling service"), } if let Some(val) = rtd3 { let modeset_content = if use_nvidia_current { format!( "# Automatically generated by lapctl\n\noptions nvidia-current-drm modeset=1\noptions nvidia-current \"NVreg_DynamicPowerManagement=0x0{}\"\noptions nvidia-current NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n", val ) } else { format!( "# Automatically generated by lapctl\n\noptions nvidia-drm modeset=1\noptions nvidia \"NVreg_DynamicPowerManagement=0x0{}\"\noptions nvidia NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n", val ) }; create_file(MODESET_PATH, &modeset_content, false); create_file(UDEV_PM_PATH, UDEV_PM_CONTENT, false); } else { let modeset_content = if use_nvidia_current { "# Automatically generated by lapctl\n\noptions nvidia-current-drm modeset=1\noptions nvidia-current NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n".to_string() } else { "# Automatically generated by lapctl\n\noptions nvidia-drm modeset=1\noptions nvidia NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n".to_string() }; create_file(MODESET_PATH, &modeset_content, false); } if no_reboot { println!("Operation completed successfully"); finalize_no_reboot(stopped_dm); } else { rebuild_initramfs(); println!("Operation completed successfully"); println!("Please reboot your computer for changes to take effect!"); } } fn switch_nvidia( dm: Option, force_comp: bool, coolbits: Option, use_nvidia_current: bool, wayland: bool, no_reboot: bool, ) { assert_root(); println!("Switching to nvidia mode"); println!("Enable ForceCompositionPipeline: {}", force_comp); println!("Enable Coolbits: {}", coolbits.is_some()); if no_reboot && let Err(e) = rescan_pci() { error!("Failed to rescan PCI bus: {}", e); } if !Path::new(CACHE_FILE_PATH).exists() { create_cache_file(); } let cache = read_cache_file(); let pci_bus = match cache { Ok(c) => c.nvidia_gpu_pci_bus, Err(_) => get_nvidia_gpu_pci_bus(), }; cleanup(); if use_nvidia_current { create_file( MODESET_PATH, "# Automatically generated by lapctl\n\noptions nvidia-current-drm modeset=1\noptions nvidia-current NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n", false, ); } else { create_file( MODESET_PATH, "# Automatically generated by lapctl\n\noptions nvidia-drm modeset=1\noptions nvidia NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n", false, ); } if wayland { create_file( "/etc/lapctl-wayland-nvidia", "# Wayland mode marker for lapctl\n", false, ); } else { let igpu_vendor = get_igpu_vendor(); if let Some(ref vendor) = igpu_vendor { if vendor == "intel" { create_file(XORG_PATH, &xorg_intel(&pci_bus), false); } else if vendor == "amd" { create_file(XORG_PATH, &xorg_amd(&pci_bus), false); } } let mut extra_xorg = EXTRA_XORG_CONTENT.to_string(); if force_comp { extra_xorg.push_str(" Option \"ForceCompositionPipeline\" \"true\"\n"); } if let Some(cb) = coolbits { extra_xorg.push_str(&format!(" Option \"Coolbits\" \"{}\"\n", cb)); } if force_comp || coolbits.is_some() { extra_xorg.push_str("EndSection\n"); create_file(EXTRA_XORG_PATH, &extra_xorg, false); } let display_manager = dm.clone().or_else(get_display_manager); if let Some(cdm) = display_manager { if cdm == "sddm" { if Path::new(SDDM_XSETUP_PATH).exists() { info!("Creating Xsetup backup"); if let Ok(content) = fs::read_to_string(SDDM_XSETUP_PATH) { create_file(&format!("{}.bak", SDDM_XSETUP_PATH), &content, false); } } create_file( SDDM_XSETUP_PATH, &generate_xrandr_script(igpu_vendor.as_ref()), true, ); } else if cdm == "lightdm" { create_file( LIGHTDM_SCRIPT_PATH, &generate_xrandr_script(igpu_vendor.as_ref()), true, ); create_file(LIGHTDM_CONFIG_PATH, LIGHTDM_CONFIG_CONTENT, false); } } } let mut stopped_dm = None; if no_reboot { match prepare_no_reboot(dm.clone()) { Ok(dm_name) => stopped_dm = dm_name, Err(e) => { error!("{}", e); return; } } if let Err(e) = kill_gpu_processes() { error!("Failed to kill GPU processes: {}", e); } } // persistenced enable let is_debug = log::log_enabled!(log::Level::Debug); let mut enable_cmd = Command::new("systemctl"); enable_cmd.args(["enable", "nvidia-persistenced.service"]); if !is_debug { enable_cmd.stdout(std::process::Stdio::null()); enable_cmd.stderr(std::process::Stdio::null()); } match enable_cmd.status() { Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"), _ => error!("An error ocurred while enabling service"), } if no_reboot { println!("Operation completed successfully"); finalize_no_reboot(stopped_dm); } else { rebuild_initramfs(); println!("Operation completed successfully"); println!("Please reboot your computer for changes to take effect!"); } } fn run_on_dgpu(command: &[String]) { if command.is_empty() { error!("No command provided to run."); return; } if get_current_mode() != "hybrid" { log::warn!( "Running on dGPU is typically only effective in Hybrid mode. \ Your current mode is '{}'. The application may fail to start or \ not use the NVIDIA GPU.", get_current_mode() ); } log::info!("Launching '{}' on the discrete GPU...", command.join(" ")); let mut child = Command::new(&command[0]); if command.len() > 1 { child.args(&command[1..]); } // Set PRIME offload variables natively child.env("__NV_PRIME_RENDER_OFFLOAD", "1"); child.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); child.env("__VK_LAYER_NV_optimus", "NVIDIA_only"); match child.status() { Ok(status) => { log::info!("Application exited with: {}", status); } Err(e) => { error!("Failed to launch application: {}", e); } } } pub fn execute(cmd: &GpuCommands) { match cmd { GpuCommands::Integrated { .. } => println!("Switching to Integrated GPU mode..."), GpuCommands::Hybrid { .. } => println!("Switching to Hybrid GPU mode..."), GpuCommands::Nvidia { .. } => println!("Switching to NVIDIA-only GPU mode..."), _ => {} } if try_call_daemon(cmd) { return; } execute_local(cmd); } pub fn execute_local(cmd: &GpuCommands) { match cmd { GpuCommands::Query => { let mode = get_current_mode(); println!("{}", mode); } GpuCommands::CacheCreate => { assert_root(); create_cache_file(); } GpuCommands::CacheDelete => { assert_root(); if Path::new(CACHE_FILE_PATH).exists() { let _ = fs::remove_file(CACHE_FILE_PATH); if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() { let _ = fs::remove_dir(parent); } } } GpuCommands::CacheQuery => { if let Ok(content) = fs::read_to_string(CACHE_FILE_PATH) { println!("{}", content); } else { println!("ERROR: Could not read {}", CACHE_FILE_PATH); } } GpuCommands::Reset => { assert_root(); if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() { create_cache_file(); } cleanup(); if Path::new(CACHE_FILE_PATH).exists() { let _ = fs::remove_file(CACHE_FILE_PATH); if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() { let _ = fs::remove_dir(parent); } } rebuild_initramfs(); println!("Operation completed successfully"); } GpuCommands::ResetSddm => { assert_root(); create_file(SDDM_XSETUP_PATH, SDDM_XSETUP_CONTENT, true); println!("Operation completed successfully"); } GpuCommands::Integrated { no_reboot } => switch_integrated(*no_reboot), GpuCommands::Hybrid { rtd3, use_nvidia_current, no_reboot, } => switch_hybrid(*rtd3, *use_nvidia_current, *no_reboot), GpuCommands::Nvidia { dm, force_comp, coolbits, use_nvidia_current, wayland, no_reboot, } => switch_nvidia( dm.clone(), *force_comp, *coolbits, *use_nvidia_current, *wayland, *no_reboot, ), GpuCommands::Run { command } => run_on_dgpu(command), } }