├── README.md ├── metadata.json ├── extension.js └── primaryGpu.js /README.md: -------------------------------------------------------------------------------- 1 | # mutter-primary-gpu 2 | A GNOME Shell extension to override primary GPU selection for Wayland 3 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mutter Primary GPU", 3 | "description": "Override primary GPU selection for Wayland", 4 | "uuid": "mutter-primary-gpu@zaidka.github.io", 5 | "shell-version": ["42"], 6 | "url": "https://github.com/zaidka/mutter-primary-gpu", 7 | "version": 1 8 | } 9 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const currentExtension = imports.misc.extensionUtils.getCurrentExtension(); 2 | const { Indicator } = currentExtension.imports.primaryGpu; 3 | const aggregateMenu = imports.ui.main.panel.statusArea.aggregateMenu; 4 | const sessionMode = imports.ui.main.sessionMode; 5 | 6 | let indicator = null; 7 | 8 | function enable() { 9 | if (indicator) disable(); 10 | const lookup = [aggregateMenu._power.menu, aggregateMenu._powerProfiles.menu]; 11 | 12 | const menuItems = aggregateMenu.menu._getMenuItems(); 13 | let index = 0; 14 | for (let i = 0; i < menuItems.length; i++) { 15 | if (lookup.includes(menuItems[i])) index = i; 16 | } 17 | 18 | indicator = new Indicator(); 19 | aggregateMenu._indicators.add_child(indicator); 20 | aggregateMenu.menu.addMenuItem(indicator.menu, index + 1); 21 | } 22 | 23 | function disable() { 24 | if (indicator) { 25 | aggregateMenu._indicators.remove_child(indicator); 26 | indicator.menu.destroy(); 27 | indicator = null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /primaryGpu.js: -------------------------------------------------------------------------------- 1 | const Gio = imports.gi.Gio; 2 | const GObject = imports.gi.GObject; 3 | const GLib = imports.gi.GLib; 4 | const File = imports.gi.Gio.File; 5 | const PanelMenu = imports.ui.panelMenu; 6 | const PopupMenu = imports.ui.popupMenu; 7 | const ByteArray = imports.byteArray; 8 | const Notification = imports.ui.messageTray.Notification; 9 | const Urgency = imports.ui.messageTray.Urgency; 10 | const SystemNotificationSource = 11 | imports.ui.messageTray.SystemNotificationSource; 12 | const messageTray = imports.ui.main.messageTray; 13 | const Meta = imports.gi.Meta; 14 | 15 | const UDEV_RULE_PATH = "/etc/udev/rules.d/61-mutter-primary-gpu.rules"; 16 | 17 | function exec(command, envVars = {}) { 18 | return new Promise((resolve, reject) => { 19 | const [, strArr] = GLib.shell_parse_argv(command); 20 | const launcher = new Gio.SubprocessLauncher({ 21 | flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE, 22 | }); 23 | 24 | for (const [key, value] of Object.entries(envVars)) { 25 | launcher.setenv(key, value, true); 26 | } 27 | 28 | const proc = launcher.spawnv(strArr); 29 | 30 | proc.communicate_utf8_async(null, null, (proc, res) => { 31 | const [arg, stdout, stderr] = proc.communicate_utf8_finish(res); 32 | if (proc.get_successful()) resolve(stdout); 33 | else reject(new Error(stderr.trim())); 34 | }); 35 | }); 36 | } 37 | 38 | function getActiveGpu() { 39 | const cmd = getPrintRendererPath(); 40 | return exec(cmd); 41 | } 42 | 43 | async function getRenderer(device) { 44 | const file = File.new_for_path(`/dev/dri/${device}`); 45 | if (!file.query_exists(null)) return null; 46 | const property = await exec( 47 | `udevadm info --query=property --property=ID_PATH_TAG /dev/dri/${device}` 48 | ); 49 | const regex = /^ID_PATH_TAG=([a-z0-9_-]+)\n?$/; 50 | const r = regex.exec(property); 51 | if (!r) return null; 52 | return exec(getPrintRendererPath(), { 53 | DRI_PRIME: r[1], 54 | }); 55 | } 56 | 57 | function getPrintRendererPath() { 58 | const knownPaths = [ 59 | "/usr/libexec/gnome-control-center-print-renderer", 60 | "/usr/lib/gnome-control-center-print-renderer", 61 | ]; 62 | 63 | for (const path of knownPaths) { 64 | if (File.new_for_path(path).query_exists(null)) { 65 | return path 66 | } 67 | } 68 | 69 | throw new Error("Unable to find `gnome-control-center-print-renderer` in any known location.") 70 | } 71 | 72 | function getGpus() { 73 | const path = File.new_for_path("/dev/dri/"); 74 | const enumerator = path.enumerate_children("standard::name", 0, null); 75 | 76 | const gpus = new Set(); 77 | 78 | let f; 79 | while ((f = enumerator.next_file(null))) { 80 | const name = f.get_name(); 81 | if (name.startsWith("card")) gpus.add(name); 82 | } 83 | 84 | return gpus; 85 | } 86 | 87 | function getPrimaryGpu() { 88 | const file = File.new_for_path(UDEV_RULE_PATH); 89 | if (!file.query_exists(null)) return null; 90 | const [, contents] = file.load_contents(null); 91 | const c = ByteArray.toString(contents).trim(); 92 | const regex = 93 | /^ENV{DEVNAME}=="\/dev\/dri\/(card[\d+])", TAG\+="mutter-device-preferred-primary"$/; 94 | const r = regex.exec(c); 95 | if (!r) return null; 96 | return r[1]; 97 | } 98 | 99 | function notify(message) { 100 | const source = new SystemNotificationSource(); 101 | messageTray.add(source); 102 | const notification = new Notification(source, "Mutter Primary GPU", message, { 103 | gicon: new Gio.ThemedIcon({ name: "video-display-symbolic" }), 104 | }); 105 | notification.setTransient(true); 106 | source.showNotification(notification); 107 | } 108 | 109 | function notifyError(err) { 110 | logError(err); 111 | const source = new SystemNotificationSource(); 112 | messageTray.add(source); 113 | const notification = new Notification( 114 | source, 115 | "Mutter Primary GPU", 116 | err.message, 117 | { gicon: new Gio.ThemedIcon({ name: "video-display-symbolic" }) } 118 | ); 119 | notification.setTransient(true); 120 | notification.setUrgency(Urgency.CRITICAL); 121 | source.showNotification(notification); 122 | } 123 | 124 | function setPrimaryGpu(primary) { 125 | const commands = []; 126 | 127 | // Only way to remove mutter-device-preferred-primary tag that might have been set previosly 128 | const untagUdevRule = `ENV{DEVNAME}=="/dev/dri/card*", TAG="dummytag"`; 129 | commands.push(`echo ${GLib.shell_quote(untagUdevRule)} > ${UDEV_RULE_PATH}`); 130 | commands.push(`udevadm control --reload-rules`); 131 | commands.push(`udevadm trigger`); 132 | 133 | if (primary) { 134 | // Add mutter-device-preferred-primary tag 135 | const tagUdevRule = `ENV{DEVNAME}=="/dev/dri/${primary}", TAG+="mutter-device-preferred-primary"`; 136 | commands.push(`echo ${GLib.shell_quote(tagUdevRule)} > ${UDEV_RULE_PATH}`); 137 | } else { 138 | commands.push(`rm ${UDEV_RULE_PATH}`); 139 | } 140 | 141 | commands.push(`udevadm control --reload-rules`); 142 | commands.push(`udevadm trigger`); 143 | 144 | exec(`pkexec sh -c ${GLib.shell_quote(commands.join(" && "))}`) 145 | .then(() => { 146 | if (!Meta.is_wayland_compositor()) 147 | notify("Primary GPU selection is only supported on Wayland."); 148 | else if (primary) 149 | notify( 150 | "The selected GPU has been tagged as primary. This change will take effect after the next login." 151 | ); 152 | else 153 | notify( 154 | "The selected GPU is no longer tagged as primary. This change will take effect after the next login." 155 | ); 156 | }) 157 | .catch(notifyError); 158 | } 159 | 160 | var Indicator = GObject.registerClass( 161 | class Indicator extends PanelMenu.SystemIndicator { 162 | _init() { 163 | super._init(); 164 | this._item = new PopupMenu.PopupSubMenuMenuItem("", true); 165 | getActiveGpu() 166 | .then((prm) => { 167 | this._item.label.text = prm; 168 | }) 169 | .catch(logError); 170 | 171 | this._item.icon.icon_name = "video-display-symbolic"; 172 | 173 | this._item.menu.connect("open-state-changed", (_, open) => { 174 | if (open) this._sync(); 175 | }); 176 | 177 | this.menu.addMenuItem(this._item); 178 | 179 | this._sync(); 180 | } 181 | 182 | _sync() { 183 | const gpus = getGpus(); 184 | const primary = getPrimaryGpu(); 185 | if (primary) gpus.add(primary); 186 | const proms = [...gpus].map(async (gpu) => { 187 | const label = await getRenderer(gpu); 188 | const item = new PopupMenu.PopupMenuItem(label ?? `/dev/dri/${gpu}`); 189 | item.setOrnament( 190 | gpu === primary ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NONE 191 | ); 192 | item.connect("activate", () => { 193 | setPrimaryGpu(gpu === primary ? null : gpu); 194 | }); 195 | return item; 196 | }); 197 | 198 | Promise.all(proms) 199 | .then((items) => { 200 | this._item.menu.removeAll(); 201 | for (const item of items) this._item.menu.addMenuItem(item); 202 | }) 203 | .catch(logError); 204 | } 205 | } 206 | ); 207 | --------------------------------------------------------------------------------