├── .gitignore ├── LICENSE ├── README.md ├── completion └── zsh │ └── _optimus-manager ├── config ├── nvidia-disable.sh ├── nvidia-enable.sh ├── optimus-manager.conf ├── xorg │ ├── hybrid-mode │ │ ├── integrated-gpu.conf │ │ └── nvidia-gpu.conf │ ├── integrated-mode │ │ └── integrated-gpu.conf │ └── nvidia-mode │ │ ├── integrated-gpu.conf │ │ └── nvidia-gpu.conf ├── xsetup-hybrid.sh ├── xsetup-integrated.sh └── xsetup-nvidia.sh ├── login_managers ├── lightdm │ └── 20-optimus-manager.conf └── sddm │ └── 20-optimus-manager.conf ├── modules └── optimus-manager.conf ├── openrc └── optimus-manager ├── optimus-manager.1 ├── optimus-manager.conf ├── optimus_manager ├── __init__.py ├── acpi_data.py ├── checks.py ├── client │ ├── __init__.py │ ├── args.py │ ├── client_checks.py │ └── error_reporting.py ├── config.py ├── config_schema.json ├── daemon.py ├── envs.py ├── hooks │ ├── __init__.py │ ├── post_daemon_stop.py │ ├── post_resume.py │ ├── post_xorg_start.py │ ├── pre_daemon_start.py │ ├── pre_suspend.py │ └── pre_xorg_start.py ├── kernel.py ├── kernel_parameters.py ├── log_utils.py ├── pci.py ├── processes.py ├── sessions.py ├── var.py └── xorg.py ├── package ├── 1-build.sh ├── 2-install.sh ├── 3-publish.sh ├── PKGBUILD └── optimus-manager.install ├── profile.d └── optimus-manager.sh ├── runit ├── finish └── run ├── s6 ├── dependencies ├── finish ├── run └── type ├── setup.py ├── system-info.sh └── systemd ├── optimus-manager.service └── suspend └── optimus-manager.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | optimus_manager.egg-info/ 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Askannz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚙️ Function 2 | 3 | Enhances the performance and power management on Nvidia Optimus Laptops, by properly selecting when to use each GPU. 4 | 5 | The Nvidia GPU runs the whole desktop, while the Intel/AMD GPU acts as relay between the Nvidia GPU and the screen. 6 | 7 | More info at the [wiki](https://github.com/Askannz/optimus-manager/wiki). 8 | 9 | 10 | ## 📚 Guides 11 | 12 | Most issues are due to the Nvidia driver itself. If you are experiencing any, first try: 13 | - [Driver installation](https://wiki.archlinux.org/title/NVIDIA). 14 | - [Driver troubleshooting](https://wiki.archlinux.org/title/NVIDIA/Troubleshooting). 15 | - [Optimus Manager FAQ](https://github.com/Askannz/optimus-manager/wiki/FAQ,-common-issues,-troubleshooting). 16 | 17 | ## 🔧 Contributing 18 | 19 | If you figured out how to fix an issue, or to how improve ease of use, you may contribute an improvement: 20 | 1. Click on the "Fork" button. 21 | 2. `git clone git@github.com:[YOUR-USER]/optimus-manager.git` 22 | 3. Modify the faulty files. 23 | 4. Thorougly test that the program still works. See the scripts at [`package`](https://github.com/Askannz/optimus-manager/tree/master/package). 24 | 5. `git summary` + `git add --all` + `git commit --message="[SUMMARY OF CHANGES]` + `git push`. 25 | 6. Open a [pull request](https://github.com/Askannz/optimus-manager/pulls). 26 | 7. Accepted in two days. 27 | 28 | ## 🗳️ Reporting issues 29 | 30 | If you are unable to fix an issue by yourself, or how to implement an idea to make things easier, report it: 31 | 1. Isolate which specific config is causing your issue. 32 | 2. Collect system info, by in the app "terminal" typing `curl --silent https://raw.githubusercontent.com/Askannz/optimus-manager/refs/heads/master/system-info.sh | bash &> ~/system-info.txt`. 33 | 3. Open an [issue report](https://github.com/Askannz/optimus-manager/issues). Include the file `system-info.txt`, generated above. 34 | 4. When requesting further info your report may be closed. Just reopen it when done so. 35 | 36 | 37 | ## 🖥️ Supported platforms 38 | 39 | - Graphic protocols: Xorg, Wayland without configurable options. 40 | - Display managers : SDDM, LightDM, GDM, [custom](https://github.com/Askannz/optimus-manager/wiki/FAQ,-common-issues,-troubleshooting#my-display-manager-is-not-sddm-lightdm-nor-sddm), [none](https://github.com/Askannz/optimus-manager/wiki/FAQ,-common-issues,-troubleshooting#i-do-not-use-a-display-manager-i-use-startx-or-xinit). 41 | 42 | 43 | ## 💽 Installation 44 | 45 | 1. If you are not using the standard `linux` kernel, install the `linux-headers` package variant that matches your kernel name. 46 | 47 | 2. [Install the appropiate `nvidia` package](https://wiki.archlinux.org/title/NVIDIA#Installation). 48 | 49 | 3. Install the `optimus-manager` package. In the AUR: [`optimus-manager-git`](https://aur.archlinux.org/packages/optimus-manager-git). 50 | 51 | 52 | ## 📝 Configuration 53 | 54 | On X11 the Nvidia GPU is used for everything by default. This provides maximum performance and ease of use at the expense of power consumption. If you want to try to optimize this, see `/etc/optimus-manager/`. 55 | 56 | On Wayland the Nvidia GPU is used for high performance apps which use GLX or Vulkan. While the integrated GPU for no so demanding apps which use EGL, like the desktop itself and the web browser. This behavior is not configurable. 57 | 58 | 59 | ## 🔀 Modes 60 | 61 | * `nvidia` switches to the Nvidia GPU. 62 | * `integrated` switches to the integrated GPU, and powers the Nvidia GPU off. 63 | * `hybrid` switches to the integrated GPU, but leaves the Nvidia GPU available for on-demand offloading. Similar to how Optimus works on Windows. More info at [the Wiki](https://github.com/Askannz/optimus-manager/wiki/Nvidia-GPU-offloading-for-%22hybrid%22-mode). 64 | 65 | ⚠️ Warning: 66 | - In the configuration file, if `auto_logout=yes`, switching will log out and close all applications. 67 | - Switching to and from "integrated" mode can be unstable. 68 | 69 | 70 | ## 📎 System Tray 71 | 72 | All desktops: 73 | * [`optimus-manager-qt`](https://github.com/Shatur95/optimus-manager-qt). 74 | 75 | Gnome: 76 | * [`optimus-manager-indicator`](https://extensions.gnome.org/extension/2908/optimus-manager-indicator/). 77 | * [`optimus-manager-argos`](https://github.com/inzar98/optimus-manager-argos). 78 | 79 | 80 | ## 🎰 Boot entries 81 | 82 | Useful if you want to have different entries for different GPU startup modes. 83 | 84 | This only affects which GPU your desktop session starts with, nothing prior to that. 85 | 86 | Edit your boot loader config to have the kernel parameter `optimus-manager.startup=[nvidia\integrated\hybrid]`. 87 | 88 | Or if you are using the GRUB bootloader, you can use [`optimus-manager-grub`](https://github.com/hakasapl/optimus-manager-grub). 89 | 90 | 91 | ## 📜 Terminal 92 | 93 | - See `man optimus-manager` 94 | -------------------------------------------------------------------------------- /completion/zsh/_optimus-manager: -------------------------------------------------------------------------------- 1 | #compdef optimus-manager 2 | 3 | declare -a args 4 | args=( 5 | '(-)'{-h,--help}'[displays usage information]' 6 | '(-)'{-V,--version}'[prints the version]' 7 | '(-)--status[prints the current status]' 8 | '(-)--print-mode[prints the current GPU mode]' 9 | '(-)--print-next-mode[prints the GPU mode that will be used on the next login]' 10 | '(-)--print-startup[prints the GPU mode that will be used on startup]' 11 | '--switch=[sets the GPU mode for future logins]:mode:(nvidia integrated hybrid)' 12 | '(--unset-temp-config)--temp-config=[sets the temporary configuration file to use only on next boot]:path:_files' 13 | '(--temp-config)--unset-temp-config[reverts --temp-config]' 14 | '--no-confirm[skips the confirmation for loggin out]' 15 | '--cleanup[removes auto-generated configuration files left over by the daemon]' 16 | ) 17 | 18 | _arguments $args 19 | -------------------------------------------------------------------------------- /config/nvidia-disable.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Executed if `switching=custom`. 4 | # Write here your own custom commands for powering down the Nvidia GPU. 5 | -------------------------------------------------------------------------------- /config/nvidia-enable.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Executed if `switching=custom`. 4 | # Write here your own custom commands for powering up the Nvidia GPU. 5 | -------------------------------------------------------------------------------- /config/optimus-manager.conf: -------------------------------------------------------------------------------- 1 | # Info at `/usr/share/optimus-manager/optimus-manager.conf`. 2 | -------------------------------------------------------------------------------- /config/xorg/hybrid-mode/integrated-gpu.conf: -------------------------------------------------------------------------------- 1 | # Options passed to the `Device` section at `/etc/X11/xorg.conf.d/10-optimus-manager.conf`. 2 | # Example: Option "Backlight" "intel_backlight" 3 | -------------------------------------------------------------------------------- /config/xorg/hybrid-mode/nvidia-gpu.conf: -------------------------------------------------------------------------------- 1 | # Options passed to the `Device` section at `/etc/X11/xorg.conf.d/10-optimus-manager.conf`. 2 | # Example: Option "ConnectToAcpid" "0" 3 | -------------------------------------------------------------------------------- /config/xorg/integrated-mode/integrated-gpu.conf: -------------------------------------------------------------------------------- 1 | # Options passed to the `Device` section at `/etc/X11/xorg.conf.d/10-optimus-manager.conf`. 2 | # Example: Option "Backlight" "intel_backlight" 3 | -------------------------------------------------------------------------------- /config/xorg/nvidia-mode/integrated-gpu.conf: -------------------------------------------------------------------------------- 1 | # Options passed to the `Device` section at `/etc/X11/xorg.conf.d/10-optimus-manager.conf`. 2 | # Example: Option "Backlight" "intel_backlight" 3 | -------------------------------------------------------------------------------- /config/xorg/nvidia-mode/nvidia-gpu.conf: -------------------------------------------------------------------------------- 1 | # Options passed to the `Device` section at `/etc/X11/xorg.conf.d/10-optimus-manager.conf`. 2 | # Example: Option "ConnectToAcpid" "0" 3 | -------------------------------------------------------------------------------- /config/xsetup-hybrid.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Write here your own custom commands for setting up the login screen in `hybrid` mode. 4 | -------------------------------------------------------------------------------- /config/xsetup-integrated.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Write here your own custom commands for setting up the login screen in `integrated` mode. 4 | -------------------------------------------------------------------------------- /config/xsetup-nvidia.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Write here your own custom commands for setting up the login screen in `nvidia` mode. 4 | # Executed before setting up Prime with xrandr commands. 5 | -------------------------------------------------------------------------------- /login_managers/lightdm/20-optimus-manager.conf: -------------------------------------------------------------------------------- 1 | [Seat:*] 2 | # Forces using X 3 | type=local 4 | display-setup-script=/sbin/prime-offload 5 | display-stopped-script=/sbin/prime-switch 6 | -------------------------------------------------------------------------------- /login_managers/sddm/20-optimus-manager.conf: -------------------------------------------------------------------------------- 1 | [X11] 2 | DisplayCommand=/sbin/prime-offload 3 | DisplayStopCommand=/sbin/prime-switch 4 | -------------------------------------------------------------------------------- /modules/optimus-manager.conf: -------------------------------------------------------------------------------- 1 | blacklist nouveau 2 | blacklist nvidia_drm 3 | blacklist nvidia_uvm 4 | blacklist nvidia_modeset 5 | blacklist nvidia 6 | -------------------------------------------------------------------------------- /openrc/optimus-manager: -------------------------------------------------------------------------------- 1 | #! /sbin/openrc-run 2 | 3 | # Source: https://github.com/anlorn/jorgicio-gentoo-overlay/blob/master/x11-misc/optimus-manager/files/optimus-manager-9999.sh 4 | # By @Anlorn 5 | 6 | command="/usr/bin/python3 -u -m optimus_manager.daemon" 7 | pidfile="${pidfile-/var/run/optimus-manager.pid}" 8 | description="Optimus Manager Commands daemon" 9 | 10 | depend() { 11 | before xdm 12 | before lightdm 13 | before sddm 14 | before gdm 15 | } 16 | 17 | start_pre() { 18 | /usr/bin/python3 -u -m optimus_manager.hooks.pre_daemon_start 19 | /usr/bin/python3 -u -m optimus_manager.hooks.pre_xorg_start 20 | } 21 | 22 | stop_post() { 23 | /usr/bin/python3 -u -m optimus_manager.hooks.post_daemon_stop 24 | } 25 | 26 | start() { 27 | ebegin "Starting Optimus Manager daemon" 28 | start-stop-daemon --quiet --background --start \ 29 | --make-pidfile --pidfile $pidfile -- $command 30 | eend $? "Failed to start Optimus Manager daemon" 31 | } 32 | 33 | stop() { 34 | ebegin "Stopping Optimus Manager daemon" 35 | start-stop-daemon --quiet --stop --pidfile $pidfile --signal QUIT 36 | eend $? "Failed to stop Optimus Manager daemon" 37 | } 38 | -------------------------------------------------------------------------------- /optimus-manager.1: -------------------------------------------------------------------------------- 1 | .TH optimus-manager "1" 2 | 3 | 4 | .SH NAME 5 | optimus-manager - configures Nvidia Optimus graphics 6 | 7 | 8 | .SH USAGE 9 | optimus-manager [option] 10 | 11 | 12 | .SH SWITCHING OPTIONS 13 | 14 | .TP 15 | .SS --switch [mode] 16 | Sets the GPU mode for future logins. Modes: nvidia, integrated, hybrid. 17 | 18 | .TP 19 | .SS --now 20 | Skips the confirmation for loggin out. 21 | 22 | 23 | .SH PRINT OPTIONS 24 | 25 | .TP 26 | .SS --status 27 | Prints the current status. 28 | 29 | .TP 30 | .SS --current-mode 31 | Prints the current GPU mode. 32 | 33 | .TP 34 | .SS --next-mode 35 | Prints the GPU mode that will be used on the next login. 36 | 37 | .TP 38 | .SS --startup-mode 39 | Prints the GPU mode that will be used on startup. 40 | 41 | 42 | .SH CONFIG OPTIONS 43 | 44 | .TP 45 | .SS --config [file] 46 | Sets the temporary configuration file to use only on next boot. 47 | 48 | .TP 49 | .SS --unset-config 50 | Reverts "--config". 51 | 52 | .TP 53 | .SS --cleanup 54 | Removes auto-generated configuration files left over by the daemon. 55 | -------------------------------------------------------------------------------- /optimus-manager.conf: -------------------------------------------------------------------------------- 1 | # [ WARNING ] 2 | # - Users: do not edit this file. 3 | # - Edit instead `/etc/optimus-manager/optimus-manager.conf`. 4 | # - This is for reference only. 5 | # - And for fallback defaults. 6 | 7 | # [ INSTRUCTIONS ] 8 | # The default settings provide the best ease and performance, along with basic power savings. 9 | # If you want even better power savings: 10 | # 1) Have an updated live USB around, just in case. 11 | # 2) Make a custom config only with the variables you want to change, including section headers. 12 | # 3) Test the custom config with `optimus-manager --temp-config [FILE]`. 13 | # 4) If it works save it into `/etc/optimus-manager/optimus-manager.conf`. 14 | # 5) Reboot. 15 | # 6) If the system no longer boots, use `Ctrl+Alt+F3` or the live USB to revert the config. 16 | 17 | 18 | [optimus] 19 | 20 | startup_mode=nvidia 21 | # - `nvidia` (high performance, high stability, high ease, high power). 22 | # - `integrated` (average performance, high stability, low power). 23 | # - `hybrid` (manual nvidia offloading, flexible power). 24 | # - `auto` (based on battery state). 25 | # - `auto_nvdisplay` (if the display connects directly to the Nvidia GPU). 26 | # Check which GPUs support offloading with `xrandr --listproviders | grep 'NVIDIA'` 27 | 28 | startup_auto_extpower_mode=nvidia 29 | startup_auto_battery_mode=integrated 30 | startup_auto_nvdisplay_on_mode=nvidia 31 | startup_auto_nvdisplay_off_mode=integrated 32 | 33 | switching=none 34 | # Method used to switch the Nvidia card. 35 | # - `nouveau` loads the `nouveau` module. 36 | # - `bbswitch` powers off the card using the `bbswitch` dependency. 37 | # - `acpi_call` brute forces various ACPI method using the `acpi_call` dependency. 38 | # - `custom` uses `/etc/optimus-manager/nvidia-[enable\disable].sh` 39 | # - `none` uses no external module. Maybe `pci_power_control` below. 40 | # Working ACPI calls are catched at `/var/lib/optimus-manager/acpi_call_string.json`. 41 | 42 | pci_power_control=no 43 | # Enables PCI power management in `integrated` mode. 44 | # Ignored while using `switching=acpi_call` or `switching=bbswitch`. 45 | 46 | pci_remove=no 47 | # Removes the Nvidia card from the PCI bus. 48 | # May prevent crashes. 49 | # Ignored if `switching=nouveau` or `switching=bbswitch`. 50 | 51 | pci_reset=no 52 | # Ensures the card is in a fresh state before reloading the nvidia module. 53 | # May fix hangs with `switching=acpi_call`. 54 | # - `no` does not perform any reset. 55 | # - `function_level` performs a selective light reset. 56 | # - `hot_reset` performs a hardware reset of the PCI bridge. ATTENTION: May stress hardware. 57 | 58 | auto_logout=yes 59 | # Automatically log out the current desktop session when switching GPUs. 60 | # If disabled or not supported, GPU switching will apply on next login. 61 | # Supported desktops: 62 | # AwesomeWM, bspwm, Deepin, dwm, GNOME, herbstluftwm, i3, KDE Plasma, LXDE, Openbox, Xfce, Xmonad. 63 | 64 | 65 | [nvidia] 66 | 67 | dynamic_power_management=fine 68 | # Allows the card to go into low power if it's not in use. 69 | # Works only in `hybrid`mode. 70 | # Check if supported with `grep "Runtime D3 status" /proc/driver/nvidia/gpus/*/power` 71 | # - `no` disables power management. 72 | # - `coarse` goes into low power if no application is using the Nvidia driver. 73 | # - `fine` goes into low power if apps haven't submitted GPU work for a short period. 74 | 75 | dynamic_power_management_memory_threshold= 76 | # Threshold, in megabytes, under which the memory is put in a low-power state. 77 | # As power to the memory is handled separately from the rest of the GPU. 78 | # Works only with `dynamic_power_management=fine`. 79 | # Leave blank for the default (200MB). Values over that are ignored. 80 | # `0` keeps the memory always powered on. 81 | 82 | DPI=96 83 | # Runs `xrandr --dpi [DPI]`. 84 | # Blank to omit. 85 | 86 | options= 87 | # Separate with comma. 88 | # - `overclocking` enables overclocking in the Nvidia Control Panel. ATTENTION: May stress hardware. 89 | # - `triple_buffer` makes framerate smoother at the expense of lag. 90 | 91 | modeset=yes 92 | # Required for Prime Sync, which prevents tearing. 93 | 94 | PAT=yes 95 | # Sets `NVreg_UsePageAttributeTable` 96 | # Disabling can cause poor CPU performance. 97 | 98 | ignore_abi=no 99 | # Loads the driver with a newer `xorg-server` ABI. 100 | 101 | allow_external_gpus=no 102 | 103 | 104 | [intel] 105 | 106 | driver=modesetting 107 | # - `modesetting` uses the built-in kernel driver. 108 | # - `intel` uses `xf86-video-intel`. 109 | # - `hybrid` uses `modesetting` on `nvidia` mode, and `intel` for `integrated` mode. 110 | 111 | accel= 112 | # Sets the `AccelMethod` in the Xorg configuration. 113 | # Options: `sna` `xna` `uxa` `none`. 114 | # Leave blank for the default (no option specified). 115 | 116 | tearfree= 117 | # Sets `TearFree` in the Xorg configuration. 118 | # Options: `yes` no`. 119 | # Leave blank for the default (no option specified) 120 | 121 | DRI=3 122 | # DRI version. 123 | # Options: `3` `2` `0`. 124 | # `0` omits the option. 125 | 126 | modeset=yes 127 | # Sets modesetting for the nouveau driver. 128 | # Does not affect the Intel GPU driver. 129 | # Works only with `switching=nouveau` 130 | 131 | 132 | [amd] 133 | 134 | driver=modesetting 135 | # - `modesetting` uses the built-in kernel driver. 136 | # - `amdgpu` uses `xf86-video-amdgpu`. 137 | # - `hybrid` uses `modesetting` for `nvidia` mode, and `amdgpu` for `integrated` mode. 138 | 139 | tearfree= 140 | # Sets `TearFree` in the Xorg configuration. 141 | # Options: `yes` no`. 142 | # Leave blank for the default (no option specified) 143 | 144 | DRI=3 145 | # DRI version. 146 | # Options: `3` `2` `0`. 147 | # `0` omits the option. 148 | -------------------------------------------------------------------------------- /optimus_manager/__init__.py: -------------------------------------------------------------------------------- 1 | from .envs import VERSION 2 | __version__ = VERSION 3 | -------------------------------------------------------------------------------- /optimus_manager/acpi_data.py: -------------------------------------------------------------------------------- 1 | # Taken from: `/usr/share/acpi_call/examples/turn_off_gpu.sh` 2 | # `On` commands are guessed 3 | 4 | ACPI_STRINGS = [ 5 | ("\\_SB.PCI0.P0P1.VGA._OFF", "\\_SB.PCI0.P0P1.VGA._ON"), 6 | ("\\_SB.PCI0.P0P2.VGA._OFF", "\\_SB.PCI0.P0P2.VGA._ON"), 7 | ("\\_SB.PCI0.P0P3.PEGP._OFF", "\\_SB.PCI0.P0P3.PEGP._ON"), 8 | ("\\_SB.PCI0.P0P2.PEGP._OFF", "\\_SB.PCI0.P0P2.PEGP._ON"), 9 | ("\\_SB.PCI0.P0P1.PEGP._OFF", "\\_SB.PCI0.P0P1.PEGP._ON"), 10 | ("\\_SB.PCI0.MXR0.MXM0._OFF", "\\_SB.PCI0.MXR0.MXM0._ON"), 11 | ("\\_SB.PCI0.PEG1.GFX0._OFF", "\\_SB.PCI0.PEG1.GFX0._ON"), 12 | ("\\_SB.PCI0.PEG0.GFX0.DOFF", "\\_SB.PCI0.PEG0.GFX0.DON"), 13 | ("\\_SB.PCI0.PEG1.GFX0.DOFF", "\\_SB.PCI0.PEG1.GFX0.DON"), 14 | ("\\_SB.PCI0.PEG0.PEGP._OFF", "\\_SB.PCI0.PEG0.PEGP._ON"), 15 | ("\\_SB.PCI0.XVR0.Z01I.DGOF", "\\_SB.PCI0.XVR0.Z01I.DGON"), 16 | ("\\_SB.PCI0.PEGR.GFX0._OFF", "\\_SB.PCI0.PEGR.GFX0._ON"), 17 | ("\\_SB.PCI0.PEG.VID._OFF", "\\_SB.PCI0.PEG.VID._ON"), 18 | ("\\_SB.PCI0.PEG0.VID._OFF", "\\_SB.PCI0.PEG0.VID._ON"), 19 | ("\\_SB.PCI0.P0P2.DGPU._OFF", "\\_SB.PCI0.P0P2.DGPU._ON"), 20 | ("\\_SB.PCI0.P0P4.DGPU.DOFF", "\\_SB.PCI0.P0P4.DGPU.DON"), 21 | ("\\_SB.PCI0.IXVE.IGPU.DGOF", "\\_SB.PCI0.IXVE.IGPU.DGON"), 22 | ("\\_SB.PCI0.LPC.EC.PUBS._OFF", "\\_SB.PCI0.LPC.EC.PUBS._ON"), 23 | ("\\_SB.PCI0.P0P2.NVID._OFF", "\\_SB.PCI0.P0P2.NVID._ON"), 24 | ("\\_SB_.PCI0.PEGP.DGFX._OFF", "\\_SB_.PCI0.PEGP.DGFX._ON"), 25 | ("\\_SB.PCI0.GPP0.PG00._OFF", "\\_SB.PCI0.GPP0.PG00._ON"), 26 | ("\\_SB.PCI0.PEG0.PEGP.SGOF", "\\_SB.PCI0.PEG0.PEGP.SGON"), 27 | ("\\_SB.PCI0.RP05.PXSX._OFF", "\\_SB.PCI0.RP05.PXSX._ON"), 28 | ("\\_SB.PCI0.RP05.PEGP._OFF", "\\_SB.PCI0.RP05.PEGP._ON") 29 | ] 30 | -------------------------------------------------------------------------------- /optimus_manager/checks.py: -------------------------------------------------------------------------------- 1 | import dbus 2 | import os 3 | import re 4 | import subprocess 5 | from ctypes import byref, c_int, c_uint, c_void_p, CDLL, POINTER, Structure 6 | from pathlib import Path 7 | from .log_utils import get_logger 8 | 9 | 10 | class CheckError(Exception): 11 | pass 12 | 13 | 14 | def check_running_graphical_session(): 15 | return subprocess.run( 16 | "xhost", 17 | shell=True, 18 | stdout=subprocess.DEVNULL, 19 | stderr=subprocess.DEVNULL 20 | ).returncode == 0 21 | 22 | 23 | def is_ac_power_connected(): 24 | for power_source_path in Path("/sys/class/power_supply/").iterdir(): 25 | try: 26 | with open(power_source_path / "type", 'r') as f: 27 | if f.read().strip() != "Mains": 28 | continue 29 | 30 | with open(power_source_path / "online", 'r') as f: 31 | if f.read(1) == "1": 32 | return True 33 | 34 | except IOError: 35 | continue 36 | 37 | return False 38 | 39 | 40 | class NvCfgPciDevice(Structure): 41 | _fields_ = [("domain", c_int), ("bus", c_int), ("slot", c_int), ("function", c_int)] 42 | 43 | 44 | NvCfgPciDevicePtr = POINTER(NvCfgPciDevice) 45 | 46 | 47 | def is_nvidia_display_connected(): 48 | num_gpus = c_int() 49 | gpus = NvCfgPciDevicePtr() 50 | 51 | try: 52 | nvcfg_lib = CDLL("libnvidia-cfg.so") 53 | 54 | except OSError: 55 | raise CheckError("Missing nvidia-utils component: libnvidia-cfg.so") 56 | 57 | if nvcfg_lib.nvCfgGetPciDevices(byref(num_gpus), byref(gpus)) != 1: 58 | return False 59 | 60 | for i in range(num_gpus.value): 61 | device_handle = c_void_p() 62 | 63 | try: 64 | if nvcfg_lib.nvCfgOpenPciDevice(gpus[i].domain, gpus[i].bus, gpus[i].slot, 65 | c_int(0), byref(device_handle)) != 1: 66 | continue 67 | 68 | mask = c_uint() 69 | 70 | if nvcfg_lib.nvCfgGetDisplayDevices(device_handle, byref(mask)) != 1: 71 | continue 72 | 73 | if mask.value != 0: 74 | return True 75 | 76 | finally: 77 | nvcfg_lib.nvCfgCloseDevice(device_handle) 78 | # Ignores if the function fails 79 | 80 | return False 81 | 82 | 83 | def is_pat_available(): 84 | return subprocess.run( 85 | "grep -E '^flags.+ pat( |$)' /proc/cpuinfo", 86 | shell=True, stdout=subprocess.DEVNULL 87 | ).returncode == 0 88 | 89 | 90 | def get_active_renderer(): 91 | if _is_gl_provider_nvidia(): 92 | return "nvidia" 93 | else: 94 | return "integrated" 95 | 96 | 97 | def get_integrated_provider(): 98 | try: 99 | out = subprocess.check_output( 100 | "xrandr --listproviders", shell=True, text=True, stderr=subprocess.PIPE).strip() 101 | 102 | except subprocess.CalledProcessError as error: 103 | raise CheckError(f"No xrandr provider: {error.stderr}") from error 104 | 105 | for line in out.splitlines(): 106 | for _p in line.split(): 107 | if _p in ["AMD", "Intel"]: 108 | return line.split("name:")[1] 109 | 110 | return "modesetting" 111 | 112 | 113 | def is_module_available(module_name): 114 | return subprocess.run( 115 | f"modinfo -n {module_name}", 116 | shell=True, stdout=subprocess.DEVNULL 117 | ).returncode == 0 118 | 119 | 120 | def is_module_loaded(module_name): 121 | return subprocess.run( 122 | f"lsmod | grep -E \"^{module_name}\"", 123 | shell=True, stdout=subprocess.DEVNULL 124 | ).returncode == 0 125 | 126 | 127 | def get_current_display_manager(): 128 | if not os.path.isfile("/etc/systemd/system/display-manager.service"): 129 | raise CheckError("Missing: display-manager.service") 130 | 131 | dm_service_path = os.path.realpath("/etc/systemd/system/display-manager.service") 132 | dm_service_filename = os.path.split(dm_service_path)[-1] 133 | dm_name = os.path.splitext(dm_service_filename)[0] 134 | return dm_name 135 | 136 | 137 | def using_patched_GDM(): 138 | folder_path_1 = "/etc/gdm/Prime" 139 | folder_path_2 = "/etc/gdm3/Prime" 140 | return os.path.isdir(folder_path_1) or os.path.isdir(folder_path_2) 141 | 142 | 143 | def check_offloading_available(): 144 | try: 145 | out = subprocess.check_output( 146 | "xrandr --listproviders", shell=True, text=True, stderr=subprocess.PIPE).strip() 147 | 148 | except subprocess.CalledProcessError as error: 149 | raise CheckError(f"Unable to list xrandr providers: {error.stderr}") from error 150 | 151 | for line in out.splitlines(): 152 | if re.search("^Provider [0-9]+:", line) and "name:NVIDIA-G0" in line: 153 | return True 154 | 155 | return False 156 | 157 | 158 | def is_xorg_intel_module_available(): 159 | return os.path.isfile("/usr/lib/xorg/modules/drivers/intel_drv.so") 160 | 161 | 162 | def is_xorg_amdgpu_module_available(): 163 | return os.path.isfile("/usr/lib/xorg/modules/drivers/amdgpu_drv.so") 164 | 165 | 166 | def is_login_manager_active(): 167 | return _is_service_active("display-manager") 168 | 169 | 170 | def is_daemon_active(): 171 | return _is_service_active("optimus-manager") 172 | 173 | 174 | def is_bumblebeed_service_active(): 175 | return _is_service_active("bumblebeed") 176 | 177 | 178 | def _is_gl_provider_nvidia(): 179 | try: 180 | out = subprocess.check_output( 181 | "__NV_PRIME_RENDER_OFFLOAD=0 glxinfo", 182 | shell=True, text=True, stderr=subprocess.PIPE).strip() 183 | 184 | except subprocess.CalledProcessError as error: 185 | raise CheckError(f"glxinfo failed: {error.stderr}") from error 186 | 187 | for line in out.splitlines(): 188 | if "server glx vendor string: NVIDIA Corporation" in line: 189 | return True 190 | 191 | return False 192 | 193 | 194 | def _is_service_active(service_name): 195 | logger = get_logger() 196 | 197 | if subprocess.run(f"which sv", shell=True).returncode == 0: 198 | return _is_service_active_sv(service_name) 199 | 200 | if subprocess.run(f"which rc-status", shell=True).returncode == 0: 201 | return _is_service_active_openrc(service_name) 202 | 203 | if subprocess.run(f"which s6-svstat", shell=True).returncode == 0: 204 | return _is_service_active_s6(service_name) 205 | 206 | if subprocess.run(f"which systemctl", shell=True).returncode == 0: 207 | try: 208 | system_bus = dbus.SystemBus() 209 | 210 | except dbus.exceptions.DBusException: 211 | logger.warning( 212 | "Falling back to Bash commands: No DBus for: %s", service_name) 213 | 214 | return _is_service_active_bash(service_name) 215 | 216 | else: 217 | return _is_service_active_dbus(system_bus, service_name) 218 | 219 | return False # No service manager found 220 | 221 | 222 | def _is_service_active_dbus(system_bus, service_name): 223 | systemd = system_bus.get_object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") 224 | 225 | try: 226 | unit_path = systemd.GetUnit("%s.service" % service_name, dbus_interface="org.freedesktop.systemd1.Manager") 227 | 228 | except dbus.exceptions.DBusException: 229 | return False 230 | 231 | optimus_manager_interface = system_bus.get_object("org.freedesktop.systemd1", unit_path) 232 | properties_manager = dbus.Interface(optimus_manager_interface, 'org.freedesktop.DBus.Properties') 233 | state = properties_manager.Get("org.freedesktop.systemd1.Unit", "SubState") 234 | return state == "running" 235 | 236 | 237 | def _is_service_active_bash(service_name): 238 | return subprocess.run( 239 | f"systemctl is-active {service_name}", shell=True 240 | ).returncode == 0 241 | 242 | 243 | def _is_service_active_openrc(service_name): 244 | if subprocess.run(f"rc-status --nocolor default | grep -E '%s.*started'" % service_name, shell=True).returncode == 0: 245 | return True 246 | return False 247 | 248 | 249 | def _is_service_active_s6(service_name): 250 | # TODO: Check if s6 service is running 251 | return True 252 | 253 | 254 | def _is_service_active_sv(service_name): 255 | if subprocess.run(f"sv status %s | grep 'up: '" % service_name, shell=True).returncode == 0: 256 | return True 257 | 258 | return False 259 | -------------------------------------------------------------------------------- /optimus_manager/client/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | import argparse 3 | import json 4 | import os 5 | import socket 6 | import sys 7 | from .args import parse_args 8 | from .error_reporting import report_errors 9 | from .client_checks import run_switch_checks 10 | from .. import checks 11 | from .. import envs 12 | from .. import sessions 13 | from ..config import load_config, ConfigError 14 | from ..kernel_parameters import get_kernel_parameters 15 | from ..var import read_temp_conf_path_var, load_state, VarError 16 | from ..xorg import cleanup_xorg_conf 17 | 18 | 19 | def main(): 20 | args = parse_args() 21 | state = load_state() 22 | fatal = report_errors(state) 23 | config = _get_config() 24 | 25 | if args.version: 26 | _print_version() 27 | 28 | elif args.print_startup: 29 | _print_startup_mode(config) 30 | 31 | elif args.temp_config: 32 | _set_temp_config_and_exit(args.temp_config) 33 | 34 | elif args.unset_temp_config: 35 | _unset_temp_config_and_exit() 36 | 37 | elif args.cleanup: 38 | _cleanup_xorg_and_exit() 39 | 40 | else: 41 | if fatal: 42 | sys.exit(1) 43 | 44 | elif args.print_mode: 45 | _print_current_mode(state) 46 | 47 | elif args.print_next_mode: 48 | _print_next_mode(state) 49 | 50 | elif args.status: 51 | _print_status(config, state) 52 | 53 | elif args.switch: 54 | _gpu_switch(config, args.switch, args.no_confirm) 55 | 56 | else: 57 | print("Invalid arguments") 58 | sys.exit(1) 59 | 60 | sys.exit(0) 61 | 62 | 63 | def _ask_confirmation(): 64 | ans = input("> ").lower() 65 | 66 | if ans == "y": 67 | return True 68 | 69 | else: 70 | if ans != "n": 71 | print("Invalid choice") 72 | 73 | print("Canceled") 74 | return False 75 | 76 | 77 | def _gpu_switch(config, switch_mode, no_confirm): 78 | if switch_mode not in ["integrated", "nvidia", "hybrid", "intel"]: 79 | print("Invalid mode: %s" % switch_mode) 80 | sys.exit(1) 81 | 82 | if switch_mode == "intel": 83 | switch_mode = "integrated" 84 | 85 | run_switch_checks(config, switch_mode) 86 | 87 | if config["optimus"]["auto_logout"] == "yes": 88 | if no_confirm: 89 | confirmation = True 90 | 91 | else: 92 | print("This will close all desktops and applications\n" 93 | "(Disable this warning with: --no-confirm)\n" 94 | "Continue? (y/N)") 95 | confirmation = _ask_confirmation() 96 | 97 | if confirmation: 98 | _send_switch_command(config, switch_mode) 99 | 100 | else: 101 | sys.exit(1) 102 | 103 | else: 104 | _send_switch_command(config, switch_mode) 105 | print("The change will apply on next login") 106 | 107 | 108 | def _send_switch_command(config, requested_mode): 109 | print("Switching to mode : %s" % requested_mode) 110 | command = {"type": "switch", "args": {"mode": requested_mode}} 111 | _send_command(command) 112 | 113 | if config["optimus"]["auto_logout"] == "yes": 114 | sessions.logout_current_desktop_session() 115 | 116 | 117 | def _get_config(): 118 | try: 119 | config = load_config() 120 | 121 | except ConfigError as error: 122 | print("Error loading config file: %s" % str(error)) 123 | sys.exit(1) 124 | 125 | return config 126 | 127 | 128 | def _print_version(): 129 | print("Version: %s" % envs.VERSION) 130 | 131 | 132 | def _print_current_mode(state): 133 | print("Current mode: %s" % state["current_mode"]) 134 | 135 | 136 | def _print_next_mode(state): 137 | if state["type"] == "pending_pre_xorg_start": 138 | res_str = state["requested_mode"] 139 | 140 | else: 141 | res_str = "Current" 142 | 143 | print("Mode for next login: %s" % res_str) 144 | 145 | 146 | def _print_startup_mode(config): 147 | startup_mode = config["optimus"]["startup_mode"] 148 | kernel_parameters = get_kernel_parameters() 149 | 150 | print("Startup mode: %s" % startup_mode) 151 | 152 | if kernel_parameters["startup_mode"] is not None: 153 | print("Startup mode overriden by kernel parameter: %s" 154 | % kernel_parameters["startup_mode"]) 155 | 156 | 157 | def _print_temp_config_path(): 158 | try: 159 | path = read_temp_conf_path_var() 160 | 161 | except VarError: 162 | print("Temporary config: None") 163 | 164 | else: 165 | print("Temporary config: %s" % path) 166 | 167 | 168 | def _print_status(config, state): 169 | _print_version() 170 | print("") 171 | _print_current_mode(state) 172 | _print_next_mode(state) 173 | _print_startup_mode(config) 174 | _print_temp_config_path() 175 | 176 | 177 | def _send_command(command): 178 | msg = json.dumps(command).encode('utf-8') 179 | 180 | try: 181 | client = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) 182 | client.connect(envs.SOCKET_PATH) 183 | client.send(msg) 184 | client.close() 185 | 186 | except (ConnectionRefusedError, OSError): 187 | print("Socket unavailable: %s" % envs.SOCKET_PATH) 188 | sys.exit(1) 189 | 190 | 191 | def _set_temp_config_and_exit(rel_path): 192 | abs_path = os.path.join(os.getcwd(), rel_path) 193 | 194 | if not os.path.isfile(abs_path): 195 | print("Temp config file doesn't exist: %s" % abs_path) 196 | sys.exit(1) 197 | 198 | print("Temp config file: %s" % abs_path) 199 | command = {"type": "temp_config", "args": {"path": abs_path}} 200 | _send_command(command) 201 | sys.exit(0) 202 | 203 | 204 | def _unset_temp_config_and_exit(): 205 | print("Unsetting temp config path") 206 | command = {"type": "temp_config", "args": {"path": ""}} 207 | _send_command(command) 208 | sys.exit(0) 209 | 210 | 211 | def _cleanup_xorg_and_exit(): 212 | if os.geteuid() != 0: 213 | print("Not root") 214 | sys.exit(1) 215 | 216 | cleanup_xorg_conf() 217 | sys.exit(0) 218 | 219 | 220 | if __name__ == '__main__': 221 | main() 222 | -------------------------------------------------------------------------------- /optimus_manager/client/args.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | def parse_args(): 5 | parser = argparse.ArgumentParser(description="optimus-manager client") 6 | 7 | # SWITCHING OPTIONS 8 | 9 | parser.add_argument('--switch', metavar='MODE', action='store', 10 | help="Sets the GPU mode for future logins" 11 | "Options: nvidia, integrated, hybrid") 12 | 13 | parser.add_argument('--no-confirm', '--now', action='store_true', 14 | help="Skips the confirmation for loggin out") 15 | 16 | # PRINT OPTIONS 17 | 18 | parser.add_argument('--status', action='store_true', 19 | help="Prints the current status") 20 | 21 | parser.add_argument('--print-mode', '--current-mode', action='store_true', 22 | help="Prints the current GPU mode") 23 | 24 | parser.add_argument('--print-next-mode', '--next-mode', action='store_true', 25 | help="Prints the GPU mode that will be used on the next login") 26 | 27 | parser.add_argument('--print-startup', '--startup-mode', action='store_true', 28 | help="Prints the GPU mode that will be used on startup") 29 | 30 | parser.add_argument('-v', '--version', action='store_true', 31 | help='Prints the version') 32 | 33 | # CONFIG OPTIONS 34 | 35 | parser.add_argument('--temp-config', '--config', metavar='PATH', action='store', 36 | help="Sets the temporary configuration file to use only on next boot") 37 | 38 | parser.add_argument('--unset-temp-config', '--unset-config', action='store_true', 39 | help="Reverts \"--config\"") 40 | 41 | parser.add_argument('--cleanup', action='store_true', 42 | help="Removes auto-generated configuration files left over by the daemon") 43 | 44 | return parser.parse_args() 45 | -------------------------------------------------------------------------------- /optimus_manager/client/client_checks.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .. import checks 3 | from .. import sessions 4 | 5 | 6 | def run_switch_checks(config, requested_mode): 7 | _check_daemon_active() 8 | _check_bbswitch_module(config) 9 | _check_nvidia_module(requested_mode) 10 | _check_patched_GDM() 11 | _check_number_of_sessions() 12 | 13 | 14 | def _check_bbswitch_module(config): 15 | if config["optimus"]["switching"] == "bbswitch" and not checks.is_module_available("bbswitch"): 16 | print("Not properly installed: bbswitch") 17 | sys.exit(1) 18 | 19 | 20 | def _check_daemon_active(): 21 | if not checks.is_daemon_active(): 22 | print("Not started: optimus-manager.service") 23 | sys.exit(1) 24 | 25 | 26 | def _check_number_of_sessions(): 27 | nb_desktop_sessions = sessions.get_number_of_desktop_sessions(ignore_gdm=True) 28 | 29 | if nb_desktop_sessions > 1: 30 | print("Unable to switch: Other users are logged in") 31 | sys.exit(1) 32 | 33 | 34 | def _check_nvidia_module(requested_mode): 35 | if requested_mode == "nvidia" and not checks.is_module_available("nvidia"): 36 | print("Not properly installed: nvidia") 37 | sys.exit(1) 38 | 39 | 40 | def _check_patched_GDM(): 41 | try: 42 | dm_name = checks.get_current_display_manager() 43 | 44 | except checks.CheckError as error: 45 | print("Unable to get the display manager name: %s" % str(error)) 46 | return 47 | 48 | if dm_name == "gdm" and not checks.using_patched_GDM(): 49 | print("Not properly installed: gdm-prime") 50 | sys.exit(1) 51 | -------------------------------------------------------------------------------- /optimus_manager/client/error_reporting.py: -------------------------------------------------------------------------------- 1 | from .. import envs 2 | from ..checks import get_active_renderer, check_offloading_available, check_running_graphical_session, CheckError 3 | 4 | 5 | def report_errors(state): 6 | if state is None: 7 | print("No state file") 8 | return True 9 | 10 | elif state["type"] == "startup_failed": 11 | print("Failed to start: optimus-manager.service") 12 | print("Log at %s/daemon/daemon-%s.log" % (envs.LOG_DIR_PATH, state["daemon_run_id"])) 13 | return True 14 | 15 | elif state["type"] == "pending_pre_xorg_start": 16 | if state["current_mode"] is None: 17 | print("GPU setup failed: Xorg pre-start hook did not run") 18 | return True 19 | 20 | else: 21 | print("GPU switch pending for next login: %s -> %s" % (state["current_mode"], state["requested_mode"])) 22 | 23 | elif state["type"] == "pre_xorg_start_failed": 24 | print("GPU setup failed: Xorg pre-start hook failed") 25 | print("Log at: %s/switch/switch-%s.log" % (envs.LOG_DIR_PATH, state["switch_id"])) 26 | return True 27 | 28 | elif state["type"] == "pending_post_xorg_start": 29 | print("GPU setup failed: Xorg post-start hook did not run") 30 | print("Log at: %s/switch/switch-%s.log" % (envs.LOG_DIR_PATH, state["switch_id"])) 31 | return True 32 | 33 | elif state["type"] == "post_xorg_start_failed": 34 | print("GPU setup failed: Xorg post-start hook failed") 35 | print("Log at: %s/switch/switch-%s.log" % (envs.LOG_DIR_PATH, state["switch_id"])) 36 | return True 37 | 38 | elif state["type"] == "done": 39 | if check_running_graphical_session(): 40 | expected_renderer = { 41 | "integrated": "integrated", 42 | "hybrid": "integrated", 43 | "nvidia": "nvidia" 44 | }[state["current_mode"]] 45 | 46 | try: 47 | active_renderer = get_active_renderer() 48 | 49 | except CheckError as error: 50 | print("GPU setup failed: Unable to check the active card (%s): %s" % (expected_renderer, str(error))) 51 | print("Log at: %s/switch/switch-%s.log" % (envs.LOG_DIR_PATH, state["switch_id"])) 52 | return True 53 | 54 | if expected_renderer != active_renderer: 55 | print("GPU setup failed: Wrong active card: \"%s\" vs \"%s\"" % (active_renderer, expected_renderer)) 56 | print("Log at: %s/switch/switch-%s.log" % (envs.LOG_DIR_PATH, state["switch_id"])) 57 | return True 58 | 59 | if state["current_mode"] == "hybrid" and not check_offloading_available(): 60 | print("Hybrid mode doesn't work: the Nvidia card is unavailable for offloading") 61 | print("Log at: %s/switch/switch-%s.log" % (envs.LOG_DIR_PATH, state["switch_id"])) 62 | 63 | return False 64 | 65 | else: 66 | assert False 67 | -------------------------------------------------------------------------------- /optimus_manager/config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import copy 3 | import json 4 | import os 5 | import shutil 6 | from pathlib import Path 7 | from . import envs 8 | from . import var 9 | from .log_utils import get_logger 10 | 11 | 12 | class ConfigError(Exception): 13 | pass 14 | 15 | 16 | def load_config(): 17 | config = _load_config() 18 | config = _convert_deprecated(config) 19 | return config 20 | 21 | 22 | def _load_config(): 23 | logger = get_logger() 24 | base_config = configparser.ConfigParser() 25 | base_config.read(envs.DEFAULT_CONFIG_PATH) 26 | base_config = _parsed_config_to_dict(base_config) 27 | _validate_config(base_config) 28 | 29 | if not os.path.isfile(envs.USER_CONFIG_COPY_PATH): 30 | return base_config 31 | 32 | try: 33 | user_config = configparser.ConfigParser() 34 | user_config.read([envs.DEFAULT_CONFIG_PATH, envs.USER_CONFIG_COPY_PATH]) 35 | user_config = _parsed_config_to_dict(user_config) 36 | 37 | except configparser.ParsingError as error: 38 | logger.error( 39 | "Falling back to default config: Defective user config: %s: %s", 40 | envs.USER_CONFIG_COPY_PATH, str(error)) 41 | 42 | return base_config 43 | 44 | corrected_config = _validate_config(user_config, fallback_config=base_config) 45 | return corrected_config 46 | 47 | 48 | def _convert_deprecated(config): 49 | if config["optimus"]["startup_mode"] == "intel": 50 | config["optimus"]["startup_mode"] = "integrated" 51 | 52 | if config["optimus"]["startup_auto_battery_mode"] == "intel": 53 | config["optimus"]["startup_auto_battery_mode"] = "integrated" 54 | 55 | if config["optimus"]["startup_auto_extpower_mode"] == "intel": 56 | config["optimus"]["startup_auto_extpower_mode"] = "integrated" 57 | 58 | return config 59 | 60 | 61 | def copy_user_config(): 62 | logger = get_logger() 63 | 64 | try: 65 | temp_config_path = var.read_temp_conf_path_var() 66 | 67 | except var.VarError: 68 | config_path = envs.USER_CONFIG_PATH 69 | 70 | else: 71 | logger.info("Using temporary configuration: %s", temp_config_path) 72 | var.remove_temp_conf_path_var() 73 | 74 | if os.path.isfile(temp_config_path): 75 | config_path = temp_config_path 76 | 77 | else: 78 | logger.warning( 79 | "Falling back to default user config: Temporary config doesn't exist: %s", 80 | temp_config_path) 81 | config_path = envs.USER_CONFIG_PATH 82 | 83 | if os.path.isfile(config_path): 84 | copy_path = Path(envs.USER_CONFIG_COPY_PATH) 85 | os.makedirs(copy_path.parent, exist_ok=True) 86 | logger.info("Copying \"%s\" into \"%s\"", config_path, copy_path) 87 | shutil.copy(config_path, copy_path) 88 | 89 | 90 | def _validate_config(config, fallback_config=None): 91 | logger = get_logger() 92 | folder_path = os.path.dirname(os.path.abspath(__file__)) 93 | schema_path = os.path.join(folder_path, "config_schema.json") 94 | 95 | with open(schema_path, "r") as schemapath: 96 | schema = json.load(schemapath) 97 | 98 | corrected_config = copy.deepcopy(config) 99 | 100 | # Checking for required sections and options 101 | for section in schema.keys(): 102 | if section not in config.keys(): 103 | raise ConfigError("No header for section: [%s]" % section) 104 | 105 | for option in schema[section].keys(): 106 | if option not in config[section].keys(): 107 | raise ConfigError("Missing option \"%s\" in section \"[%s]\"" % (option, section)) 108 | 109 | valid, msg = _validate_option(schema[section][option], config[section][option]) 110 | 111 | if not valid: 112 | error_msg = "Invalid option \"%s\" in section \"[%s]\": %s" % (option, section, msg) 113 | 114 | if fallback_config is not None: 115 | logger.error(error_msg) 116 | logger.info("Falling back to default value: %s", fallback_config[section][option]) 117 | corrected_config[section][option] = fallback_config[section][option] 118 | 119 | else: 120 | raise ConfigError(error_msg) 121 | 122 | # Checking for unknown sections or options 123 | for section in config.keys(): 124 | if section not in schema.keys(): 125 | logger.warning("Ignoring unknown section: [%s]", section) 126 | continue 127 | 128 | for option in config[section].keys(): 129 | if option not in schema[section].keys(): 130 | logger.warning("Ignoring unknown option \"%s\" in section \"[%s]\"", option, section) 131 | del corrected_config[section][option] 132 | 133 | return corrected_config 134 | 135 | 136 | def _parsed_config_to_dict(config): 137 | config_dict = {} 138 | 139 | for section in config.keys(): 140 | if section == "DEFAULT": 141 | continue 142 | 143 | config_dict[section] = {} 144 | 145 | for option in config[section].keys(): 146 | config_dict[section][option] = config[section][option] 147 | 148 | return config_dict 149 | 150 | 151 | def _validate_option(schema_option_info, config_option_value): 152 | parameter_type = schema_option_info[0] 153 | assert parameter_type in ["multi_words", "single_word", "integer"] 154 | 155 | if parameter_type == "multi_words": 156 | valid, msg = _validate_multi_words(schema_option_info, config_option_value) 157 | 158 | elif parameter_type == "single_word": 159 | valid, msg = _validate_single_word(schema_option_info, config_option_value) 160 | 161 | elif parameter_type == "integer": 162 | valid, msg = _validate_integer(schema_option_info, config_option_value) 163 | 164 | return valid, msg 165 | 166 | 167 | def _validate_multi_words(schema_option_info, config_option_value): 168 | parameter_type, allowed_values, can_be_blank = schema_option_info 169 | assert parameter_type == "multi_words" 170 | values = config_option_value.replace(" ", "").split(",") 171 | 172 | if values == ['']: 173 | if not can_be_blank: 174 | msg = "At least one parameter required" 175 | return False, msg 176 | 177 | else: 178 | for value in values: 179 | if value not in allowed_values: 180 | msg = "Invalid value: %s" % value 181 | return False, msg 182 | 183 | return True, None 184 | 185 | 186 | def _validate_single_word(schema_option_info, config_option_value): 187 | parameter_type, allowed_values, can_be_blank = schema_option_info 188 | assert parameter_type == "single_word" 189 | val = config_option_value.replace(" ", "") 190 | 191 | if val == "": 192 | if not can_be_blank: 193 | msg = "Non-blank value required" 194 | return False, msg 195 | 196 | else: 197 | if val not in allowed_values: 198 | msg = "Invalid value: %s" % val 199 | return False, msg 200 | 201 | return True, None 202 | 203 | 204 | def _validate_integer(schema_option_info, config_option_value): 205 | parameter_type, can_be_blank = schema_option_info 206 | assert parameter_type == "integer" 207 | value = config_option_value.replace(" ", "") 208 | 209 | if value == "": 210 | if not can_be_blank: 211 | msg = "Value can't be blank" 212 | return False, msg 213 | 214 | else: 215 | try: 216 | v = int(value) 217 | if v < 0: 218 | raise ValueError 219 | except ValueError: 220 | msg = f"Positive integer required: {value}" 221 | return False, msg 222 | 223 | return True, None 224 | 225 | 226 | def load_extra_xorg_options(): 227 | logger = get_logger() 228 | xorg_extra = {} 229 | 230 | for mode, path_by_gpu in envs.EXTRA_XORG_OPTIONS_PATHS.items(): 231 | xorg_extra[mode] = {} 232 | 233 | for gpu, path in path_by_gpu.items(): 234 | try: 235 | config_lines = _load_extra_xorg_file(path) 236 | 237 | except FileNotFoundError: 238 | config_lines = [] 239 | 240 | if len(config_lines) > 0: 241 | logger.info("Loaded extra %s Xorg options: %d", mode, len(config_lines)) 242 | 243 | xorg_extra[mode][gpu] = config_lines 244 | 245 | return xorg_extra 246 | 247 | 248 | def _load_extra_xorg_file(path): 249 | with open(path, 'r') as filepath: 250 | config_lines = [] 251 | 252 | for line in filepath: 253 | line = line.strip() 254 | line_nospaces = line.replace(" ", "") 255 | 256 | if len(line_nospaces) == 0 or line_nospaces[0] == "#": 257 | continue 258 | 259 | config_lines.append(line) 260 | 261 | return config_lines 262 | -------------------------------------------------------------------------------- /optimus_manager/config_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "optimus": 3 | { 4 | "switching": ["single_word", ["nouveau", "bbswitch", "acpi_call", "custom", "none"], false], 5 | "pci_power_control": ["single_word", ["yes", "no"], false], 6 | "pci_remove": ["single_word", ["yes", "no"], false], 7 | "pci_reset": ["single_word", ["no", "function_level", "hot_reset"], false], 8 | "auto_logout": ["single_word", ["yes", "no"], false], 9 | "startup_mode": ["single_word", ["integrated", "hybrid", "nvidia", "auto", "auto_nvdisplay", "intel"], false], 10 | "startup_auto_battery_mode": ["single_word", ["integrated", "hybrid", "nvidia"], false], 11 | "startup_auto_extpower_mode": ["single_word", ["integrated", "hybrid", "nvidia"], false], 12 | "startup_auto_nvdisplay_off_mode": ["single_word", ["integrated", "hybrid", "nvidia"], false], 13 | "startup_auto_nvdisplay_on_mode": ["single_word", ["integrated", "hybrid", "nvidia"], false] 14 | }, 15 | 16 | "intel": 17 | { 18 | "driver": ["single_word", ["modesetting", "intel", "hybrid"], false], 19 | "accel": ["single_word", ["sna", "xna", "uxa", "none"], true], 20 | "tearfree": ["single_word", ["yes", "no"], true], 21 | "dri": ["single_word", ["0", "2", "3"], false], 22 | "modeset": ["single_word", ["yes", "no"], false] 23 | }, 24 | 25 | "amd": 26 | { 27 | "driver": ["single_word", ["modesetting", "amdgpu", "hybrid"], false], 28 | "tearfree": ["single_word", ["yes", "no"], true], 29 | "dri": ["single_word", ["0", "2", "3"], false] 30 | }, 31 | 32 | "nvidia": 33 | { 34 | "modeset": ["single_word", ["yes", "no"], false], 35 | "pat": ["single_word", ["yes", "no"], false], 36 | "dpi": ["integer", true], 37 | "ignore_abi": ["single_word", ["yes", "no"], false], 38 | "allow_external_gpus": ["single_word", ["yes", "no"], false], 39 | "options": ["multi_words", ["overclocking", "triple_buffer"], true], 40 | "dynamic_power_management": ["single_word", ["no", "coarse", "fine"], false], 41 | "dynamic_power_management_memory_threshold": ["integer", true] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /optimus_manager/daemon.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | import json 3 | import os 4 | import select 5 | import signal 6 | import socket 7 | import sys 8 | from . import envs 9 | from . import var 10 | from .log_utils import get_logger, set_logger_config 11 | 12 | 13 | def main(): 14 | daemon_run_id = var.load_daemon_run_id() 15 | no_id = False 16 | 17 | if daemon_run_id is None: 18 | no_id = True 19 | daemon_run_id = var.make_daemon_run_id() 20 | var.write_daemon_run_id(daemon_run_id) 21 | 22 | set_logger_config("daemon", daemon_run_id) 23 | logger = get_logger() 24 | 25 | try: 26 | logger.info("Starting daemon") 27 | 28 | if no_id: 29 | logger.warning( 30 | "Created a new daemon ID: The daemon pre-start hook failed to do so") 31 | 32 | server_socket = _open_server_socket(logger) 33 | _setup_signal_handler(logger, server_socket) 34 | logger.info("Ready to receive commands") 35 | 36 | while True: 37 | msg = _wait_for_command(server_socket) 38 | logger.info("Received command: %s", msg) 39 | _process_command(logger, msg) 40 | 41 | # pylint: disable=W0703 42 | except Exception: 43 | logger.exception("Daemon crashed") 44 | 45 | 46 | def _open_server_socket(logger): 47 | if os.path.exists(envs.SOCKET_PATH): 48 | logger.warning("Overwriting existing socket: %s" % envs.SOCKET_PATH) 49 | os.remove(envs.SOCKET_PATH) 50 | 51 | server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) 52 | server_socket.settimeout(envs.SOCKET_TIMEOUT) 53 | server_socket.bind(envs.SOCKET_PATH) 54 | os.chmod(envs.SOCKET_PATH, 0o666) 55 | return server_socket 56 | 57 | 58 | def _setup_signal_handler(logger, server_socket): 59 | handler = _SignalHandler(logger, server_socket) 60 | signal.signal(signal.SIGTERM, handler.handler) 61 | signal.signal(signal.SIGINT, handler.handler) 62 | 63 | 64 | def _wait_for_command(server_socket): 65 | select.select([server_socket], [], []) 66 | datagram = server_socket.recv(1024) 67 | msg = datagram.decode('utf-8') 68 | return msg 69 | 70 | 71 | def _process_command(logger, msg): 72 | try: 73 | command = json.loads(msg) 74 | except json.decoder.JSONDecodeError: 75 | logger.error("JSON decode failed: Invalid command: %s" % msg) 76 | return 77 | 78 | try: 79 | if command["type"] == "switch": 80 | mode = command["args"]["mode"] 81 | logger.info("Writing requested GPU mode: %s" % mode) 82 | state = var.load_state() 83 | 84 | if state is None: 85 | logger.error("Unable to switch: State file doesn't exist") 86 | return 87 | 88 | new_state = { 89 | "type": "pending_pre_xorg_start", 90 | "requested_mode": mode, 91 | "current_mode": state["current_mode"] 92 | } 93 | 94 | var.write_state(new_state) 95 | 96 | elif command["type"] == "temp_config": 97 | if command["args"]["path"] == "": 98 | logger.info("Removing temporary config file") 99 | var.remove_temp_conf_path_var() 100 | else: 101 | logger.info("Writing temporary config file: %s" % command["args"]["path"]) 102 | var.write_temp_conf_path_var(command["args"]["path"]) 103 | 104 | elif command["type"] == "user_config": 105 | _replace_user_config(logger, command["args"]["content"]) 106 | 107 | else: 108 | logger.error("Unkown command type: %s: %s" % (command["type"], msg)) 109 | 110 | except KeyError as error: 111 | logger.error("Invalid command key: %s: %s" % (str(error), msg)) 112 | 113 | 114 | def _replace_user_config(logger, config_content): 115 | logger.info("Replacing user config at: %s" % envs.USER_CONFIG_PATH) 116 | 117 | with open(envs.USER_CONFIG_PATH, "w") as f: 118 | f.write(config_content) 119 | 120 | 121 | class _SignalHandler: 122 | def __init__(self, logger, server_socket): 123 | self.logger = logger 124 | self.server_socket = server_socket 125 | 126 | def handler(self, signum, frame): 127 | #pylint: disable=unused-argument 128 | self.logger.info("Stopping daemon...") 129 | self.server_socket.close() 130 | os.remove(envs.SOCKET_PATH) 131 | self.logger.info("Stopped") 132 | sys.exit(0) 133 | 134 | 135 | if __name__ == '__main__': 136 | main() 137 | -------------------------------------------------------------------------------- /optimus_manager/envs.py: -------------------------------------------------------------------------------- 1 | VERSION = "0" 2 | 3 | LOG_DIR_PATH = "/var/log/optimus-manager" 4 | SOCKET_PATH = "/tmp/optimus-manager" 5 | SOCKET_TIMEOUT = 1.0 6 | 7 | MODULES_UNLOAD_WAIT_MAX_TRIES = 5 8 | MODULES_UNLOAD_WAIT_PERIOD = 1 9 | 10 | DEFAULT_CONFIG_PATH = "/usr/share/optimus-manager/optimus-manager.conf" 11 | USER_CONFIG_PATH = "/etc/optimus-manager/optimus-manager.conf" 12 | XORG_CONF_PATH = "/etc/X11/xorg.conf.d/10-optimus-manager.conf" 13 | 14 | NVIDIA_MANUAL_ENABLE_SCRIPT_PATH = "/etc/optimus-manager/nvidia-enable.sh" 15 | NVIDIA_MANUAL_DISABLE_SCRIPT_PATH = "/etc/optimus-manager/nvidia-disable.sh" 16 | 17 | PERSISTENT_VARS_FOLDER_PATH = "/var/lib/optimus-manager/persistent" 18 | ACPI_CALL_STRING_VAR_PATH = "%s/acpi_call_strings.json" % PERSISTENT_VARS_FOLDER_PATH 19 | TEMP_CONFIG_PATH_VAR_PATH = "%s/temp_conf_path" % PERSISTENT_VARS_FOLDER_PATH 20 | 21 | TMP_VARS_FOLDER_PATH = "/var/lib/optimus-manager/tmp" 22 | LAST_ACPI_CALL_STATE_VAR = "%s/last_acpi_call_state" % TMP_VARS_FOLDER_PATH 23 | STATE_FILE_PATH = "%s/state.json" % TMP_VARS_FOLDER_PATH 24 | USER_CONFIG_COPY_PATH = "%s/config_copy.conf" % TMP_VARS_FOLDER_PATH 25 | CURRENT_DAEMON_RUN_ID = "%s/daemon_run_id" % TMP_VARS_FOLDER_PATH 26 | 27 | 28 | EXTRA_XORG_OPTIONS_PATHS = { 29 | "hybrid-mode": { 30 | "integrated-gpu": "/etc/optimus-manager/xorg/hybrid-mode/integrated-gpu.conf", 31 | "nvidia-gpu" : "/etc/optimus-manager/xorg/hybrid-mode/nvidia-gpu.conf" 32 | }, 33 | "integrated-mode": { 34 | "integrated-gpu": "/etc/optimus-manager/xorg/integrated-mode/integrated-gpu.conf" 35 | }, 36 | "nvidia-mode": { 37 | "integrated-gpu": "/etc/optimus-manager/xorg/nvidia-mode/integrated-gpu.conf", 38 | "nvidia-gpu" : "/etc/optimus-manager/xorg/nvidia-mode/nvidia-gpu.conf" 39 | } 40 | } 41 | 42 | 43 | XSETUP_SCRIPTS_PATHS = { 44 | "hybrid": "/etc/optimus-manager/xsetup-hybrid.sh", 45 | "integrated": "/etc/optimus-manager/xsetup-integrated.sh", 46 | "nvidia": "/etc/optimus-manager/xsetup-nvidia.sh", 47 | "intel": "/etc/optimus-manager/xsetup-intel.sh" #DEPRECATED 48 | } 49 | -------------------------------------------------------------------------------- /optimus_manager/hooks/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /optimus_manager/hooks/post_daemon_stop.py: -------------------------------------------------------------------------------- 1 | from .. import var 2 | 3 | 4 | def main(): 5 | var.cleanup_tmp_vars() 6 | 7 | 8 | if __name__ == "__main__": 9 | main() 10 | -------------------------------------------------------------------------------- /optimus_manager/hooks/post_resume.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .. import var 3 | from ..config import load_config 4 | from ..kernel import get_available_modules, nvidia_power_down 5 | from ..log_utils import get_logger, set_logger_config 6 | 7 | 8 | def main(): 9 | prev_state = var.load_state() 10 | 11 | if prev_state is None or prev_state["type"] != "pending_post_resume": 12 | return 13 | 14 | switch_id = prev_state["switch_id"] 15 | set_logger_config("switch", switch_id) 16 | logger = get_logger() 17 | current_mode = prev_state["current_mode"] 18 | 19 | try: 20 | logger.info("Running post-resume hook") 21 | logger.info("Previous state was: %s", str(prev_state)) 22 | config = load_config() 23 | switching_option = config["optimus"]["switching"] 24 | 25 | if current_mode == "integrated": 26 | logger.info("Turning Nvidia GPU off again") 27 | available_modules = get_available_modules() 28 | nvidia_power_down(config, available_modules) 29 | 30 | state = { 31 | "type": "done", 32 | "switch_id": switch_id, 33 | "current_mode": current_mode, 34 | } 35 | 36 | var.write_state(state) 37 | 38 | # pylint: disable=W0703 39 | except Exception: 40 | logger.exception("Post-resume hook failed") 41 | 42 | state = { 43 | "type": "post_resume_failed", 44 | "switch_id": switch_id, 45 | "current_mode": current_mode 46 | } 47 | 48 | var.write_state(state) 49 | sys.exit(1) 50 | 51 | else: 52 | logger.info("Post-resume hook completed") 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /optimus_manager/hooks/post_xorg_start.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .. import var 3 | from ..config import load_config 4 | from ..log_utils import get_logger, set_logger_config 5 | from ..xorg import do_xsetup, set_DPI 6 | 7 | 8 | def main(): 9 | prev_state = var.load_state() 10 | 11 | if prev_state is None or prev_state["type"] != "pending_post_xorg_start": 12 | return 13 | 14 | switch_id = prev_state["switch_id"] 15 | set_logger_config("switch", switch_id) 16 | logger = get_logger() 17 | requested_mode = None 18 | 19 | try: 20 | logger.info("Running Xorg post-start hook") 21 | requested_mode = prev_state["requested_mode"] 22 | do_xsetup(requested_mode) 23 | config = load_config() 24 | set_DPI(config) 25 | 26 | state = { 27 | "type": "done", 28 | "switch_id": switch_id, 29 | "current_mode": requested_mode 30 | } 31 | 32 | var.write_state(state) 33 | 34 | # pylint: disable=W0703 35 | except Exception: 36 | logger.exception("Xorg post-start hook failed") 37 | 38 | state = { 39 | "type": "post_xorg_start_failed", 40 | "switch_id": switch_id, 41 | "requested_mode": requested_mode 42 | } 43 | 44 | var.write_state(state) 45 | sys.exit(1) 46 | 47 | else: 48 | logger.info("Xorg post-start hook completed") 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /optimus_manager/hooks/pre_daemon_start.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .. import var 3 | from ..checks import is_ac_power_connected, is_nvidia_display_connected 4 | from ..config import copy_user_config, load_config 5 | from ..kernel_parameters import get_kernel_parameters 6 | from ..log_utils import get_logger, set_logger_config 7 | from ..xorg import cleanup_xorg_conf 8 | 9 | 10 | def main(): 11 | var.cleanup_tmp_vars() 12 | daemon_run_id = var.make_daemon_run_id() 13 | var.write_daemon_run_id(daemon_run_id) 14 | set_logger_config("daemon", daemon_run_id) 15 | logger = get_logger() 16 | startup_mode = None 17 | 18 | try: 19 | logger.info("Running daemon pre-start hook") 20 | cleanup_xorg_conf() 21 | copy_user_config() 22 | config = load_config() 23 | kernel_parameters = get_kernel_parameters() 24 | 25 | if kernel_parameters["startup_mode"] is not None: 26 | startup_mode = kernel_parameters["startup_mode"] 27 | 28 | else: 29 | startup_mode = config["optimus"]["startup_mode"] 30 | 31 | logger.info("Startup mode is: %s", startup_mode) 32 | 33 | if startup_mode == "auto": 34 | if is_ac_power_connected(): 35 | eff_startup_mode = config["optimus"]["startup_auto_extpower_mode"] 36 | 37 | else: 38 | eff_startup_mode = config["optimus"]["startup_auto_battery_mode"] 39 | 40 | logger.info("Effective startup mode is: %s", eff_startup_mode) 41 | 42 | elif startup_mode == "auto_nvdisplay": 43 | if is_nvidia_display_connected(): 44 | eff_startup_mode = config["optimus"]["startup_auto_nvdisplay_on_mode"] 45 | 46 | else: 47 | eff_startup_mode = config["optimus"]["startup_auto_nvdisplay_off_mode"] 48 | 49 | logger.info("Effective startup mode is: %s", eff_startup_mode) 50 | 51 | else: 52 | eff_startup_mode = startup_mode 53 | 54 | state = { 55 | "type": "pending_pre_xorg_start", 56 | "requested_mode": eff_startup_mode, 57 | "current_mode": None 58 | } 59 | 60 | var.write_state(state) 61 | 62 | # pylint: disable=W0703 63 | except Exception: 64 | logger.exception("Daemon start hook failed") 65 | 66 | state = { 67 | "type": "startup_failed", 68 | "startup_mode": startup_mode, 69 | "daemon_run_id": daemon_run_id 70 | } 71 | 72 | var.write_state(state) 73 | sys.exit(1) 74 | 75 | else: 76 | logger.info("Daemon pre-start hook completed") 77 | 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /optimus_manager/hooks/pre_suspend.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .. import var 3 | from ..config import load_config 4 | from ..kernel import get_available_modules, nvidia_power_up 5 | from ..log_utils import get_logger, set_logger_config 6 | 7 | 8 | def main(): 9 | prev_state = var.load_state() 10 | 11 | if prev_state is None or prev_state["type"] != "done": 12 | return 13 | 14 | switch_id = prev_state["switch_id"] 15 | set_logger_config("switch", switch_id) 16 | logger = get_logger() 17 | current_mode = prev_state["current_mode"] 18 | 19 | try: 20 | logger.info("Running pre-suspend hook") 21 | logger.info("Previous state was: %s", str(prev_state)) 22 | config = load_config() 23 | switching_option = config["optimus"]["switching"] 24 | logger.info("Switching option: %s", switching_option) 25 | 26 | if current_mode == "integrated": 27 | logger.info("Turning Nvidia GPU back on") 28 | available_modules = get_available_modules() 29 | nvidia_power_up(config, available_modules) 30 | 31 | state = { 32 | "type": "pending_post_resume", 33 | "switch_id": switch_id, 34 | "current_mode": current_mode 35 | } 36 | 37 | var.write_state(state) 38 | 39 | # pylint: disable=W0703 40 | except Exception: 41 | logger.exception("Pre-suspend hook failed") 42 | 43 | state = { 44 | "type": "pre_suspend_failed", 45 | "switch_id": switch_id, 46 | "current_mode": current_mode 47 | } 48 | 49 | var.write_state(state) 50 | sys.exit(1) 51 | 52 | else: 53 | logger.info("Pre-suspend hook completed") 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | -------------------------------------------------------------------------------- /optimus_manager/hooks/pre_xorg_start.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from .. import var 4 | from ..config import load_config 5 | from ..kernel import setup_kernel_state 6 | from ..log_utils import get_logger, set_logger_config 7 | from ..xorg import cleanup_xorg_conf, configure_xorg, is_xorg_running 8 | 9 | 10 | def main(): 11 | prev_state = var.load_state() 12 | 13 | if prev_state is None: 14 | return 15 | 16 | elif prev_state["type"] == "pending_pre_xorg_start": 17 | switch_id = var.make_switch_id() 18 | setup_kernel = True 19 | requested_mode = prev_state["requested_mode"] 20 | 21 | elif prev_state["type"] == "done": 22 | switch_id = prev_state["switch_id"] 23 | setup_kernel = False 24 | requested_mode = prev_state["current_mode"] 25 | 26 | else: 27 | return 28 | 29 | set_logger_config("switch", switch_id) 30 | logger = get_logger() 31 | 32 | try: 33 | if os.environ.get("RUNNING_UNDER_GDM", False) and is_xorg_running(): 34 | sys.exit(0) 35 | 36 | logger.info("Running Xorg pre-start hook") 37 | logger.info("Previous state was: %s", str(prev_state)) 38 | logger.info("Requested mode is: %s", requested_mode) 39 | config = load_config() 40 | 41 | if setup_kernel: 42 | setup_kernel_state(config, prev_state, requested_mode) 43 | 44 | configure_xorg(config, requested_mode) 45 | 46 | state = { 47 | "type": "pending_post_xorg_start", 48 | "switch_id": switch_id, 49 | "requested_mode": requested_mode, 50 | } 51 | 52 | var.write_state(state) 53 | 54 | # pylint: disable=W0703 55 | except Exception: 56 | logger.exception("Xorg pre-start hook failed") 57 | cleanup_xorg_conf() 58 | 59 | state = { 60 | "type": "pre_xorg_start_failed", 61 | "switch_id": switch_id, 62 | "requested_mode": requested_mode 63 | } 64 | 65 | var.write_state(state) 66 | sys.exit(1) 67 | 68 | else: 69 | logger.info("Xorg pre-start hook completed") 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /optimus_manager/kernel.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import time 3 | from . import checks 4 | from . import envs 5 | from . import pci 6 | from . import var 7 | from .acpi_data import ACPI_STRINGS 8 | from .log_utils import get_logger 9 | 10 | 11 | class KernelSetupError(Exception): 12 | pass 13 | 14 | 15 | def setup_kernel_state(config, prev_state, requested_mode): 16 | assert requested_mode in ["hybrid", "integrated", "nvidia"] 17 | assert prev_state["type"] == "pending_pre_xorg_start" 18 | current_mode = prev_state["current_mode"] 19 | 20 | if current_mode in ["integrated", None] and requested_mode in ["nvidia", "hybrid"]: 21 | _nvidia_up(config, hybrid=(requested_mode == "hybrid")) 22 | 23 | elif current_mode in ["nvidia", "hybrid", None] and requested_mode == "integrated": 24 | _nvidia_down(config) 25 | 26 | 27 | def get_available_modules(): 28 | MODULES = [ 29 | "nouveau", "bbswitch", "acpi_call", "nvidia", 30 | "nvidia_drm", "nvidia_modeset", "nvidia_uvm" 31 | ] 32 | 33 | return [module for module in MODULES if checks.is_module_available(module)] 34 | 35 | 36 | def nvidia_power_up(config, available_modules): 37 | logger = get_logger() 38 | switching_mode = config["optimus"]["switching"] 39 | 40 | if switching_mode == "bbswitch": 41 | _try_load_bbswitch(available_modules) 42 | _try_set_bbswitch_state("ON") 43 | 44 | elif switching_mode == "acpi_call": 45 | _try_load_acpi_call(available_modules) 46 | _try_set_acpi_call_state("ON") 47 | 48 | elif switching_mode == "custom": 49 | _try_custom_set_power_state("ON") 50 | 51 | else: 52 | logger.info("Skipping Nvidia power up: switching_mode=%s", switching_mode) 53 | 54 | 55 | def nvidia_power_down(config, available_modules): 56 | logger = get_logger() 57 | switching_mode = config["optimus"]["switching"] 58 | 59 | if switching_mode == "bbswitch": 60 | _try_load_bbswitch(available_modules) 61 | _set_bbswitch_state("OFF") 62 | 63 | elif switching_mode == "acpi_call": 64 | _try_load_acpi_call(available_modules) 65 | _try_set_acpi_call_state("OFF") 66 | 67 | elif switching_mode == "custom": 68 | _try_custom_set_power_state("OFF") 69 | 70 | else: 71 | logger.info("Skipping Nvidia power down: switching_mode=%s", switching_mode) 72 | 73 | 74 | def _nvidia_up(config, hybrid): 75 | logger = get_logger() 76 | available_modules = get_available_modules() 77 | logger.info("Available modules: %s", str(available_modules)) 78 | _unload_nouveau(available_modules) 79 | nvidia_power_up(config, available_modules) 80 | 81 | if not pci.is_nvidia_visible(): 82 | logger.info("Nvidia card not visible in PCI bus: Rescanning...") 83 | _try_rescan_pci() 84 | 85 | if config["optimus"]["pci_reset"] != "no": 86 | _try_pci_reset(config, available_modules) 87 | 88 | power_control = ( 89 | config["optimus"]["pci_power_control"] == "yes" or \ 90 | config["nvidia"]["dynamic_power_management"] != "no" 91 | ) 92 | 93 | if power_control: 94 | _try_set_pci_power_state("auto" if hybrid else "on") 95 | 96 | _load_nvidia_modules(config, available_modules) 97 | 98 | 99 | def _nvidia_down(config): 100 | logger = get_logger() 101 | available_modules = get_available_modules() 102 | logger.info("Available modules: %s", str(available_modules)) 103 | _unload_nvidia_modules(available_modules) 104 | nvidia_power_down(config, available_modules) 105 | switching_mode = config["optimus"]["switching"] 106 | 107 | if switching_mode == "nouveau": 108 | _try_load_nouveau(config, available_modules) 109 | 110 | if config["optimus"]["pci_remove"] == "yes": 111 | if switching_mode == "nouveau" or switching_mode == "bbswitch": 112 | logger.warning("Option ignored: pci_remove: due to switching_mode=%s", switching_mode) 113 | 114 | else: 115 | logger.info("Removing Nvidia from PCI bus") 116 | _try_remove_pci() 117 | 118 | power_control = ( 119 | config["optimus"]["pci_power_control"] == "yes" or \ 120 | config["nvidia"]["dynamic_power_management"] != "no" 121 | ) 122 | 123 | if power_control: 124 | switching_mode = config["optimus"]["switching"] 125 | 126 | if switching_mode == "bbswitch" or switching_mode == "acpi_call": 127 | logger.warning("Option ignored: pci_power_control: due to switching_mode=%s", switching_mode) 128 | 129 | elif config["optimus"]["pci_remove"] == "yes": 130 | logger.warning("Option ignored: pci_power_control: due to pci_remove=enabled") 131 | 132 | else: 133 | _try_set_pci_power_state("auto") 134 | 135 | 136 | def _load_nvidia_modules(config, available_modules): 137 | logger = get_logger() 138 | nvidia_options = [] 139 | 140 | ### nvidia module 141 | 142 | if config["nvidia"]["pat"] == "yes" and checks.is_pat_available(): 143 | nvidia_options.append("NVreg_UsePageAttributeTable=1") 144 | 145 | if config["nvidia"]["dynamic_power_management"] == "coarse": 146 | nvidia_options.append("NVreg_DynamicPowerManagement=0x01") 147 | 148 | elif config["nvidia"]["dynamic_power_management"] == "fine": 149 | nvidia_options.append("NVreg_DynamicPowerManagement=0x02") 150 | 151 | mem_th = config["nvidia"]["dynamic_power_management_memory_threshold"] 152 | 153 | if mem_th != "": 154 | nvidia_options.append(f"NVreg_DynamicPowerManagementVideoMemoryThreshold={mem_th}") 155 | 156 | _load_module(available_modules, "nvidia", options=nvidia_options) 157 | 158 | ### nvidia_drm module 159 | 160 | nvidia_drm_options = [] 161 | 162 | if config["nvidia"]["modeset"] == "yes": 163 | nvidia_drm_options.append("modeset=1") 164 | 165 | _load_module(available_modules, "nvidia_drm", options=nvidia_drm_options) 166 | 167 | 168 | def _load_nouveau(config, available_modules): 169 | # TODO: Move the option to [optimus] 170 | nouveau_options = ["modeset=1"] if config["intel"]["modeset"] == "yes" else [] 171 | _load_module(available_modules, "nouveau", options=nouveau_options) 172 | 173 | 174 | def _try_load_nouveau(config, available_modules): 175 | logger = get_logger() 176 | 177 | try: 178 | _load_nouveau(config, available_modules) 179 | 180 | except KernelSetupError as error: 181 | logger.error( 182 | "Unable to load nouveau: %s", str(error)) 183 | 184 | 185 | def _try_load_bbswitch(available_modules): 186 | logger = get_logger() 187 | 188 | try: 189 | _load_module(available_modules, "bbswitch") 190 | 191 | except KernelSetupError as error: 192 | logger.error( 193 | "Unable to load bbswitch: %s", str(error)) 194 | 195 | 196 | def _try_load_acpi_call(available_modules): 197 | logger = get_logger() 198 | 199 | try: 200 | _load_module(available_modules, "acpi_call") 201 | 202 | except KernelSetupError as error: 203 | logger.error( 204 | "Unable to load acpi_call: %s", str(error)) 205 | 206 | 207 | def _unload_nvidia_modules(available_modules): 208 | _unload_modules(available_modules, ["nvidia_drm", "nvidia_modeset", "nvidia_uvm", "nvidia"]) 209 | 210 | 211 | def _unload_nouveau(available_modules): 212 | _unload_modules(available_modules, ["nouveau"]) 213 | 214 | 215 | def _try_unload_bbswitch(available_modules): 216 | logger = get_logger() 217 | 218 | try: 219 | _unload_modules(available_modules, ["bbswitch"]) 220 | 221 | except KernelSetupError as error: 222 | logger.error( 223 | "Unable to unload bbswitch: %s", str(error)) 224 | 225 | 226 | def _unload_bbswitch(available_modules): 227 | _unload_modules(available_modules, ["bbswitch"]) 228 | 229 | 230 | def _load_module(available_modules, module, options=None): 231 | logger = get_logger() 232 | options = options or [] 233 | logger.info("Loading module: %s", module) 234 | 235 | if module not in available_modules: 236 | raise KernelSetupError( 237 | "Module not installed properly: %s" % module) 238 | 239 | try: 240 | subprocess.check_call( 241 | f"modprobe {module} {' '.join(options)}", 242 | shell=True, text=True, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL) 243 | 244 | except subprocess.CalledProcessError as error: 245 | raise KernelSetupError(f"Failed to modprobe {module}: {error.stderr}") from error 246 | 247 | 248 | def _unload_modules(available_modules, modules_list): 249 | logger = get_logger() 250 | modules_to_unload = [module for module in modules_list if module in available_modules] 251 | 252 | if len(modules_to_unload) == 0: 253 | return 254 | 255 | logger.info("Unloading modules: %s", str(modules_to_unload)) 256 | counter = 0 257 | success = False 258 | 259 | while not success and counter < envs.MODULES_UNLOAD_WAIT_MAX_TRIES: 260 | counter += 1 261 | 262 | try: 263 | subprocess.check_call( 264 | f"modprobe -r {' '.join(modules_to_unload)}", 265 | shell=True, text=True, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL) 266 | success = True 267 | 268 | except subprocess.CalledProcessError as error: 269 | if counter >= envs.MODULES_UNLOAD_WAIT_MAX_TRIES: 270 | logger.info(f"Max tries exceeded: {counter}") 271 | raise KernelSetupError(f"Failed to unload modules: {modules_to_unload}: {error.stderr}") from error 272 | 273 | else: 274 | logger.info(f"Failed to unload modules: {error.stderr}") 275 | logger.info(f"Waiting {envs.MODULES_UNLOAD_WAIT_PERIOD}s and retrying...") 276 | time.sleep(envs.MODULES_UNLOAD_WAIT_PERIOD) 277 | 278 | 279 | def _set_bbswitch_state(state): 280 | logger = get_logger() 281 | assert state in ["OFF", "ON"] 282 | logger.info("Setting power via bbswitch to: %s", state) 283 | 284 | try: 285 | with open("/proc/acpi/bbswitch", "w") as bbfile: 286 | bbfile.write(state) 287 | 288 | except FileNotFoundError as error: 289 | raise KernelSetupError("Unable to open: /proc/acpi/bbswitch") from error 290 | 291 | except IOError as error: 292 | raise KernelSetupError("Unable to write to: /proc/acpi/bbswitch") from error 293 | 294 | 295 | def _set_acpi_call_state(state): 296 | logger = get_logger() 297 | assert state in ["OFF", "ON"] 298 | logger.info("Setting power via acpi_call to: %s", state) 299 | 300 | try: 301 | acpi_strings_list = var.read_acpi_call_strings() 302 | 303 | except var.VarError: 304 | acpi_strings_list = ACPI_STRINGS 305 | logger.info("Trying all ACPI calls (may polute the kernel log)") 306 | 307 | working_strings = [] 308 | 309 | for off_str, on_str in acpi_strings_list: 310 | if not working_strings: 311 | string = off_str if state == "OFF" else on_str 312 | 313 | try: 314 | logger.info("Sending ACPI call: %s", string) 315 | 316 | with open("/proc/acpi/call", "w") as callwrite: 317 | callwrite.write(string) 318 | with open("/proc/acpi/call", "r") as callread: 319 | output = callread.read() 320 | 321 | except FileNotFoundError as error: 322 | raise KernelSetupError("Unable to open: /proc/acpi/call") from error 323 | 324 | except IOError: 325 | continue 326 | 327 | if "Error" not in output: 328 | logger.info("Saving working ACPI call: %s", string) 329 | working_strings.append((off_str, on_str)) 330 | 331 | var.write_last_acpi_call_state(state) 332 | var.write_acpi_call_strings(working_strings) 333 | 334 | 335 | def _try_remove_pci(): 336 | logger = get_logger() 337 | 338 | try: 339 | pci.remove_nvidia() 340 | 341 | except pci.PCIError as error: 342 | logger.error( 343 | "Unable to remove Nvidia from PCI bus: %s", str(error)) 344 | 345 | 346 | def _try_rescan_pci(): 347 | logger = get_logger() 348 | 349 | try: 350 | pci.rescan() 351 | 352 | if not pci.is_nvidia_visible(): 353 | logger.error("Nvidia card not showing up in PCI bus after rescan") 354 | 355 | except pci.PCIError as error: 356 | logger.error("Unable to rescan PCI bus: %s", str(error)) 357 | 358 | 359 | def _try_set_pci_power_state(state): 360 | logger = get_logger() 361 | logger.info("Setting Nvidia PCI power state to: %s", state) 362 | 363 | try: 364 | pci.set_power_state(state) 365 | 366 | except pci.PCIError as error: 367 | logger.error( 368 | "Unable to set PCI power state: %s", str(error)) 369 | 370 | 371 | def _try_pci_reset(config, available_modules): 372 | logger = get_logger() 373 | logger.info("Resetting Nvidia PCI device") 374 | 375 | try: 376 | _pci_reset(config, available_modules) 377 | 378 | except KernelSetupError as error: 379 | logger.error( 380 | "Unable to reset Nvidia PCI device: %s", str(error)) 381 | 382 | 383 | def _try_set_acpi_call_state(state): 384 | logger = get_logger() 385 | 386 | try: 387 | _set_acpi_call_state(state) 388 | 389 | except KernelSetupError as error: 390 | logger.error( 391 | "Unable to set acpi_call_sate to %s: %s", state, str(error)) 392 | 393 | 394 | def _try_set_bbswitch_state(state): 395 | logger = get_logger() 396 | 397 | try: 398 | _set_bbswitch_state(state) 399 | 400 | except KernelSetupError as error: 401 | logger.error( 402 | "Unable to set bbswitch_state to %s: %s", state, str(error)) 403 | 404 | 405 | def _pci_reset(config, available_modules): 406 | logger = get_logger() 407 | _unload_bbswitch(available_modules) 408 | 409 | try: 410 | if config["optimus"]["pci_reset"] == "function_level": 411 | logger.info("Performing function-level reset of Nvidia") 412 | pci.function_level_reset_nvidia() 413 | 414 | elif config["optimus"]["pci_reset"] == "hot_reset": 415 | logger.info("Starting hot reset of Nvidia") 416 | pci.hot_reset_nvidia() 417 | 418 | except pci.PCIError as error: 419 | raise KernelSetupError(f"Unable to perform PCI reset: {error}") from error 420 | 421 | 422 | def _try_custom_set_power_state(state): 423 | logger = get_logger() 424 | 425 | if state == "ON": 426 | script_path = envs.NVIDIA_MANUAL_ENABLE_SCRIPT_PATH 427 | 428 | elif state == "OFF": 429 | script_path = envs.NVIDIA_MANUAL_DISABLE_SCRIPT_PATH 430 | 431 | logger.info("Running custom power switching script: %s", script_path) 432 | 433 | try: 434 | subprocess.check_call( 435 | script_path, text=True, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL) 436 | 437 | except subprocess.CalledProcessError as error: 438 | logger.error(f"Unable to run power switching script: {error.stderr}") 439 | -------------------------------------------------------------------------------- /optimus_manager/kernel_parameters.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .log_utils import get_logger 3 | 4 | def get_kernel_parameters(): 5 | logger = get_logger() 6 | 7 | with open("/proc/cmdline", "r") as cmdfile: 8 | cmdline = cmdfile.read() 9 | 10 | index = 0 11 | parameters = cmdline.split() 12 | startup_mode = None 13 | 14 | while startup_mode is None and index < len(parameters): 15 | if re.fullmatch("optimus-manager\\.startup=[^ ]+", parameters[index]): 16 | logger.info("Kernel parameter: %s", parameters[index]) 17 | potential_mode = parameters[index].split("=")[-1] 18 | 19 | if potential_mode in ["auto", "hybrid", "integrated", "nvidia"]: 20 | startup_mode = potential_mode 21 | 22 | else: 23 | logger.error( 24 | "Ignored invalid startup mode in kernel parameter: %s", potential_mode) 25 | 26 | index += 1 27 | 28 | return {"startup_mode": startup_mode} 29 | -------------------------------------------------------------------------------- /optimus_manager/log_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from pathlib import Path 5 | from . import envs 6 | 7 | 8 | def set_logger_config(log_type, log_id): 9 | log_dir_path = Path(envs.LOG_DIR_PATH) 10 | log_filepath = log_dir_path / log_type / ("%s-%s.log" % (log_type, log_id)) 11 | 12 | if not log_filepath.parent.exists(): 13 | os.makedirs(log_filepath.parent) 14 | os.chmod(log_filepath.parent, 0o777) 15 | 16 | logging.basicConfig( 17 | level=logging.INFO, 18 | format="[%(relativeCreated)d] %(levelname)s: %(message)s", 19 | handlers=[ 20 | logging.StreamHandler(stream=sys.stdout), 21 | logging.FileHandler(filename=log_filepath) 22 | ] 23 | ) 24 | 25 | try: 26 | os.chmod(log_filepath, 0o777) 27 | 28 | except PermissionError: 29 | pass 30 | 31 | 32 | def get_logger(): 33 | return logging.getLogger() 34 | -------------------------------------------------------------------------------- /optimus_manager/pci.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | from pathlib import Path 5 | from .log_utils import get_logger 6 | 7 | VENDOR_IDS = { 8 | "nvidia": "10de", 9 | "intel": "8086", 10 | "amd": "1002" 11 | } 12 | 13 | GPU_PCI_CLASS_PATTERN = "03[0-9a-f]{2}" 14 | PCI_BRIDGE_PCI_CLASS_PATTERN = "0604" 15 | 16 | 17 | class PCIError(Exception): 18 | pass 19 | 20 | 21 | def set_power_state(mode): 22 | _write_to_nvidia_path("power/control", mode) 23 | 24 | 25 | def function_level_reset_nvidia(): 26 | _write_to_nvidia_path("reset", "1") 27 | 28 | 29 | def hot_reset_nvidia(): 30 | logger = get_logger() 31 | bus_ids = get_gpus_bus_ids(notation_fix=False) 32 | 33 | if "nvidia" not in bus_ids.keys(): 34 | raise PCIError("Nvidia isn't in the PCI bus") 35 | 36 | nvidia_pci_bridges_ids_list = _get_connected_pci_bridges(bus_ids["nvidia"]) 37 | 38 | if len(nvidia_pci_bridges_ids_list) == 0: 39 | raise PCIError("Unable to PCI hot reset: Unable to find the PCI bridge connected to the Nvidia card") 40 | 41 | if len(nvidia_pci_bridges_ids_list) > 1: 42 | raise PCIError("Unable to PCI hot reset: Found more than one PCI bridge connected to the Nvidia card") 43 | 44 | nvidia_pci_bridge = nvidia_pci_bridges_ids_list[0] 45 | logger.info("Removing Nvidia from PCI bridge") 46 | remove_nvidia() 47 | logger.info("Triggering PCI hot reset of bridge: %s", nvidia_pci_bridge) 48 | 49 | try: 50 | subprocess.check_call( 51 | f"setpci -s {nvidia_pci_bridge} 0x488.l=0x2000000:0x2000000", 52 | shell=True, text=True, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL) 53 | 54 | except subprocess.CalledProcessError as error: 55 | raise PCIError(f"Failed to run setpci: {e.stderr}") from error 56 | 57 | logger.info("Rescanning PCI bus") 58 | rescan() 59 | 60 | if not is_nvidia_visible(): 61 | raise PCIError("Failed to bring the Nvidia card back") 62 | 63 | 64 | def remove_nvidia(): 65 | _write_to_nvidia_path("remove", "1") 66 | 67 | 68 | def is_nvidia_visible(): 69 | bus_ids = get_gpus_bus_ids(notation_fix=False) 70 | 71 | if "nvidia" not in bus_ids.keys(): 72 | return False 73 | 74 | pci_path = "/sys/bus/pci/devices/0000:%s/" % bus_ids["nvidia"] 75 | return os.path.isdir(pci_path) 76 | 77 | 78 | def rescan(): 79 | _write_to_pci_path("/sys/bus/pci/rescan", "1") 80 | 81 | 82 | def get_gpus_bus_ids(notation_fix=True): 83 | logger = get_logger() 84 | bus_ids = {} 85 | 86 | for manufacturer, vendor_id in VENDOR_IDS.items(): 87 | ids_list = _search_bus_ids( 88 | match_pci_class=GPU_PCI_CLASS_PATTERN, 89 | match_vendor_id=vendor_id, 90 | notation_fix=notation_fix) 91 | 92 | if len(ids_list) > 1: 93 | logger.warning(f"Multiple {manufacturer} GPUs found: Picking the last one") 94 | 95 | if len(ids_list) > 0: 96 | bus_ids[manufacturer] = ids_list[-1] 97 | 98 | if "intel" in bus_ids and "amd" in bus_ids: 99 | logger.warning("Found both an Intel and an AMD GPU: Picking the Intel one") 100 | del bus_ids["amd"] 101 | 102 | elif not ("intel" in bus_ids or "amd" in bus_ids): 103 | logger.warning("No integrated GPU on: Using nvidia mode") 104 | 105 | return bus_ids 106 | 107 | 108 | def _search_bus_ids(match_pci_class, match_vendor_id, notation_fix=True): 109 | try: 110 | out = subprocess.check_output( 111 | "lspci -n", shell=True, text=True, stderr=subprocess.PIPE).strip() 112 | 113 | except subprocess.CalledProcessError as error: 114 | raise PCIError(f"Unable to run lspci -n: {error.stderr}") from error 115 | 116 | bus_ids_list = [] 117 | 118 | for line in out.splitlines(): 119 | items = line.split(" ") 120 | bus_id = items[0] 121 | 122 | if notation_fix: 123 | # Example: `3c:00:0` (kernel hexadecimal) -> `PCI:60:0:0` (xorg decimal) 124 | 125 | # If there are devices with different number 126 | # `lspci -n` can sometimes output domain number 127 | 128 | bus_id = "PCI:" + ":".join( 129 | str(int(field, 16)) for field in re.split("[.:]", bus_id)[-3:] 130 | ) 131 | 132 | pci_class = items[1][:-1] 133 | vendor_id, _ = items[2].split(":") 134 | 135 | if re.fullmatch(match_pci_class, pci_class) and re.fullmatch(match_vendor_id, vendor_id): 136 | bus_ids_list.append(bus_id) 137 | 138 | return bus_ids_list 139 | 140 | 141 | def _write_to_nvidia_path(relative_path, string): 142 | logger = get_logger() 143 | bus_ids = get_gpus_bus_ids(notation_fix=False) 144 | 145 | if "nvidia" not in bus_ids.keys(): 146 | raise PCIError("Nvidia isn't in the PCI bus") 147 | 148 | nvidia_id = bus_ids["nvidia"] 149 | res = re.fullmatch(r"([0-9]{2}:[0-9]{2})\.[0-9]", nvidia_id) 150 | 151 | if res is None: 152 | raise PCIError(f"Unexpected PCI ID format: {nvidia_id}") 153 | 154 | partial_id = res.groups()[0] 155 | # Bus ID minus the PCI function number 156 | 157 | # Applied to all PCI functions of the Nvidia card 158 | # in case they have an audio chipset or a Thunderbolt controller 159 | for device_path in Path("/sys/bus/pci/devices/").iterdir(): 160 | device_id = device_path.name 161 | 162 | if re.fullmatch(f"0000:{partial_id}\\.([0-9])", device_id): 163 | write_path = device_path / relative_path 164 | logger.info(f"Writing \"{string}\" to: {write_path}") 165 | _write_to_pci_path(write_path, string) 166 | 167 | 168 | def _write_to_pci_path(pci_path, string): 169 | try: 170 | with open(pci_path, "w") as pcifile: 171 | pcifile.write(string) 172 | 173 | except FileNotFoundError as error: 174 | raise PCIError(f"Unable to find PCI path: {pci_path}") from error 175 | 176 | except IOError as error: 177 | raise PCIError(f"Unable to write to PCI path: {pci_path}: {str(error)}") from error 178 | 179 | 180 | def _read_pci_path(pci_path): 181 | try: 182 | with open(pci_path, "r") as pcifile: 183 | string = pcifile.read() 184 | 185 | except FileNotFoundError as error: 186 | raise PCIError("Unable to find PCI path: %s" % pci_path) from error 187 | 188 | except IOError as error: 189 | raise PCIError("Unable to read from PCI path: %s" % pci_path) from error 190 | 191 | return string 192 | 193 | 194 | def _get_connected_pci_bridges(pci_id): 195 | bridges = _search_bus_ids( 196 | match_pci_class=PCI_BRIDGE_PCI_CLASS_PATTERN, 197 | match_vendor_id=".+", 198 | notation_fix=False) 199 | 200 | connected_bridges = [] 201 | 202 | for bridge in bridges: 203 | path = "/sys/bus/pci/devices/0000:%s/" % bridge 204 | directories = os.listdir(path) 205 | index = 0 206 | 207 | while not connected_bridges and index < len(directories): 208 | dir_path = os.path.join(path, directories[index]) 209 | 210 | if os.path.isdir(dir_path) and directories[index] == "0000:%s" % pci_id: 211 | connected_bridges.append(bridge) 212 | 213 | index += 1 214 | 215 | return connected_bridges 216 | -------------------------------------------------------------------------------- /optimus_manager/processes.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from .log_utils import get_logger 3 | 4 | 5 | class ProcessesError(Exception): 6 | pass 7 | 8 | 9 | def get_PIDs_from_process_names(processes_names_list): 10 | logger = get_logger() 11 | PIDs_list = [] 12 | 13 | for p_name in processes_names_list: 14 | try: 15 | process_PIDs_str = subprocess.check_output( 16 | f"pidof {p_name}", shell=True, text=True).strip() 17 | 18 | except subprocess.CalledProcessError: 19 | continue 20 | 21 | try: 22 | process_PIDs_list = [ 23 | int(pid_str) 24 | for pid_str in process_PIDs_str.split(" ") 25 | ] 26 | 27 | except ValueError: 28 | logger.warning(f"Invalid process value for {p_name}: {process_PIDs_str}") 29 | continue 30 | 31 | PIDs_list += process_PIDs_list 32 | 33 | return PIDs_list 34 | 35 | 36 | def get_PID_user(PID_value): 37 | try: 38 | user = subprocess.check_output( 39 | f"ps -o uname= -p {PID_value}", shell=True, text=True).strip() 40 | 41 | except subprocess.CalledProcessError as error: 42 | raise ProcessesError("PID doesn't exist: %d" % PID_value) from error 43 | 44 | return user 45 | 46 | 47 | def kill_PID(PID_value, signal): 48 | try: 49 | subprocess.check_call( 50 | f"kill {signal} {PID_value}", shell=True, text=True, 51 | stderr=subprocess.PIPE, stdout=subprocess.DEVNULL) 52 | 53 | except subprocess.CalledProcessError as error: 54 | raise ProcessesError(f"Unable to kill PID {PID_value}: {error.stderr}") from error 55 | -------------------------------------------------------------------------------- /optimus_manager/sessions.py: -------------------------------------------------------------------------------- 1 | import dbus 2 | from subprocess import CalledProcessError, check_call 3 | from .log_utils import get_logger 4 | 5 | 6 | class SessionsError(Exception): 7 | pass 8 | 9 | 10 | def logout_current_desktop_session(): 11 | logger = get_logger() 12 | logger.info("Logging out") 13 | 14 | try: 15 | session_bus = dbus.SessionBus() 16 | 17 | except dbus.exceptions.DBusException: 18 | pass 19 | 20 | else: 21 | def logout_kde(): 22 | kde = session_bus.get_object("org.kde.Shutdown", "/Shutdown") 23 | kde.logout() 24 | 25 | def logout_gnome(): 26 | gnome = session_bus.get_object("org.gnome.SessionManager", "/org/gnome/SessionManager") 27 | gnome.Logout(1, dbus_interface="org.gnome.SessionManager") 28 | 29 | def logout_xfce(): 30 | xfce = session_bus.get_object("org.xfce.SessionManager", "/org/xfce/SessionManager") 31 | xfce.Logout(False, True, dbus_interface="org.xfce.Session.Manager") 32 | 33 | def logout_deepin(): 34 | deepin = session_bus.get_object('com.deepin.SessionManager', '/com/deepin/SessionManager') 35 | deepin.RequestLogout() 36 | 37 | for logout_func in [ 38 | logout_kde, 39 | logout_gnome, 40 | logout_xfce, 41 | logout_deepin 42 | ]: 43 | try: 44 | logout_func() 45 | 46 | except dbus.exceptions.DBusException: 47 | pass 48 | 49 | for cmd in [ 50 | "awesome-client \"awesome.quit()\"", # AwesomeWM 51 | "bspc quit", # bspwm 52 | "pkill -SIGTERM -f dwm", # dwm 53 | "herbstclient quit", # herbstluftwm 54 | "i3-msg exit", # i3 55 | "pkill -SIGTERM -f lightdm" # lightdm 56 | "pkill -SIGTERM -f lxsession", # LXDE 57 | "openbox --exit", # Openbox 58 | "qtile cmd-obj -o cmd -f shutdown", # qtile 59 | "pkill -SIGTERM -f xmonad", # Xmonad 60 | ]: 61 | try: 62 | check_call(cmd, shell=True) 63 | except CalledProcessError: 64 | pass 65 | 66 | 67 | def get_number_of_desktop_sessions(ignore_gdm=True): 68 | sessions_list = _get_sessions_list() 69 | count = 0 70 | 71 | for session in sessions_list: 72 | username = session[2] 73 | session_type = _get_session_type(session) 74 | 75 | if (session_type == "wayland" or session_type == "x11") and \ 76 | (username != "gdm" or not ignore_gdm): 77 | count += 1 78 | 79 | return count 80 | 81 | 82 | def _get_sessions_list(): 83 | system_bus = dbus.SystemBus() 84 | logind = system_bus.get_object("org.freedesktop.login1", "/org/freedesktop/login1") 85 | sessions_list = logind.ListSessions(dbus_interface="org.freedesktop.login1.Manager") 86 | return sessions_list 87 | 88 | 89 | def _get_session_type(session): 90 | system_bus = dbus.SystemBus() 91 | session_interface = system_bus.get_object("org.freedesktop.login1", session[4]) 92 | properties_manager = dbus.Interface(session_interface, 'org.freedesktop.DBus.Properties') 93 | return properties_manager.Get("org.freedesktop.login1.Session", "Type") 94 | -------------------------------------------------------------------------------- /optimus_manager/var.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import time 5 | from pathlib import Path 6 | from . import envs 7 | from .log_utils import get_logger 8 | 9 | 10 | class VarError(Exception): 11 | pass 12 | 13 | 14 | def read_temp_conf_path_var(): 15 | filepath = Path(envs.TEMP_CONFIG_PATH_VAR_PATH) 16 | 17 | try: 18 | with open(filepath, 'r') as readfile: 19 | return readfile.read().strip() 20 | 21 | except FileNotFoundError as error: 22 | raise VarError("File doesn't exist: %s" % str(filepath)) from error 23 | 24 | except IOError as error: 25 | raise VarError("Unable to read: %s" % str(filepath)) from error 26 | 27 | 28 | def write_temp_conf_path_var(path): 29 | filepath = Path(envs.TEMP_CONFIG_PATH_VAR_PATH) 30 | os.makedirs(filepath.parent, exist_ok=True) 31 | 32 | try: 33 | with open(envs.TEMP_CONFIG_PATH_VAR_PATH, 'w') as writefile: 34 | writefile.write(path) 35 | 36 | except IOError as error: 37 | raise VarError("Unable to write to: %s" % envs.TEMP_CONFIG_PATH_VAR_PATH) from error 38 | 39 | 40 | def remove_temp_conf_path_var(): 41 | try: 42 | os.remove(envs.TEMP_CONFIG_PATH_VAR_PATH) 43 | 44 | except FileNotFoundError: 45 | pass 46 | 47 | 48 | def write_acpi_call_strings(call_strings_list): 49 | filepath = Path(envs.ACPI_CALL_STRING_VAR_PATH) 50 | os.makedirs(filepath.parent, exist_ok=True) 51 | 52 | try: 53 | with open(filepath, 'w') as writefile: 54 | json.dump(call_strings_list, writefile) 55 | 56 | except IOError as error: 57 | raise VarError("Unable to write to: %s" % str(filepath)) from error 58 | 59 | 60 | def read_acpi_call_strings(): 61 | filepath = Path(envs.ACPI_CALL_STRING_VAR_PATH) 62 | 63 | try: 64 | with open(filepath, 'r') as readfile: 65 | return json.load(readfile) 66 | 67 | except FileNotFoundError as error: 68 | raise VarError("File doesn't exist: %s" % str(filepath)) from error 69 | 70 | except (IOError, json.decoder.JSONDecodeError) as error: 71 | raise VarError("Unable to read: %s" % str(filepath)) from error 72 | 73 | 74 | def write_last_acpi_call_state(state): 75 | filepath = Path(envs.LAST_ACPI_CALL_STATE_VAR) 76 | os.makedirs(filepath.parent, exist_ok=True) 77 | 78 | try: 79 | with open(filepath, 'w') as writefile: 80 | writefile.write(state) 81 | 82 | except IOError as error: 83 | raise VarError("Unable to write to: %s" % str(filepath)) from error 84 | 85 | 86 | def read_last_acpi_call_state(): 87 | filepath = Path(envs.LAST_ACPI_CALL_STATE_VAR) 88 | 89 | try: 90 | with open(filepath, 'r') as readfile: 91 | return readfile.read().strip() 92 | 93 | except FileNotFoundError as error: 94 | raise VarError("File doesn't exist: %s" % str(filepath)) from error 95 | 96 | except IOError as error: 97 | raise VarError("Unable to read: %s" % str(filepath)) from error 98 | 99 | 100 | def make_daemon_run_id(): 101 | return time.strftime("%Y%m%dT%H%M%S") 102 | 103 | 104 | def make_switch_id(): 105 | return time.strftime("%Y%m%dT%H%M%S") 106 | 107 | 108 | def write_daemon_run_id(daemon_run_id): 109 | filepath = Path(envs.CURRENT_DAEMON_RUN_ID) 110 | os.makedirs(filepath.parent, exist_ok=True) 111 | 112 | with open(filepath, "w") as writefile: 113 | writefile.write(str(daemon_run_id)) 114 | 115 | 116 | def load_daemon_run_id(): 117 | try: 118 | with open(envs.CURRENT_DAEMON_RUN_ID, "r") as readfile: 119 | return readfile.read().strip() 120 | 121 | except FileNotFoundError: 122 | return None 123 | 124 | 125 | def write_state(state): 126 | logger = get_logger() 127 | logger.info("Writing state: %s", str(state)) 128 | filepath = Path(envs.STATE_FILE_PATH) 129 | os.makedirs(filepath.parent, exist_ok=True) 130 | 131 | with open(filepath, "w") as writefile: 132 | json.dump(state, writefile) 133 | 134 | try: 135 | os.chmod(filepath, mode=0o666) 136 | 137 | except PermissionError: 138 | pass 139 | 140 | 141 | def load_state(): 142 | try: 143 | with open(envs.STATE_FILE_PATH, "r") as readfile: 144 | return json.load(readfile) 145 | 146 | except FileNotFoundError: 147 | return None 148 | 149 | 150 | def cleanup_tmp_vars(): 151 | shutil.rmtree(envs.TMP_VARS_FOLDER_PATH, ignore_errors=True) 152 | -------------------------------------------------------------------------------- /optimus_manager/xorg.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import optimus_manager.checks as checks 4 | import optimus_manager.envs as envs 5 | from pathlib import Path 6 | from .config import load_extra_xorg_options 7 | from .log_utils import get_logger 8 | from .pci import get_gpus_bus_ids 9 | 10 | 11 | class XorgSetupError(Exception): 12 | pass 13 | 14 | 15 | def configure_xorg(config, requested_gpu_mode): 16 | bus_ids = get_gpus_bus_ids() 17 | xorg_extra = load_extra_xorg_options() 18 | 19 | if requested_gpu_mode == "nvidia" or not ("intel" in bus_ids or "amd" in bus_ids): 20 | xorg_conf_text = _generate_nvidia(config, bus_ids, xorg_extra) 21 | 22 | elif requested_gpu_mode == "integrated": 23 | xorg_conf_text = _generate_integrated(config, bus_ids, xorg_extra) 24 | 25 | elif requested_gpu_mode == "hybrid": 26 | xorg_conf_text = _generate_hybrid(config, bus_ids, xorg_extra) 27 | 28 | _remove_conflicting_confs() 29 | _write_xorg_conf(xorg_conf_text) 30 | 31 | 32 | def cleanup_xorg_conf(): 33 | _remove_conf(envs.XORG_CONF_PATH) 34 | 35 | 36 | def is_xorg_running(): 37 | return any([ 38 | subprocess.run( 39 | f"pidof {name}", shell=True, stdout=subprocess.DEVNULL 40 | ).returncode == 0 41 | 42 | for name in ["X", "Xorg"] 43 | ]) 44 | 45 | 46 | def do_xsetup(requested_mode): 47 | logger = get_logger() 48 | 49 | if requested_mode == "nvidia": 50 | provider = checks.get_integrated_provider() 51 | logger.info("Running xrandr commands") 52 | 53 | try: 54 | for cmd in [ 55 | (f"xrandr --setprovideroutputsource \"{provider}\" NVIDIA-0"), 56 | "xrandr --auto" 57 | ]: 58 | subprocess.check_call( 59 | cmd, shell=True, text=True, stderr=subprocess.PIPE, 60 | stdout=subprocess.DEVNULL) 61 | 62 | except subprocess.CalledProcessError as error: 63 | logger.error(f"Unable to setup Prime: xrandr error: {error.stderr}") 64 | 65 | script_path = _get_xsetup_script_path(requested_mode) 66 | logger.info("Running script: %s", script_path) 67 | 68 | try: 69 | subprocess.check_call( 70 | script_path, text=True, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL) 71 | 72 | except subprocess.CalledProcessError as e: 73 | logger.error(f"Unable to run script: {script_path}: {e.stderr}") 74 | 75 | 76 | def set_DPI(config): 77 | logger = get_logger() 78 | dpi_str = config["nvidia"]["dpi"] 79 | 80 | if dpi_str == "": 81 | return 82 | 83 | try: 84 | subprocess.check_call( 85 | f"xrandr --dpi {dpi_str}", shell=True, text=True, 86 | stderr=subprocess.PIPE, stdout=subprocess.DEVNULL) 87 | 88 | except subprocess.CalledProcessError as error: 89 | logger.error(f"Unable to set DPI: xrandr error: {error.stderr}") 90 | 91 | 92 | def _get_xsetup_script_path(requested_mode): 93 | logger = get_logger() 94 | bus_ids = get_gpus_bus_ids() 95 | 96 | if requested_mode == "nvidia" or not ("intel" in bus_ids or "amd" in bus_ids): 97 | script_name = "nvidia" 98 | 99 | elif requested_mode == "integrated": 100 | if "intel" in bus_ids and Path(envs.XSETUP_SCRIPTS_PATHS["intel"]).exists(): 101 | script_name = "intel" 102 | 103 | else: 104 | script_name = "integrated" 105 | 106 | elif requested_mode == "hybrid": 107 | script_name = "hybrid" 108 | 109 | else: 110 | assert False 111 | 112 | script_path = envs.XSETUP_SCRIPTS_PATHS[script_name] 113 | return script_path 114 | 115 | 116 | def _generate_nvidia(config, bus_ids, xorg_extra): 117 | integrated_gpu = "intel" if "intel" in bus_ids else "amd" 118 | driver = "modesetting" 119 | 120 | if config[integrated_gpu]["driver"] != "modesetting" and config[integrated_gpu]["driver"] != "hybrid": 121 | 122 | if "intel" in bus_ids and checks.is_xorg_intel_module_available(): 123 | driver = "intel" 124 | 125 | elif checks.is_xorg_amdgpu_module_available(): 126 | driver = "amdgpu" 127 | 128 | else: 129 | driver = "modesetting" 130 | 131 | xorg_extra_nvidia = xorg_extra["nvidia-mode"]["nvidia-gpu"] 132 | xorg_extra_integrated = xorg_extra["nvidia-mode"]["integrated-gpu"] 133 | text = _make_modules_paths_section() 134 | 135 | text += "Section \"ServerLayout\"\n" \ 136 | "\tIdentifier \"layout\"\n" \ 137 | "\tScreen 0 \"nvidia\"\n" \ 138 | "\tInactive \"integrated\"\n" \ 139 | "EndSection\n\n" 140 | 141 | text += _make_nvidia_device_section(config, bus_ids, xorg_extra_nvidia) 142 | 143 | text += "Section \"Screen\"\n" \ 144 | "\tIdentifier \"nvidia\"\n" \ 145 | "\tDevice \"nvidia\"\n" \ 146 | "\tOption \"AllowEmptyInitialConfiguration\"\n" 147 | 148 | if config["nvidia"]["allow_external_gpus"] == "yes": 149 | text += "\tOption \"AllowExternalGpus\"\n" 150 | 151 | text += "EndSection\n\n" 152 | 153 | text += "Section \"Device\"\n" \ 154 | "\tIdentifier \"integrated\"\n" \ 155 | "\tDriver \"%s\"\n" % driver 156 | 157 | if driver == "modesetting": 158 | text += "\tOption \"AccelMethod\" \"none\"\n" 159 | 160 | text += "\tBusID \"%s\"\n" % bus_ids[integrated_gpu] 161 | 162 | for line in xorg_extra_integrated: 163 | text += ("\t" + line + "\n") 164 | 165 | text += "EndSection\n\n" 166 | 167 | text += "Section \"Screen\"\n" \ 168 | "\tIdentifier \"integrated\"\n" \ 169 | "\tDevice \"integrated\"\n" \ 170 | "EndSection\n\n" 171 | 172 | text += _make_server_flags_section(config) 173 | return text 174 | 175 | 176 | def _generate_integrated(config, bus_ids, xorg_extra): 177 | xorg_extra_lines = xorg_extra["integrated-mode"]["integrated-gpu"] 178 | 179 | text = "Section \"ServerLayout\"\n" \ 180 | "\tIdentifier \"layout\"\n" \ 181 | "\tScreen 0 \"integrated\"\n" \ 182 | "EndSection\n\n" 183 | 184 | text += "Section \"Monitor\"\n" \ 185 | "\tIdentifier \"monitor0\"\n" \ 186 | "EndSection\n\n" 187 | 188 | if "intel" in bus_ids: 189 | text += _make_intel_device_section(config, bus_ids, xorg_extra_lines) 190 | 191 | else: 192 | text += _make_amd_device_section(config, bus_ids, xorg_extra_lines) 193 | 194 | text += "Section \"Screen\"\n" \ 195 | "\tIdentifier \"integrated\"\n" \ 196 | "\tDevice \"integrated\"\n" \ 197 | "\tMonitor \"monitor0\"\n" \ 198 | "EndSection\n\n" 199 | 200 | return text 201 | 202 | 203 | def _generate_hybrid(config, bus_ids, xorg_extra): 204 | xorg_extra_lines_integrated = xorg_extra["hybrid-mode"]["integrated-gpu"] 205 | xorg_extra_lines_nvidia = xorg_extra["hybrid-mode"]["nvidia-gpu"] 206 | text = _make_modules_paths_section() 207 | 208 | text += "Section \"ServerLayout\"\n" \ 209 | "\tIdentifier \"layout\"\n" \ 210 | "\tScreen 0 \"integrated\"\n" \ 211 | "\tInactive \"nvidia\"\n" \ 212 | "\tOption \"AllowNVIDIAGPUScreens\"\n" \ 213 | "EndSection\n\n" 214 | 215 | if "intel" in bus_ids: 216 | text += _make_intel_device_section(config, bus_ids, xorg_extra_lines_integrated) 217 | 218 | else: 219 | text += _make_amd_device_section(config, bus_ids, xorg_extra_lines_integrated) 220 | 221 | text += "Section \"Screen\"\n" \ 222 | "\tIdentifier \"integrated\"\n" \ 223 | "\tDevice \"integrated\"\n" 224 | 225 | if config["nvidia"]["allow_external_gpus"] == "yes": 226 | text += "\tOption \"AllowExternalGpus\"\n" 227 | 228 | text += "EndSection\n\n" 229 | text += _make_nvidia_device_section(config, bus_ids, xorg_extra_lines_nvidia) 230 | 231 | text += "Section \"Screen\"\n" \ 232 | "\tIdentifier \"nvidia\"\n" \ 233 | "\tDevice \"nvidia\"\n" \ 234 | "EndSection\n\n" 235 | 236 | text += _make_server_flags_section(config) 237 | return text 238 | 239 | 240 | def _make_modules_paths_section(): 241 | return "Section \"Files\"\n" \ 242 | "\tModulePath \"/usr/lib/nvidia\"\n" \ 243 | "\tModulePath \"/usr/lib32/nvidia\"\n" \ 244 | "\tModulePath \"/usr/lib32/nvidia/xorg/modules\"\n" \ 245 | "\tModulePath \"/usr/lib32/xorg/modules\"\n" \ 246 | "\tModulePath \"/usr/lib64/nvidia/xorg/modules\"\n" \ 247 | "\tModulePath \"/usr/lib64/nvidia/xorg\"\n" \ 248 | "\tModulePath \"/usr/lib64/xorg/modules\"\n" \ 249 | "EndSection\n\n" 250 | 251 | 252 | def _make_nvidia_device_section(config, bus_ids, xorg_extra_lines): 253 | options = config["nvidia"]["options"].replace(" ", "").split(",") 254 | 255 | text = "Section \"Device\"\n" \ 256 | "\tIdentifier \"nvidia\"\n" \ 257 | "\tDriver \"nvidia\"\n" 258 | 259 | text += "\tBusID \"%s\"\n" % bus_ids["nvidia"] 260 | 261 | if "overclocking" in options: 262 | text += "\tOption \"Coolbits\" \"28\"\n" 263 | 264 | if "triple_buffer" in options: 265 | text += "\tOption \"TripleBuffer\" \"true\"\n" 266 | 267 | for line in xorg_extra_lines: 268 | text += ("\t" + line + "\n") 269 | 270 | text += "EndSection\n\n" 271 | return text 272 | 273 | 274 | def _make_intel_device_section(config, bus_ids, xorg_extra_lines): 275 | logger = get_logger() 276 | 277 | if config["intel"]["driver"] == "intel" and not checks.is_xorg_intel_module_available(): 278 | logger.warning("The Xorg module intel is not available. Defaulting to modesetting.") 279 | driver = "modesetting" 280 | 281 | elif config["intel"]["driver"] == "hybrid": 282 | driver = "intel" 283 | 284 | else: 285 | driver = config["intel"]["driver"] 286 | 287 | dri = int(config["intel"]["dri"]) 288 | text = "Section \"Device\"\n" \ 289 | "\tIdentifier \"integrated\"\n" 290 | text += "\tDriver \"%s\"\n" % driver 291 | text += "\tBusID \"%s\"\n" % bus_ids["intel"] 292 | 293 | if config["intel"]["accel"] != "": 294 | text += "\tOption \"AccelMethod\" \"%s\"\n" % config["intel"]["accel"] 295 | 296 | if config["intel"]["tearfree"] != "": 297 | tearfree_enabled_str = {"yes": "true", "no": "false"}[config["intel"]["tearfree"]] 298 | text += "\tOption \"TearFree\" \"%s\"\n" % tearfree_enabled_str 299 | 300 | if dri != 0: 301 | text += "\tOption \"DRI\" \"%d\"\n" % dri 302 | 303 | for line in xorg_extra_lines: 304 | text += ("\t" + line + "\n") 305 | 306 | text += "EndSection\n\n" 307 | return text 308 | 309 | 310 | def _make_amd_device_section(config, bus_ids, xorg_extra_lines): 311 | logger = get_logger() 312 | 313 | if config["amd"]["driver"] == "amdgpu" and not checks.is_xorg_amdgpu_module_available(): 314 | logger.warning("The Xorg module amdgpu is not available. Defaulting to modesetting.") 315 | driver = "modesetting" 316 | 317 | elif config["amd"]["driver"] == "hybrid": 318 | driver = "amdgpu" 319 | 320 | else: 321 | driver = config["amd"]["driver"] 322 | 323 | dri = int(config["amd"]["dri"]) 324 | 325 | text = "Section \"Device\"\n" \ 326 | "\tIdentifier \"integrated\"\n" 327 | 328 | text += "\tDriver \"%s\"\n" % driver 329 | text += "\tBusID \"%s\"\n" % bus_ids["amd"] 330 | 331 | if config["amd"]["tearfree"] != "": 332 | tearfree_enabled_str = {"yes": "true", "no": "false"}[config["amd"]["tearfree"]] 333 | text += "\tOption \"TearFree\" \"%s\"\n" % tearfree_enabled_str 334 | 335 | if dri != 0: 336 | text += "\tOption \"DRI\" \"%d\"\n" % dri 337 | 338 | for line in xorg_extra_lines: 339 | text += ("\t" + line + "\n") 340 | 341 | text += "EndSection\n\n" 342 | return text 343 | 344 | 345 | def _make_server_flags_section(config): 346 | if config["nvidia"]["ignore_abi"] == "yes": 347 | return ( 348 | "Section \"ServerFlags\"\n" 349 | "\tOption \"IgnoreABI\" \"1\"\n" 350 | "EndSection\n\n" 351 | ) 352 | 353 | return "" 354 | 355 | 356 | def _remove_conf(conf): 357 | try: 358 | os.remove(conf) 359 | 360 | except FileNotFoundError: 361 | pass 362 | 363 | 364 | def _remove_conflicting_confs(): 365 | _remove_conf("/etc/X11/xorg.conf") 366 | _remove_conf("/etc/X11/xorg.conf.d/90-mhwd.conf") 367 | 368 | 369 | def _write_xorg_conf(xorg_conf_text): 370 | logger = get_logger() 371 | filepath = Path(envs.XORG_CONF_PATH) 372 | 373 | try: 374 | os.makedirs(filepath.parent, mode=0o755, exist_ok=True) 375 | 376 | with open(filepath, 'w') as writefile: 377 | logger.info("Writing Xorg conf: %s", envs.XORG_CONF_PATH) 378 | writefile.write(xorg_conf_text) 379 | 380 | except IOError as error: 381 | raise XorgSetupError("Unable to write Xorg conf: %s" % str(filepath)) from error 382 | -------------------------------------------------------------------------------- /package/1-build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | here="$(realpath "$(dirname "${0}")")" 5 | 6 | 7 | mainFunction () { 8 | sudo -v 9 | so rm --force "${here}/"*.pkg.* 10 | syncSrc 11 | makePackage 12 | zeroPkgver 13 | } 14 | 15 | 16 | checkRequiredSoftware () { 17 | if [[ ! -x "/usr/bin/rsync" ]]; then 18 | echo "Not installed: rsync" >&2 19 | exit 1 20 | fi 21 | } 22 | 23 | 24 | makePackage () { 25 | cd "${here}" 26 | 27 | export PACMAN_AUTH="sudo" 28 | export PACKAGER="${USER} <@${HOSTNAME}>" 29 | so makepkg --needed --noconfirm --noextract --syncdeps --skipinteg --rmdeps 30 | } 31 | 32 | 33 | so () { 34 | local commands="${*}" 35 | 36 | if [[ "${verbose}" -eq 1 ]]; then 37 | if ! ${commands}; then 38 | exit "${?}" 39 | fi 40 | elif ! error="$(eval "${commands}" 2>&1 >"/dev/null")" ; then 41 | if [ "${error}" == "" ] ; then 42 | error="Command failed: ${commands}" 43 | fi 44 | 45 | echo "${FUNCNAME[1]}: ${error}" >&2 46 | exit 1 47 | fi 48 | } 49 | 50 | 51 | syncSrc () { 52 | local file 53 | local destDir="${here}/src/optimus-manager" 54 | 55 | so mkdir --parents "${destDir}" 56 | cd "${here}/.." 57 | 58 | while read -r file; do 59 | rsync --archive "${file}" "${destDir}" 60 | done < <( 61 | find . -mindepth 1 -maxdepth 1 -not -name "package" -and -not -name ".*" | 62 | cut --delimiter='/' --fields=2- 63 | ) 64 | } 65 | 66 | 67 | zeroPkgver () { 68 | sed --in-place "s|^pkgver=.*|pkgver=0|" "${here}/PKGBUILD" 69 | } 70 | 71 | 72 | checkRequiredSoftware 73 | mainFunction 74 | -------------------------------------------------------------------------------- /package/2-install.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | here="$(realpath "$(dirname "${0}")")" 4 | package="$(find "${here}" -name "*.pkg.*")" 5 | 6 | sudo pacman --upgrade "${package}" 7 | -------------------------------------------------------------------------------- /package/3-publish.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | here="$(realpath "$(dirname "${0}")")" 5 | files=("optimus-manager.install" "PKGBUILD") 6 | 7 | 8 | mainFunction () { 9 | checkDescription 10 | cloneAurRepo 11 | checkFilesHaveBeenChanged 12 | syncFiles 13 | updateSrcinfo 14 | uploadChanges 15 | removeAurRepoClone 16 | } 17 | 18 | 19 | checkDescription () { 20 | if [[ -z "${description}" ]]; then 21 | echo "No description provided" >&2 22 | exit 1 23 | fi 24 | } 25 | 26 | 27 | checkFilesHaveBeenChanged () { 28 | local aurSum 29 | local changed=false 30 | local file 31 | local index=0 32 | local repoSum 33 | 34 | while [[ "${index}" -lt "${#files[@]}" ]] && ! "${changed}"; do 35 | repoSum="$(fileSum "${here}/${files[$index]}")" 36 | aurSum="$(fileSum "${here}/optimus-manager-git/${files[$index]}")" 37 | 38 | if [[ "${repoSum}" != "${aurSum}" ]]; then 39 | changed=true 40 | else 41 | index="$((index +1))" 42 | fi 43 | done 44 | 45 | if ! "${changed}"; then 46 | removeAurRepoClone 47 | exit 0 48 | fi 49 | } 50 | 51 | 52 | cloneAurRepo () { 53 | cd "${here}" 54 | 55 | if [[ ! -d "optimus-manager-git" ]]; then 56 | so git clone \ 57 | --depth 1 --shallow-submodules \ 58 | "ssh://aur@aur.archlinux.org/optimus-manager-git.git" 59 | else 60 | so git fetch 61 | fi 62 | } 63 | 64 | 65 | fileModificationEpoch () { 66 | local file="${1}" 67 | stat --format=%Y "${file}" 68 | } 69 | 70 | 71 | fileSum () { 72 | local file="${1}" 73 | 74 | sha1sum "${file}" | 75 | cut --delimiter=' ' --fields=1 76 | } 77 | 78 | 79 | removeAurRepoClone () { 80 | so rm --recursive --force "${here}/optimus-manager-git" 81 | } 82 | 83 | 84 | so () { 85 | local error 86 | 87 | #shellcheck disable=SC2068 88 | if ! error="$(${@} 2>&1 >"/dev/null")" ; then 89 | if [[ -z "${error}" ]] ; then 90 | error="Command failed" 91 | fi 92 | 93 | echo "${FUNCNAME[1]}: ${*}:" >&2 94 | echo "${error}" >&2 95 | exit 1 96 | fi 97 | } 98 | 99 | 100 | syncFiles () { 101 | local file 102 | 103 | for file in "${files[@]}"; do 104 | so cp --force "${here}/${file}" "${here}/optimus-manager-git/${file}" 105 | done 106 | } 107 | 108 | 109 | updateSrcinfo () { 110 | cd "${here}/optimus-manager-git" 111 | makepkg --printsrcinfo > .SRCINFO 112 | } 113 | 114 | 115 | uploadChanges () { 116 | cd "${here}/optimus-manager-git" 117 | 118 | so git add --all 119 | #shellcheck disable=SC2016 120 | git commit --message="${description}" >/dev/null 121 | so git push 122 | } 123 | 124 | 125 | description="${*}" 126 | mainFunction 127 | -------------------------------------------------------------------------------- /package/PKGBUILD: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | pkgname="optimus-manager-git" 4 | install="optimus-manager.install" 5 | pkgdesc="Allows using Nvidia Optimus laptop graphics" 6 | license=("MIT") 7 | 8 | # PKGBUILD and program maintained at: 9 | url="https://github.com/Askannz/optimus-manager" 10 | 11 | epoch=2 12 | pkgver=0 13 | pkgrel=1 14 | arch=("any") 15 | 16 | source=("git+${url}.git") 17 | sha1sums=("SKIP") 18 | 19 | 20 | conflicts=( 21 | "bumblebee" 22 | "envycontrol" 23 | "nvidia-exec" 24 | "nvidia-switch" 25 | "nvidia-xrun" 26 | "optimus-manager" 27 | "switcheroo-control" 28 | ) 29 | 30 | 31 | provides=( 32 | "optimus-manager=${pkgver}" 33 | ) 34 | 35 | 36 | makedepends=( 37 | "git" 38 | "python-build" 39 | "python-installer" 40 | "python-setuptools" 41 | "python-wheel" 42 | ) 43 | 44 | 45 | depends=( 46 | "dbus-python" 47 | "glxinfo" 48 | "NVIDIA-MODULE" 49 | "python" 50 | "xorg-xrandr" 51 | ) 52 | 53 | 54 | optdepends=( 55 | 'gdm-prime: needed if your login screen is gdm' 56 | 'bbswitch: alternatively switches GPUs by using standard Optimus ACPI calls' 57 | 'acpi_call: alternatively switches GPUs by brute forcing ACPI calls' 58 | ) 59 | 60 | 61 | backup=( 62 | 'etc/optimus-manager/nvidia-enable.sh' 63 | 'etc/optimus-manager/nvidia-disable.sh' 64 | 'etc/optimus-manager/optimus-manager.conf' 65 | 'etc/optimus-manager/xorg/integrated-mode/integrated-gpu.conf' 66 | 'etc/optimus-manager/xorg/nvidia-mode/integrated-gpu.conf' 67 | 'etc/optimus-manager/xorg/nvidia-mode/nvidia-gpu.conf' 68 | 'etc/optimus-manager/xorg/hybrid-mode/integrated-gpu.conf' 69 | 'etc/optimus-manager/xorg/hybrid-mode/nvidia-gpu.conf' 70 | 'etc/optimus-manager/xsetup-integrated.sh' 71 | 'etc/optimus-manager/xsetup-nvidia.sh' 72 | 'etc/optimus-manager/xsetup-hybrid.sh' 73 | 'var/lib/optimus-manager/persistent/startup_mode' 74 | ) 75 | 76 | 77 | SoftwareVersion () { 78 | cd "${srcdir}/optimus-manager" 79 | local Version="$(git rev-list --count HEAD)" 80 | 81 | if [[ "${Version}" -le 1 ]]; then 82 | echo 0 83 | else 84 | echo "${Version}" 85 | fi 86 | } 87 | 88 | 89 | PythonVersion () { 90 | pacman --sync --print-format "%v" python | 91 | cut --delimiter='.' --fields=1,2 92 | } 93 | 94 | 95 | pkgver () { 96 | local SoftwareVersion; SoftwareVersion="$(SoftwareVersion)" 97 | local PythonVersion; PythonVersion="$(PythonVersion)" 98 | 99 | if [[ -z "${SoftwareVersion}" ]]; then 100 | echo "Failed to retrieve: SoftwareVersion" >&2 101 | false 102 | elif [[ -z "${PythonVersion}" ]]; then 103 | echo "Failed to retrieve: PythonVersion" >&2 104 | false 105 | else 106 | printf "%s.python%s" \ 107 | "${SoftwareVersion}" \ 108 | "${PythonVersion}" 109 | fi 110 | } 111 | 112 | 113 | prepare () { 114 | local Version; Version="$(SoftwareVersion)" 115 | 116 | sed --in-place \ 117 | "s|^VERSION = \".*\"$|VERSION = \"${Version}\"|" \ 118 | "${srcdir}/optimus-manager/optimus_manager/envs.py" 119 | } 120 | 121 | 122 | build () { 123 | cd "${srcdir}/optimus-manager" 124 | python3 setup.py build 125 | } 126 | 127 | 128 | package () { 129 | PackageFiles 130 | GeneratePyCache 131 | } 132 | 133 | 134 | PackageFiles () { 135 | PackageDefaultConf 136 | PackageEtc 137 | PackageLicense 138 | PackageLoginManagers 139 | PackageManual 140 | PackageModprobe 141 | PackageProfile 142 | PackageSystemd 143 | } 144 | 145 | 146 | PackageDefaultConf () { 147 | install -Dm644 \ 148 | "${srcdir}/optimus-manager/optimus-manager.conf" \ 149 | "${pkgdir}/usr/share/optimus-manager/optimus-manager.conf" 150 | } 151 | 152 | 153 | PackageEtc () { 154 | cd "${srcdir}/optimus-manager/config" 155 | 156 | local Etc="${pkgdir}/etc/optimus-manager" 157 | local File 158 | local Files; readarray -t Files < <( 159 | find . -type f | 160 | cut --delimiter='/' --fields=2- 161 | ) 162 | 163 | for File in "${Files[@]}"; do 164 | if echo "${File}" | grep --quiet --extended-regexp ".sh|.py"; then 165 | local Permissions=755 166 | else 167 | local Permissions=644 168 | fi 169 | 170 | install -Dm"${Permissions}" "${File}" "${Etc}/${File}" 171 | done 172 | } 173 | 174 | 175 | PackageLicense () { 176 | install -Dm644 \ 177 | "${srcdir}/optimus-manager/LICENSE" \ 178 | "${pkgdir}/usr/share/licenses/$pkgname/LICENSE" 179 | } 180 | 181 | 182 | PackageLoginManagers () { 183 | cd "${srcdir}/optimus-manager" 184 | 185 | install -Dm644 \ 186 | login_managers/lightdm/20-optimus-manager.conf \ 187 | "${pkgdir}/etc/lightdm/lightdm.conf.d/20-optimus-manager.conf" 188 | 189 | install -Dm644 \ 190 | login_managers/sddm/20-optimus-manager.conf \ 191 | "${pkgdir}/etc/sddm.conf.d/20-optimus-manager.conf" 192 | } 193 | 194 | 195 | PackageManual () { 196 | install -Dm644 \ 197 | "${srcdir}/optimus-manager/optimus-manager.1" \ 198 | "${pkgdir}/usr/share/man/man1/optimus-manager.1" 199 | } 200 | 201 | 202 | PackageModprobe () { 203 | install -Dm644 \ 204 | "${srcdir}/optimus-manager/modules/optimus-manager.conf" \ 205 | "${pkgdir}/usr/lib/modprobe.d/optimus-manager.conf" 206 | } 207 | 208 | 209 | PackageProfile () { 210 | install -Dm644 \ 211 | "${srcdir}/optimus-manager/profile.d/optimus-manager.sh" \ 212 | "${pkgdir}/etc/profile.d/optimus-manager.sh" 213 | } 214 | 215 | 216 | PackageSystemd () { 217 | cd "${srcdir}/optimus-manager" 218 | 219 | install -Dm644 \ 220 | "systemd/optimus-manager.service" \ 221 | "${pkgdir}/usr/lib/systemd/system/optimus-manager.service" 222 | 223 | install -Dm755 \ 224 | "systemd/suspend/optimus-manager.py" \ 225 | "${pkgdir}/usr/lib/systemd/system-sleep/optimus-manager.py" 226 | } 227 | 228 | 229 | GeneratePyCache () { 230 | cd "${srcdir}/optimus-manager" 231 | 232 | python3 setup.py install \ 233 | --root="${pkgdir}" \ 234 | --optimize=1 \ 235 | --skip-build 236 | } 237 | -------------------------------------------------------------------------------- /package/optimus-manager.install: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | 4 | post_install () { 5 | rm --force \ 6 | "etc/X11/xorg.conf" \ 7 | "etc/X11/xorg.conf.backup" \ 8 | "etc/X11/xorg.conf.nvidia-xconfig-original" \ 9 | "etc/X11/nvidia-xorg.conf.d/"*nvidia* 10 | 11 | systemctl enable "optimus-manager.service" 12 | } 13 | 14 | 15 | post_upgrade () { 16 | systemctl daemon-reload 17 | } 18 | 19 | 20 | pre_remove() { 21 | systemctl disable --now "optimus-manager.service" 22 | rm --force "etc/X11/xorg.conf.d/10-optimus-manager.conf" 23 | } 24 | -------------------------------------------------------------------------------- /profile.d/optimus-manager.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [[ "${XDG_SESSION_TYPE}" == "wayland" ]] && [[ -x "/usr/bin/nvidia-persistenced" ]]; then 4 | export __GLX_VENDOR_LIBRARY_NAME="nvidia" 5 | export __VK_LAYER_NV_optimus="NVIDIA_only" 6 | # The desktop and most apps use EGL instead, so they will still run on the integrated GPU. 7 | # Check which apps are using the Nvidia GPU with `nvidia-smi`. 8 | fi 9 | -------------------------------------------------------------------------------- /runit/finish: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | python3 -u -m optimus_manager.hooks.post_daemon_stop 4 | -------------------------------------------------------------------------------- /runit/run: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | python3 -u -m optimus_manager.hooks.pre_daemon_start 4 | python3 -u -m optimus_manager.hooks.pre_xorg_start 5 | exec python3 -u -m optimus_manager.daemon 6 | -------------------------------------------------------------------------------- /s6/dependencies: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Askannz/optimus-manager/43bfbcb356ab0ba816e68e6f1b2b38c3846314ad/s6/dependencies -------------------------------------------------------------------------------- /s6/finish: -------------------------------------------------------------------------------- 1 | #! /usr/bin/execlineb -P 2 | 3 | foreground { python3 -u -m optimus_manager.hooks.post_daemon_stop } 4 | -------------------------------------------------------------------------------- /s6/run: -------------------------------------------------------------------------------- 1 | #! /usr/bin/execlineb -P 2 | 3 | foreground { python3 -u -m optimus_manager.hooks.pre_daemon_start } 4 | foreground { python3 -u -m optimus_manager.hooks.pre_xorg_start } 5 | exec python3 -u -m optimus_manager.daemon 6 | -------------------------------------------------------------------------------- /s6/type: -------------------------------------------------------------------------------- 1 | longrun 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | from os.path import dirname, join 3 | from setuptools import setup, find_packages 4 | from optimus_manager import __version__ 5 | 6 | 7 | setup( 8 | name='optimus-manager', 9 | version=__version__, 10 | description='Handles GPU switching on Optimus laptops', 11 | long_description=open( 12 | join(dirname(__file__), 'README.md')).read(), 13 | url='https://github.com/Askannz/optimus-manager', 14 | author='Robin Lange', 15 | author_email='robin.langenc@gmail.com', 16 | license='MIT', 17 | packages=find_packages(), 18 | entry_points={ 19 | 'console_scripts': [ 20 | 'optimus-manager=optimus_manager.client:main', 21 | 'prime-switch=optimus_manager.hooks.pre_xorg_start:main', 22 | 'prime-offload=optimus_manager.hooks.post_xorg_start:main' 23 | ], 24 | }, 25 | package_data={'optimus_manager': ['config_schema.json']}, 26 | keywords=['optimus', 'nvidia', 'bbswitch', 'prime', 'gpu'], 27 | classifiers=[ 28 | 'License :: OSI Approved :: MIT License', 29 | 'Programming Language :: Python :: 3', 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /system-info.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | 5 | mainFunction () { 6 | inxiInfo 7 | displayManagerInfo 8 | 9 | lspciInfo 10 | xrandrInfo 11 | glxinfoOffloadedInfo 12 | 13 | kernelErrorsInfo 14 | displayManagerErrorsInfo 15 | nvidiaX11LogInfo 16 | 17 | nvidiaSmiInfo 18 | 19 | optimusManagerDaemonInfo 20 | optimusManagerSwitchingInfo 21 | optimusManagerConfInfo 22 | optimusManagerX11ConfInfo 23 | } 24 | 25 | 26 | checkDepends () { 27 | local missing 28 | local missings=() 29 | local depend 30 | 31 | local depends=( 32 | glxinfo 33 | inxi 34 | lspci 35 | nvidia-smi 36 | xrandr 37 | ) 38 | 39 | for depend in "${depends[@]}"; do 40 | if [[ ! -x "/usr/bin/${depend}" ]]; then 41 | missings+=("${depend}") 42 | fi 43 | done 44 | 45 | if [[ "${#missings}" -ne 0 ]]; then 46 | echo "Required software not installed:" >&2 47 | 48 | for missing in "${missings[@]}"; do 49 | echo "- ${missing}" >&2 50 | done 51 | 52 | exit 1 53 | fi 54 | } 55 | 56 | 57 | displayManagerErrorsInfo () { 58 | if [[ -f "/usr/lib/systemd/system/${displayManager}.service" ]]; then 59 | echo "=== display manager errors ===" 60 | journalctl --boot=1 --priority=3 --unit="${displayManager}.service" --no-pager 61 | echo 62 | fi 63 | } 64 | 65 | 66 | displayManagerInfo () { 67 | local service="/etc/systemd/system/display-manager.service" 68 | 69 | if [[ -f "${service}" ]]; then 70 | echo "=== display manager ===" 71 | 72 | displayManager="$( 73 | grep "^ExecStart" "${service}" | 74 | cut --delimiter='=' --fields=2 | 75 | rev | 76 | cut --delimiter='/' --fields=1 | 77 | rev 78 | )" 79 | 80 | echo "${displayManager}" 81 | echo 82 | fi 83 | } 84 | 85 | 86 | glxinfoOffloadedInfo () { 87 | echo "=== glxinfo offloaded ===" 88 | 89 | __NV_PRIME_RENDER_OFFLOAD=1 \ 90 | __GLX_VENDOR_LIBRARY_NAME="nvidia" \ 91 | __VK_LAYER_NV_optimus="NVIDIA_only" \ 92 | __GL_SHOW_GRAPHICS_OSD=1 \ 93 | glxinfo | 94 | grep --ignore-case -e vendor -e renderer || 95 | true 96 | 97 | echo 98 | } 99 | 100 | 101 | inxiInfo () { 102 | echo "=== inxi ===" 103 | inxi -SMGsr -c0 104 | echo 105 | } 106 | 107 | 108 | kernelErrorsInfo () { 109 | if [[ -x "/usr/bin/journalctl" ]]; then 110 | echo "=== kernel errors ===" 111 | journalctl --boot=1 --priority=3 --dmesg --no-pager 112 | echo 113 | fi 114 | } 115 | 116 | 117 | lspciInfo () { 118 | echo "=== lspci ===" 119 | lspci | grep --ignore-case -e "3d" -e "vga" 120 | echo 121 | } 122 | 123 | 124 | nvidiaSmiInfo () { 125 | echo "=== nvidia-smi ===" 126 | nvidia-smi 127 | echo 128 | } 129 | 130 | 131 | nvidiaX11LogInfo () { 132 | local log="/var/log/Xorg.0.log" 133 | echo "=== nvidia x11 log ===" 134 | 135 | if [[ ! "${log}" ]]; then 136 | echo "no log at: ${log}" 137 | else 138 | local messages; messages="$( 139 | grep -e "nvidia" "${log}" 140 | )" 141 | 142 | if [[ -z "${messages}" ]]; then 143 | echo "no nvidia messages at: ${messages}" 144 | else 145 | echo "(--) = probed" 146 | echo "(**) = from config file" 147 | echo "(==) = default setting" 148 | echo "(++) = from command line" 149 | echo "(!!) = notice" 150 | echo "(II) = informational" 151 | echo "(WW) = warning" 152 | echo "(EE) = error" 153 | echo "(NI) = not implemented" 154 | echo "(??) = unknown" 155 | echo "${messages}" 156 | fi 157 | fi 158 | 159 | echo 160 | } 161 | 162 | 163 | optimusManagerConfInfo () { 164 | if [[ -x "/usr/bin/optimus-manager" ]]; then 165 | echo "=== optimus-manager conf ===" 166 | cat "/etc/optimus-manager/optimus-manager.conf" || true 167 | echo 168 | fi 169 | } 170 | 171 | 172 | optimusManagerDaemonInfo () { 173 | echo "=== optimus-manager daemon ===" 174 | 175 | if [[ ! -x "/usr/bin/optimus-manager" ]]; then 176 | echo "not installed" >&2 177 | elif [[ -x "/usr/bin/systemctl" ]]; then 178 | optimusManagerServiceStatus 179 | elif local log && log="$(optimusManagerLog "daemon")" && [[ -n "${log}" ]]; then 180 | cat "${log}" 181 | else 182 | optimus-manager --status || true 183 | fi 184 | 185 | echo 186 | } 187 | 188 | 189 | optimusManagerLog () { 190 | local type="${1}" 191 | local dir="/var/log/optimus-manager/${type}" 192 | 193 | if [[ -d "${dir}" ]]; then 194 | find "${dir}" | 195 | sort | 196 | tail -n1 197 | fi 198 | } 199 | 200 | 201 | optimusManagerServiceStatus () { 202 | SYSTEMD_COLORS=0 \ 203 | systemctl --full --no-pager status optimus-manager || 204 | true 205 | } 206 | 207 | 208 | optimusManagerSwitchingInfo () { 209 | echo "=== optimus-manager switching log ===" 210 | 211 | if local log && log="$(optimusManagerLog "switch")" && [[ -n "${log}" ]]; then 212 | cat "${log}" 213 | else 214 | echo "absent" 215 | fi 216 | 217 | echo 218 | } 219 | 220 | 221 | optimusManagerX11ConfInfo () { 222 | local conf="/etc/X11/xorg.conf.d/10-optimus-manager.conf" 223 | 224 | echo "=== optimus-manager x11 conf ===" 225 | 226 | if [[ -f "${conf}" ]]; then 227 | cat "${conf}" 228 | else 229 | echo "not generated" 230 | fi 231 | 232 | echo 233 | } 234 | 235 | 236 | xrandrInfo () { 237 | echo "=== xrandr providers ===" 238 | xrandr --listproviders || true 239 | echo 240 | } 241 | 242 | 243 | checkDepends 244 | mainFunction 245 | -------------------------------------------------------------------------------- /systemd/optimus-manager.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Optimus Manager Commands Daemon 3 | Before=display-manager.service 4 | 5 | [Service] 6 | ExecStartPre=-/usr/bin/python3 -u -m optimus_manager.hooks.pre_daemon_start 7 | ExecStartPre=-/usr/bin/python3 -u -m optimus_manager.hooks.pre_xorg_start 8 | ExecStart=/usr/bin/python3 -u -m optimus_manager.daemon 9 | ExecStopPost=/usr/bin/python3 -u -m optimus_manager.hooks.post_daemon_stop 10 | 11 | [Install] 12 | WantedBy=graphical.target 13 | -------------------------------------------------------------------------------- /systemd/suspend/optimus-manager.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | import sys 3 | from optimus_manager.hooks import pre_suspend, post_resume 4 | 5 | 6 | def main(): 7 | try: 8 | state = sys.argv[1] # pre or post 9 | _mode = sys.argv[2] # suspend, hibernate or hybrid-sleep 10 | 11 | except IndexError: 12 | print("Not enough arguments") 13 | sys.exit(1) 14 | 15 | if state == "pre": 16 | pre_suspend.main() 17 | 18 | elif state == "post": 19 | post_resume.main() 20 | 21 | else: 22 | print(f"Invalid first argument: {state}") 23 | sys.exit(1) 24 | 25 | 26 | if __name__ == "__main__": 27 | main() 28 | --------------------------------------------------------------------------------