ZrnSelectiveSuspend Alpha
Install curl -fsSLO https://github.com/z3r0n3br4instorm/ZrnSelectiveSuspend/releases/latest/download/zss-installer.run && sh zss-installer.run

ZrnSelectiveSuspend (ZSS) takes the graphics card out of a running Linux desktop and gives it back with no reboot and no new session: the kernel, the desktop and its programs stay up throughout, even if the card loses power without warning. Programs on the card move to the other GPU while they draw, and back when it is switched on.

Alpha. Built for any GPU and driver, proven so far on one laptop, on X11. Support for more GPUs, drivers, distributions and Wayland will come in future releases. Will it work here? says which parts work anywhere today.

Vulkan
1.1Programs that need 1.2 or later don't start under ZSS yet
OpenGL
3.2 · ES 3.1Through Mesa's Zink, on the tested pair of GPUs
Session
X11Wayland compositors are recognised but untested
  • Moves running Vulkan and OpenGL programs between GPUs mid-draw
  • Powers the discrete GPU off and on without crashing the X session
  • Lends the GPU to a virtual machine with no reboot, and takes it back
  • Recovers a GPU that loses power mid-frame, without a reboot
  • Chromium, Electron and Unity games keep drawing while they move
  • Switches off on battery and back on at the charger, programs moved
A MacBook Pro 15-inch from 2012, seen from above with the lid open
Tested onMacBook Pro 15-inch, Mid 2012
  • Arch Linux
  • X11 (Xorg)
  • NVIDIA 470
  • Vulkan 1.1
  • OpenGL 3.2
Model
MacBookPro9,1
Processor
Intel Core i7-3615QM, 4 cores, Ivy Bridge, VT-d
Memory
16 GB
Integrated GPU
Intel HD Graphics 4000 (i915, Mesa hasvk)
Discrete GPU
NVIDIA GeForce GT 650M Mac Edition, 512 MB, Kepler GK107
GPU driver
NVIDIA 470.256.02 with the ZSS patch, revision 6
Display switch
Apple gmux, classic (v1.9.35)
Thunderbolt
Intel CV82524 “Lightridge”
Panel
15.4-inch, 1440 × 900
System
Arch Linux, kernel 7.2, X11

How ZSS runs on it

X runs on the Intel GPU alone. Programs draw on the NVIDIA card through ZSS_AirLock, and each frame is copied to the Intel GPU in order.

zssctl off moves them to Intel, suspends the patched driver and cuts power through the gmux.

For a VM, the card and its IOMMU group go to vfio-pci, and come back by a power cycle.

Start

Demos

Recorded on the MacBook Pro, 9 October 2026. No cuts except where noted.

Switching the GT 650M off and on under a running X session

Top left, intel_gpu_top on the Intel GPU; bottom left, nvidia-smi every second. The cube is vkcube started with zss-run, drawing on the NVIDIA card.

zssctl off moves the cube and Teams to the Intel GPU, suspends the driver and cuts the card's power through the gmux (0.66 s). nvidia-smi can't find the card and lspci -x reads all ff: no power. zssctl on brings it back in about a second and the programs move home. The cube never stops.

Lending the card to a virtual machine and taking it back, without a reboot

zssctl lend moves the cube off the card and hands it, with its audio function and the Thunderbolt controller in its isolation group, to vfio-pci (1.1 s). A CachyOS live image then boots in QEMU with the card passed through; the boot is sped up. Inside the guest, lspci shows the GT 650M and the guest's nouveau reads its chip ID. After the guest shuts down, zssctl reclaim power-cycles the card and gives it back to the host's driver (4.6 s), and the cube moves back.

The guest's driver stops after reading the chip: the Mac Edition card carries no video BIOS of its own, so nouveau finds none. Giving QEMU a copy of it (romfile=) is the next step. One failed launch, a permissions error on /dev/vfio, is cut out.

Start

What it does

Three things a running desktop normally can't do with its graphics card.

Power off

Switch the discrete GPU off while X and your programs keep running. The card draws 0 W until you switch it back on.

Move

Move Vulkan programs, OpenGL programs (through Zink), browsers, Electron apps and Unity games from one GPU to another while they draw, and back again.

Lend

Hand the card to a virtual machine through vfio-pci and take it back afterwards. The host's programs are moved away and then returned.

Also

  • Survives a card that vanishes, even mid-frame. A card that loses power without warning is noticed within about a tenth of a second, and the desktop carries on. Programs drawing on it are rebuilt on the other GPU. zssctl on brings the card back without a reboot: if its driver was frozen in time it resumes, and if the driver saw the loss the card is taken off the bus and found again, the way NVIDIA's driver handles an unplugged eGPU.
  • Rebuilds programs after a loss. Programs under ZSS_AirLock whose GPU disappears are rebuilt from memory on another GPU, or parked until one is available.
  • Follows the charger. Unplug it and the card switches off with its programs moved; plug it back in and they return.
  • Idle timer. An unused card can be switched off after a set time and comes back when something asks for it.
  • Hides a switched-off card from programs started meanwhile, as if it had been unplugged, so that nothing new wakes it.
  • Freezes what it can't move. A program that uses the card but wasn't started under ZSS is paused while the card is off and resumed afterwards. The report names it.
  • Tester kit. A report of what any machine has for ZSS, with the moving tests, ready to email.

What it doesn't do

  • It doesn't start or configure virtual machines. It prepares the host side, and any passthrough setup works after that.
  • It never edits your bootloader configuration. Switching the IOMMU on, if you want lending, is your step.
  • It can't move a program that wasn't started under ZSS_AirLock. Such a program is frozen, or it blocks the operation.
  • After a card lost mid-frame comes back, the programs that were moved off it stay on the other GPU until you restart them, programs that present through NVIDIA's own driver (not through ZSS_AirLock) need a reboot, and the card can't be switched off or lent again until the machine restarts. See When a card is lost unexpectedly.

Start

Installing

One command, or look first with --check.

Needs

  • sudo rights.
  • glibc 2.34 or later: Debian 12, Ubuntu 22.04 and newer, Fedora, openSUSE Tumbleweed, Arch Linux (see Releases). Otherwise build from source.
  • For the driver patch: NVIDIA 470.256.02, DKMS and headers for the running kernel.
  • For ZSS_Interceptor: DKMS and headers for the running kernel.

Assumes

  • An X11 session. Wayland compositors are recognised but untested.
  • No other tool switches the discrete GPU's power (bbswitch, optimus-manager, envycontrol and the like).
  • You can reach a text console (Ctrl+Alt+F2) if the desktop stops answering.

Watch out

  • The driver patch rebuilds the NVIDIA module. The stock modules are kept and put back at boot if the patched one doesn't load, but save your work first.
  • If the GPU driver is in the initial ramdisk, it is regenerated. The installer says so before it does.
  • Nothing is powered off during installation. --check changes nothing at all.
Read this firstZSS cuts power to hardware and can patch a kernel driver. The installer asks before it patches the GPU driver and before it builds the kernel module. It never touches the bootloader. You use it at your own risk; it comes with no warranty.

In one line

curl -fsSLO https://github.com/z3r0n3br4instorm/ZrnSelectiveSuspend/releases/latest/download/zss-installer.run && sh zss-installer.run

This downloads the latest release's installer into the current folder and runs it. The installer is a shell script with a compressed archive attached. Run, it does four things:

  1. Unpacks itself into a temporary folder and asks for your password (through sudo).
  2. Installs the daemon, the control tool and the launcher, creates the zss group and adds you to it, and sets up the zssd service.
  3. If your GPU driver is one the patch has been validated against, offers to patch it. Then it offers ZSS_Interceptor, the kernel module. Both go through DKMS, so they survive kernel updates.
  4. Records what your machine has for ZSS, together with its own log, in ~/.zss/debug.log. If something failed, it also saves a full report and offers to open your mail program to send it.

Arguments after sh zss-installer.run go to the installer, for example sh zss-installer.run --vm-passthrough. Log in again afterwards so your new group membership takes effect.

Look before you install

sh zss-installer.run --check

This says what the machine supports and changes nothing. It covers the GPUs and their drivers, which power backend would be used, whether the driver patch applies, the kernel module, and (for lending) whether the IOMMU is on and whether the card shares its isolation group with other devices.

Installer options

OptionEffect
--checkReport what this machine supports. Changes nothing, needs no password.
--patch-driverApply the NVIDIA wake-on-touch patch without asking.
--no-patch-driverNever touch the GPU driver.
--kernel-moduleBuild and install ZSS_Interceptor without asking.
--no-kernel-moduleDon't install the kernel module. The daemon then does a narrower version of its work from user space (NVIDIA and gmux only).
--vm-passthroughAlso prepare the GPU for lending: a udev rule keeps the display server and the login manager off the card's display nodes.
--no-startInstall everything but leave the service alone (when a reboot is next anyway).
--build DIRFrom source only: the Meson build folder (default ./build).
--destdir DIRFrom source only: install into a staging root. Skips everything that acts on the running system.

What gets installed

WhatWhere
Daemon, control tool, launcherzssd, zssctl, zss-run under /usr/local
ZSS_AirLocklibzss_airlock.so and its Vulkan driver manifest, used only by programs started under it
Servicezssd.service, settings in /etc/zss/zssd.conf
Groupzss: members may switch GPUs off and on
Helperszss-nvidia-patch (patch status, apply, remove), zss-power-event (charger events)
Driver patch (optional)Through DKMS, with a pacman hook that re-applies it after a driver update, and zss-nvidia-check.service, which restores the stock modules at boot if the patched driver doesn't load
Kernel module (optional)zss.ko through DKMS. Loaded when zssd starts, never from the initial ramdisk, so it can't stop the machine from booting
Lending rule (optional)/etc/udev/rules.d/72-zss-lend-seat.rules

If no GPU could be detected for /etc/zss/zssd.conf, the installer says so: set gpu = there and run sudo systemctl enable --now zssd.

After installing

zssctl status                  # the managed GPU, its state, wake support, who is using it
zss-run vkcube                 # a program on the dedicated GPU, under ZSS_AirLock
zssctl off 0000:01:00.0        # move it away and switch the card off
zssctl on 0000:01:00.0         # switch it on and bring the program back

Find your card's address with lspci -D | grep -i -E 'vga|3d'.

The tester kit

curl -fsSLO https://github.com/z3r0n3br4instorm/ZrnSelectiveSuspend/releases/latest/download/zss-tester.run && sh zss-tester.run

It changes nothing and installs nothing. See Helping test it.

Releases

Every push to the main branch builds, tests and publishes a release on GitHub with two downloads: zss-installer-VERSION.run and zss-tester-VERSION.run. zss-installer.run and zss-tester.run are the same files under names that never change, which is what the one-liners use. Versions are MAJOR.MINOR.PATCH; the patch number counts up with each release.

Both files are built on Ubuntu 22.04 and tested on Arch Linux, and need glibc 2.34 or later. Install, --check, uninstall and the tester kit were tried on Debian 12, Ubuntu 22.04 and 24.04, Fedora 44 and openSUSE Tumbleweed. On an older system, build from source.

Removing it

sh zss-installer.run --unpack-only ~/zss-installer
sudo ~/zss-installer/packaging/uninstall.sh            # --purge also drops the settings and the group
zss-nvidia-patch remove                                # if you patched the driver: puts the stock driver back

From a source checkout, sudo ./packaging/uninstall.sh. On Arch, packaging/arch/ builds a package instead, which pacman then removes.

Start

Will it work here?

Written for any GPU; proven on one machine. Here is which part is which.

The parts that move programs are vendor-neutral and work with any Vulkan driver. The parts that cut power are tied to hardware: today, power-off under a running desktop needs an Apple laptop with a classic gmux and the patched NVIDIA 470 driver. On other machines the installer says so and installs program migration only.

PartWorks withTested on
Moving programs between GPUs (ZSS_AirLock)Any Vulkan driver: NVIDIA, Mesa (Intel, AMD, nouveau), softwareNVIDIA 470, Intel hasvk, llvmpipe
Moving OpenGL programs (through Mesa's Zink)Any Vulkan driver. ZSS_AirLock supplies dynamic rendering and VK_KHR_maintenance5 where a driver lacks themNVIDIA 470, Intel hasvk (OpenGL 3.2)
Showing frames from one GPU on a screen driven by another, in orderDrivers that don't hand frames across themselves (NVIDIA proprietary, card not in X)NVIDIA 470 to Intel
Lending to a virtual machine and taking it backAny PCI GPU behind an IOMMU, with ZSS_Interceptor; the display server must not hold the cardMacBookPro9,1 with a QEMU guest; QEMU with an emulated IOMMU
Recovering programs when a GPU vanishesAny Vulkan driverQEMU, by pulling a virtual card
Daemon, zssctl, rules, freezing, idle timerAny GPUHost tests and QEMU
Quiescing the driver before a power cut (quiesce=pm)Any driver that survives a laptop suspend: amdgpu, i915, xe, nouveau, …QEMU's bochs driver only
Saving and restoring PCI state, noticing a lost cardAny PCI GPUQEMU; MacBookPro9,1
Cutting power: Apple classic gmuxApple laptops with a classic gmuxMacBookPro9,1
Cutting power: firmware power resources (acpi)Most hybrid laptops since about 2015Nowhere yet: written, never run
Cutting power: PCIe hot-plug slotAny driver, on a hot-plug slotQEMU only
Power-off under a running display serverNVIDIA 470.256.02 with the patch onlyMacBookPro9,1
Recovering card and driver after a sudden power loss, card idleNVIDIA 470.256.02 with the patch; drivers with PCI error handlers in principleMacBookPro9,1
The same, with a program rendering on the cardNVIDIA 470.256.02 with patch revision 6 (the card is taken off the bus and found again, as an unplugged eGPU)MacBookPro9,1, card out of X
Hiding a switched-off GPU from new programsDevice nodes: any driver. Loader and /proc files: NVIDIA onlyMacBookPro9,1

What it would take to cover more

  • amdgpu, i915/xe, nouveau. ZSS_Interceptor already runs any driver's own sleep and wake code, which also re-runs the card's video BIOS when power returns. What's missing is a trial on real hardware for each driver.
  • Another laptop. The acpi backend should cover most of them and needs someone with such a machine to try it. Other platforms need a backend of their own: three functions (probe, power off, power on).
  • Other NVIDIA versions. The patch has to be checked against each one and added to patches/validated-versions.

The quickest way to help is the tester kit: its report says exactly which of these rows your machine falls under.

Use

Running programs under ZSS

Only programs started under ZSS_AirLock can be moved.

Needs

  • zssd running: zssctl status answers.
  • A program that needs Vulkan 1.1 or less, or OpenGL 3.2 or less (with --gl).
  • A Vulkan driver for every GPU it may move to (Mesa for Intel and AMD).

Assumes

  • The program is started by zss-run or the launch router. A program that is already running can't be taken over.
  • The other GPU has enough free memory for the program's textures and buffers.
  • The session is X11, so that frames drawn on one GPU can be shown by the other.

Watch out

  • A program that uses the GPU but wasn't started under ZSS can't be moved: switching the card off freezes it.
  • Programs started before the daemon was restarted aren't known to it. Restart them before relying on a move.

ZSS_AirLock is a Vulkan driver that sits in front of the real ones. A program has to be started under it to be movable. zss-run does that, and starts the program on the dedicated GPU.

zss-run vkcube                     # a Vulkan program, on the dedicated GPU
zss-run --gl glxgears              # an OpenGL program, through Zink
zss-run --on 0000:00:02.0 vkcube   # start on another GPU (a PCI address, part of a name, or "any")
zss-run chromium                   # Chromium and Electron apps get their Vulkan switches automatically
zss-run --print code .             # show what would run, run nothing

Vulkan programs

Programs see a Vulkan 1.1 device, with the extensions ZSS_AirLock can carry across a move. Programs that need Vulkan 1.2 or later don't start under it yet; run those without zss-run.

What a program is offered is what every GPU it may move to has, so that nothing it turns on can hold it to one card. On the reference laptop that withholds seven features of the NVIDIA card. ZSS_PROFILE=native offers each GPU's own features instead; a program that uses the difference is then parked rather than moved.

OpenGL programs

zss-run --gl runs the program on Mesa's Zink, which draws OpenGL through Vulkan. The program is then under ZSS_AirLock like any Vulkan program and can be moved. On the reference laptop's two GPUs that gives OpenGL 3.2 and OpenGL ES 3.1.

Zink needs two things the NVIDIA 470 driver lacks: dynamic rendering and VK_KHR_maintenance5. ZSS_AirLock provides both itself on such a driver.

Browsers and Electron apps

zss-run recognises a program built on Chromium by the runtime files beside its binary, following launcher scripts to find it. It then adds two switches:

  • --use-angle=vulkan makes ANGLE, which gives Chromium its OpenGL ES, draw through Vulkan.
  • --enable-features=Vulkan makes Chromium's compositor draw and present through Vulkan as well.

Without them, Chromium draws through OpenGL, creates no Vulkan device, and isn't movable. --plain leaves the arguments alone. Recognised on the reference laptop: chromium, helium-browser, code (VS Code) and teams-for-linux. Chromium was moved between the NVIDIA and Intel GPUs while drawing, and kept the same GPU process throughout.

Games

  • Unity games use OpenGL on Linux unless told otherwise. Start them with zss-run ./Game.x86_64 -force-vulkan, or under OpenGL with zss-run --gl ./Game.x86_64. IFSCL runs and moves under both renderers.
  • Steam: in the game's Properties → Launch options, write zss-run %command% for a Vulkan game or zss-run --gl %command% for an OpenGL one. A game that Steam runs inside its runtime container may not see ZSS_AirLock; if ZSS_DEBUG=1 prints nothing, start the game's own binary with zss-run instead.
  • Proton and DXVK need Vulkan 1.3, which ZSS_AirLock doesn't offer yet.
  • Memory. A program can only move to a GPU with room for it. A game that fills the NVIDIA card's memory can fail to move to the Intel GPU, which has about 1.5 GB to share.

Everything you start, without typing zss-run

ZrnLaunchRouter is a separate, optional project. A small library, preloaded into the desktop session, looks at every program as it starts, whether from the menu, the runner, the dock, the file manager or a terminal. It gives it what zss-run would give it:

ProgramRecognised byWhat it gets
Built on Chromiumicudtl.dat and a V8 snapshot beside the binary, and a binary that knows --use-angleThe ZSS environment and the two Vulkan switches
Uses VulkanLinks or names libvulkan.so.1The ZSS environment
Unity game with VulkanA <name>_Data folder beside the binaryThe ZSS environment and -force-vulkan
Anything elseNothing
zrn-launch-router on                     # from the next login
zrn-launch-router check helium-browser   # what it would do with a program
zrn-launch-router log                    # what was routed, and why
zrn-launch-router off

Chromium's helper processes, and anything already under ZSS, are left alone. If ZSS isn't installed, or anything is in doubt, the program starts exactly as asked.

Use

Switching the card off and on

Programs move first; then the driver sleeps; then the power goes.

Needs

  • A power backend for your machine: an Apple laptop with a classic gmux today. The acpi backend is untested.
  • With a display server running: the patched NVIDIA 470 driver. Without the patch, zssctl off is refused while the display server holds the card.
  • ZSS_Interceptor loaded (recommended): zssctl status shows backend=zss-kmod.

Assumes

  • Your screen is driven by the other GPU. Never switch off the card your desktop is shown on.
  • Every program on the card was started under ZSS, or can safely be paused until zssctl on.
  • Nothing else is changing the card's power or driver at the same time.

Watch out

  • Check zssctl status before your first off. Every process it lists that isn't under ZSS_AirLock, a recognised display server or a stop_services entry is frozen until zssctl on.
  • Recognised display servers: Xorg, Xwayland, GNOME Shell, KWin, Sway, Weston, Hyprland, labwc, Wayfire, COSMIC, Mutter, River, Niri. A compositor or window manager missing from this list that holds the card is frozen, and the desktop stops answering: switch to a text console and run zssctl on.
  • Run zssctl off from a terminal that doesn't use the card, or with --console. It refuses to freeze the terminal it runs in.
zssctl status                 # state, wake support, who is using the GPU
zssctl off 0000:01:00.0       # move programs away, suspend the driver, cut power
zssctl on 0000:01:00.0        # power on and bring the programs back
zssctl on 0000:01:00.0 --stay # power on, leave the programs where they are

Each step is printed as it happens:

[ZrnSelectiveSuspend] Suspending device: 0000:01:00.0  NVIDIA Corporation GK107M [GeForce GT 650M Mac Edition]
[ZrnSelectiveSuspend]   driver nvidia, power through zss-kmod, wake on demand: yes
[ZrnSelectiveSuspend]   [1/7] In use by: Xorg (display server, idle), vkcube (application, will be moved)
[ZrnSelectiveSuspend]   [2/7] Applications: 1 moved to 0000:00:02.0, 0 parked
[ZrnSelectiveSuspend]   [3/7] Services stopped: nvidia-persistenced; processes frozen: none
[ZrnSelectiveSuspend]         Hidden from new programs (17): /dev/nvidia0, /dev/dri/card2, ...
[ZrnSelectiveSuspend]   [4/7] PCI state: saved and restored in the kernel (zss module)
[ZrnSelectiveSuspend]   [5/7] Driver nvidia suspended (0.13 s)
[ZrnSelectiveSuspend]   [6/7] Power cut through zss-kmod (0.03 s); the device has left the bus
[ZrnSelectiveSuspend]   [7/7] Watching for wake requests
[ZrnSelectiveSuspend] Device 0000:01:00.0 is powered off (0.43 s).

A card you switch off stays off

  • Programs you start meanwhile don't see it, as if it had been unplugged. Vulkan and OpenGL programs run on the other GPU, and nvidia-smi says it can't reach the device.
  • If the display server needs the card for a moment, it gets it, and the card switches off again a second or two later. zssctl status counts these as served=.
  • If the card has started driving a display by then, it stays on.
  • hide_while_off = no in the settings turns hiding off. A program that then reaches the driver waits until zssctl on; zssctl status shows it as waiting=.

A card switched off by the idle timer is different: it isn't hidden, and it comes back for anyone who asks.

Is it really off?

lspci keeps listing the card, because the kernel keeps its entry for a card that can't be removed. Ask the card itself:

lspci -x -s 01:00.0 | head -3     # all "ff": no power. Real bytes: it is on

Programs that can't be moved

A program that uses the card but wasn't started under ZSS_AirLock (a monitor such as btop, say) can't be moved. zssctl off freezes it and zssctl on lets it carry on; the report names it.

  • Never frozen: PID 1, systemd-logind, elogind, seatd, the recognised display servers, and the terminal running zssctl off. Your login manager is not on that list.
  • zssctl off --console shows the report on a text console and keeps the screen there while the card is off. Press a key to power it on.

Switching off when idle

Set idle_timeout (seconds) in /etc/zss/zssd.conf. A card that drives a display is never powered off, and the timer never freezes anything: a program outside ZSS_AirLock keeps the card on.

A monitor plugged in while the card is off isn't noticed until something asks about displays or you run zssctl on.

Use

Moving programs by hand

Move without touching the power.

Needs

  • zssd running, and programs started under ZSS_AirLock.
  • A second GPU, or allow_software = yes for the CPU renderer.

Assumes

  • The target GPU has enough free memory.
  • Nothing about the card's power changes: detach and attach only move programs.

Watch out

  • A program that can't go anywhere suitable is parked: its window stops updating until zssctl resume PID or until its GPU comes back.
  • --to software runs programs on the CPU. It works, slowly; a game becomes a slide show.
  • Both are refused while the card is lent.
zssctl detach 0000:01:00.0                  # move every program off this GPU
zssctl detach 0000:01:00.0 --to 0000:00:02.0  # ... to a chosen GPU
zssctl detach 0000:01:00.0 --to software    # ... to the CPU renderer (if allowed)
zssctl attach 0000:01:00.0                  # bring back every program that started there
zssctl resume PID                           # resume a parked program on a suitable GPU
zssctl monitor                              # follow the daemon's events

A parked program keeps its state while it waits. It is only resumed on the CPU renderer if allow_software = yes.

Trying it without the service

A private daemon with the dry-run backend really moves programs between GPUs but never touches the card's power or driver. This is how the moving tests run.

# terminal 1
zssd --socket "$XDG_RUNTIME_DIR/zss.sock" --gpu 0000:01:00.0=dry-run --allow-software
# terminal 2
export ZSS_SOCKET="$XDG_RUNTIME_DIR/zss.sock"
zss-run vkcube &
zssctl detach 0000:01:00.0
zssctl attach 0000:01:00.0

Use

On battery

The card follows the charger.

Needs

  • Everything switching off needs.
  • A power manager that runs zss-power-event on charger changes (zrn_perfd, or a udev rule of your own).

Assumes

  • The same as a manual off: the screen is on the other GPU, and everything on the card was started under ZSS.
  • Your power manager doesn't pause background programs while /run/zss/moving exists.

Watch out

  • It switches off without you watching, with the same freezing rules as a manual off. Before relying on it, open your usual programs and check zssctl status for anything that would be frozen.
  • A card that is lent stays lent: the battery event's off is refused.
zss-power-event battery     # switch the dedicated GPU off, moving its programs
zss-power-event ac          # switch it back on and return them
zss-power-event battery --dry-run   # show the progress dialog, switch nothing
  • Progress (“Moving application 2 of 3”) appears in zrn-warning-dialog when that is installed.
  • ac only switches the card back on if it was the battery event that switched it off. A card switched off by hand stays off.
  • The performance manager zrn_perfd calls it on every charger change once it is installed at /usr/local/sbin/zss-power-event. Any other power manager can call it the same way.
  • While programs are being moved, the daemon creates /run/zss/moving. A power manager that pauses background programs should leave them running while that file exists: a paused program can't answer the request to move.

Use

Lending to a virtual machine

GPU passthrough without a reboot or a log-out.

Needs

  • The IOMMU switched on (intel_iommu=on or amd_iommu=on) and vfio-pci in the kernel.
  • ZSS_Interceptor managing the card (backend=zss-kmod). It resets the card when it comes back.
  • The display server and login manager kept off the card: install with --vm-passthrough, then reboot.
  • The card switched on: zssctl on PCI --stay first if it is off.

Assumes

  • The card doesn't drive your screen, and the host has another display device.
  • No program outside ZSS has the card open (nvidia-smi, btop, a CUDA job). Close them first.
  • You trust the guest, if your machine has no interrupt remapping.
  • The host stays awake while the card is lent. Suspend with a lent card is untested.

Watch out

  • Run zssctl lend --check and read every line. Every obstacle it reports is something that would otherwise break the lend or the session.
  • --with-group takes the host drivers away from every device in the card's isolation group until reclaim. If that includes a USB controller, network card or disk controller, your keyboard, network or disk goes with it.
  • Lending is refused, not forced, while the display server holds the card or its driver won't let go. Don't work around a refusal by unbinding drivers by hand: that is what hangs X.
  • With libvirt, use managed='no'. Untested under Wayland.

ZSS does the host's side. It moves programs off the card, gives the card to vfio-pci, and leaves it alone while a guest has it. On the way back it resets the card with a power cycle and returns it to the host's driver, with its programs.

zssctl lend --check 0000:01:00.0     # what stands in the way, and what would be affected; changes nothing
zssctl lend 0000:01:00.0             # programs moved away, card handed to vfio-pci
zssctl status                        # state=lent, and the process holding the card
zssctl reclaim 0000:01:00.0          # power cycle, host driver back, programs returned
zssctl reclaim 0000:01:00.0 --stay   # the same, leaving programs where they are

zssctl lend --check checks everything under Needs above at once and says, for each obstacle, what would remove it. --json gives the same as one document. sh zss-installer.run --vm-passthrough adds the udev rule that keeps X and the login manager off the card's display nodes (they open every display node of their seat at start-up, even for a card they don't use).

Shared isolation groups

--with-group hands over the other devices in the card's isolation group with it. They have no host driver while the card is lent, and get it back on reclaim. On the reference laptop that is the Thunderbolt controller, so there's no Thunderbolt on the host while the card is lent. Without the option, such a device is an obstacle.

What lending does

  1. Moves every program under ZSS_AirLock off the card. A program that has the card open and wasn't started under ZSS blocks the lend: freezing it wouldn't help, because it would still hold the card.
  2. Unloads the kernel modules stacked on the card's driver (for NVIDIA: nvidia_uvm, nvidia_drm, nvidia_modeset). It waits up to ten seconds for their users to go, then requires the driver to have no users left. A driver told to let go of a card still in use waits forever, so instead the lend is refused and undone.
  3. Unbinds the card's functions from their drivers, with a 15-second limit so that a stuck driver can't take the daemon with it, and binds them to vfio-pci.
  4. Marks the card lent. ZSS no longer watches, wakes or powers it, and refuses off, on, detach and attach for it.

Reclaim is refused while a virtual machine holds the card; ZSS never stops a virtual machine. A card that is off is switched on first: zssctl on PCI --stay, then zssctl lend PCI.

Starting a guest

Once the card is lent, any passthrough setup works. With QEMU, for a card whose functions are 01:00.0 and 01:00.1:

qemu-system-x86_64 -machine q35,accel=kvm -cpu host -m 4G \
  -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2-ovmf/x64/OVMF_CODE.4m.fd \
  -device vfio-pci,host=01:00.0,multifunction=on -device vfio-pci,host=01:00.1 \
  -display gtk ...

With libvirt, add the two functions as host devices with managed='no': ZSS has already bound them, and libvirt mustn't rebind them itself.

On the reference laptop

  • IOMMU. A boot entry of its own adds intel_iommu=on iommu=pt. It isn't the default; it's chosen at the boot menu.
  • X server. The NVIDIA card is out of the X configuration altogether, and the udev rule puts its display nodes on a seat of their own. Frames drawn on it reach the screen through ZSS_AirLock's presenter. The cost: no PRIME offload through X, and nothing on the outputs wired to the NVIDIA card (the external port) for now.
  • Interrupts. The firmware's interrupt-remapping table is broken (“ioapic 2 has no mapping iommu”), so vfio refuses a group until told to allow unsafe interrupts. For one run: echo Y | sudo tee /sys/module/vfio_iommu_type1/parameters/allow_unsafe_interrupts, and N afterwards. This is acceptable for a guest you trust, not for one you don't.
  • Display. Arch's qemu-base has no window; install qemu-ui-gtk for -display gtk. The “Mac Edition” card has no option ROM, so QEMU warns and the guest's boot display is QEMU's emulated card.
  • Result, 9 October 2026: lent in 2.9 s, used by a QEMU guest running a CachyOS live image, reclaimed in 11.2 s after the guest shut down, with the desktop and its programs running throughout.

Not done yet

  • Lending a card straight from “off”.
  • Taking the card back automatically when the virtual machine exits (a libvirt hook could call zssctl reclaim).
  • Charger events while lent: the power-off they ask for is refused, so the card simply stays lent.

Use

When a card is lost unexpectedly

A card that loses power without being switched off first.

Needs

  • ZSS_Interceptor loaded, to notice the loss within a tenth of a second.
  • The patched NVIDIA driver, revision 6, to bring the card back whether or not the driver saw the loss. Other drivers need PCI error handlers.
  • The card out of the X configuration (install with --vm-passthrough), so that the display server never waits on it.

Assumes

  • Nothing about what was running: an idle card and a card being rendered on both come back without a reboot.
  • loss_while_busy is left at leave unless you know you want otherwise.

Watch out

  • Until zssctl on, the card's device files are hidden: a monitor such as nvtop or nvidia-smi that reached the driver that had given the card up could take the desktop with it.
  • Programs that were moved off the card stay on the other GPU after it comes back; restart them to use the card. Programs that present through NVIDIA's own driver need a reboot.
  • After a card the driver gave up comes back, it can't be switched off or lent again until the machine restarts: NVIDIA's display modules still hold the old card, and suspending the driver through them crashed the kernel. zssctl off and lend refuse and say so.
  • loss_while_busy = freeze stops the display server until the card is back. Don't set it on a machine you use.
  • If zssctl on refuses, read the reason before trying again: every refusal stands for something that hung the machine in testing.

A card that stops answering, or leaves the bus unexpectedly, is noticed by ZSS_Interceptor within about a tenth of a second. It is marked disconnected, and its driver is told through the kernel's PCI error-recovery handlers if it has them, or frozen if it is the patched NVIDIA driver. What follows depends on whether the driver noticed before ZSS did. Here is what happened in real power cuts on the reference laptop, with the card out of X:

The card wasThe desktopPrograms on the cardGetting the card back
IdleCarries onNonezssctl on: power, PCI state and driver restored, no reboot
Being rendered onCarries onRebuilt on the Intel GPU in about 3.5 s, nothing lostzssctl on: the card is taken off the bus, powered and found again by the driver, in 4 to 7 s. No reboot
zssctl status                 # state=lost
zssctl on 0000:01:00.0        # resumes a frozen driver, or replugs the card the driver gave up

How a card the driver gave up comes back

A program that is rendering calls into the driver many times in the tenth of a second it takes to notice the loss, so the driver usually finds out first (Xid 79 … GPU has fallen off the bus). From then on NVIDIA's driver won't use the card again, and every ordinary way of taking it off the card waits forever: unbinding it, or unloading the modules stacked on it.

NVIDIA's driver does allow one device to leave with programs still holding it: an eGPU pulled from its Thunderbolt port. Patch revision 6 lets ZSS mark the lost card that way. zssctl on then:

  1. marks the card as unplugged (/proc/driver/nvidia/zss_hold);
  2. takes its functions off the PCI bus while its power is still off, each step with a time limit, so the driver lets go;
  3. switches the power rail on and rescans the bridge above the card;
  4. waits for nvidia to find the card as a new device, and manages it again.

Tried twice on 9 October 2026 with vkcube rendering and btop polling the card: X answered throughout, vkcube moved to Intel with its picture intact, and afterwards nvidia-smi answered and a new program rendered on the card. The card comes back as /dev/nvidia1; the old device number is freed when its last holder closes it.

The loss_while_busy setting

ValueWhat happens when the card vanishes while in use
leave (default)The driver finds out for itself. Programs get errors and are moved, the display server carries on, and zssctl on brings the card back by replugging it (patch revision 6).
refuseThe driver is frozen and turns callers away at once. Programs are moved, the display server isn't held, and the driver can still be given the card back. Needs patch revision 5; not yet proven on hardware.
freezeThe driver is frozen and callers wait: the display server stops.

When the card is idle the driver is frozen if ZSS gets there first, and then it simply resumes. Programs under ZSS_AirLock on a lost card are rebuilt on another GPU from what ZSS_AirLock kept in memory (see ZSS_RETAIN), or parked.

Use

Helping test it

Every other machine teaches us something.

Needs

  • Python 3. For the window: PySide6 or PyQt6.
  • A desktop session, for the moving tests. Without one, the facts are still collected.

Assumes

  • Nothing. It installs nothing and changes nothing; its test daemon runs in dry-run mode, so no card is switched off and no driver is touched.

Watch out

  • A few test windows appear and disappear during the moving tests. Leave them be.
  • Run full test is different: it installs ZSS and switches the discrete card off and on once, with the same freezing rules as zssctl off. Save your work and check Switching the card off first.
curl -fsSLO https://github.com/z3r0n3br4instorm/ZrnSelectiveSuspend/releases/latest/download/zss-tester.run && sh zss-tester.run

With PySide6 or PyQt6 installed and a desktop running, it opens a window. Otherwise it runs in the terminal (--mail opens your mail program at the end). It takes a few minutes, and does three things:

  1. Collects facts: the graphics cards and their drivers, how their power can be switched, the IOMMU, what Vulkan and OpenGL see, the display server, and graphics-related kernel messages.
  2. Runs the moving tests: test programs are moved between your graphics cards and back, and what they draw is compared. It uses a test daemon in dry-run mode.
  3. Saves the report as a folder and a .tar.gz file in your home folder.

It changes nothing, installs nothing and sends nothing by itself. Before anything is saved, your computer's name, your user name and home folder, disk identifiers, serial numbers, network addresses and email addresses are replaced by placeholders.

Send report opens your mail program addressed to the author with a summary filled in. Attach the .tar.gz (its path is copied for you) and send.

The full test (optional)

It builds and installs ZSS with its kernel module (your graphics driver isn't patched), runs the whole test suite, and switches your discrete card off and on once. It asks for your password and shows every command first.

It builds from source, so it is offered when the kit runs from a checkout of the repository rather than from the download:

git clone https://github.com/z3r0n3br4instorm/ZrnSelectiveSuspend
cd ZrnSelectiveSuspend
tester/zss-report-gui                # or tester/zss-report --full-test in a terminal
sudo ./packaging/uninstall.sh        # removes it again afterwards

How it works

The three parts

A user-space shim, a daemon and a kernel shim, around the GPU's own driver.

ZSS architecture: programs draw through ZSS_AirLock onto the real drivers; zssd sequences moves and power through ZSS_Interceptor in the kernel. USER SPACE Programs: Vulkan · OpenGL through Zink · Chromium / Electron · Unity started by zss-run, or by the launch router ZSS_AirLock libzss_airlock.so owns every handle · records command buffers · shadows memory rebuilds a program on another GPU · presents on the screen's GPU NVIDIA Vulkan Mesa: Intel, AMD, … llvmpipe (CPU) zssctlzss-power-event zssd who holds the GPU move · freeze · off · on lend · reclaim socket KERNEL /sys/kernel/zss/…/power ZSS_Interceptor zss.ko quiesce the driver · save / restore PCI state · power backends: gmux, acpi · loss guard · lent GPU driver: nvidia (with the patch) · amdgpu · i915 · xe · nouveau — or vfio-pci while lent
Programs never talk to ZSS directly: they see ZSS_AirLock as their Vulkan driver.
PartWhat it isIts job
ZSS_AirLockUser-space Vulkan driver shim, libzss_airlock.so, started with zss-run. Messages begin [ZSS_AirLock]Keeps enough state to rebuild a program on another GPU, and does so when asked or when the GPU is lost
zssdThe daemon, a system serviceDecides and sequences: who is using the GPU, what to move, freeze or stop; then driver suspend and the power cut, and the reverse; and lending
ZSS_InterceptorThe kernel shim, zss.ko (its file, /sys entries and kernel messages keep the name zss). OptionalThe part that touches hardware: quiesces the driver, saves and restores PCI state, cuts and restores power, watches for a card that goes silent
The NVIDIA patchA small patch to NVIDIA's driver, applied on your machineLets the driver sleep under a running display server and be frozen when its card vanishes

The original design put everything in the kernel and shielded a running driver from a vanishing card by shadowing its registers. What was built instead moves programs off the card first, then powers it off with the driver's own cooperation. That needs far less from the kernel, and it works on real hardware today.

How it works

ZSS_AirLock

The program's Vulkan driver, with the real drivers underneath.

A program started under ZSS_AirLock gets it as its only Vulkan driver. ZSS_AirLock loads the real drivers itself and hands out handles of its own. Because it stands between the program and the GPU for every call, it can take a program's work off one GPU and put it on another without the program noticing.

What it keeps

  • Every object the program creates: devices, buffers, images, pipelines, descriptor sets, swapchains. Each is a handle of ZSS_AirLock's own, backed by an object on the real GPU that can be replaced.
  • Command buffers, recorded in its own form and replayed onto the real GPU, so they can be replayed again on another one.
  • Memory the CPU can see, shadowed, so its contents survive a move.
  • Uploaded textures, kept on disk (or in RAM) so that a program can be rebuilt even after its GPU vanished without warning (ZSS_RETAIN).

A move, step by step

  1. The daemon asks the program, over its control socket, to move off a GPU.
  2. ZSS_AirLock holds the program's calls at the door, waits for the GPU to finish, and reads back what lives only on the GPU: images, buffers, the window's current picture.
  3. It creates each object again on the new GPU, uploads the contents, and replays the recorded command buffers.
  4. It rebuilds the window's swapchain in place, keeping its images' contents, so that a program which redraws only what changed (Chromium does) still shows the whole picture.
  5. It lets the program's calls through again. The program never received an error; it just carries on, on another GPU.

The portable profile

A program is offered Vulkan 1.1 with the features and limits that every GPU it might move to has, including the number of queues. Nothing it enables can then hold it to one card. ZSS_PROFILE=native switches this off.

Standing in for missing features

Some extensions are offered on every GPU and emulated where a driver lacks them. Dynamic rendering is lowered to cached render passes and framebuffers. VK_KHR_maintenance5 is mapped onto older calls. That is what lets Zink, and so OpenGL, run on the NVIDIA 470 driver. Also offered: timeline semaphores, extended dynamic state, robustness2, conditional rendering, custom border colours, line rasterisation, transform feedback, sampler Y′CbCr conversion and more.

What it costs

The same programs measured with and without ZSS_AirLock on the reference laptop, frames per second, median of three:

ProgramWithout ZSS_AirLockUnder ZSS_AirLock
vkcube on Intel1004947
vkcube on NVIDIA, shown on the Intel screenabout 1530861
glxgears through Zink on Intel1175549 to 655
glxgears through Zink on NVIDIA—1077

Two costs were found and removed on 9 October. Frames from the NVIDIA card were held to the refresh rate whatever a program asked for; the presenter now uses the program's own present mode. And every submission copied all of a program's mapped memory to the GPU, about 5 GB a second for glxgears; now the kernel reports which pages were written (userfaultfd write protection with PAGEMAP_SCAN, Linux 6.7 and later) and only those are copied.

Presenting on the screen's GPU

A program may draw on one GPU while another drives the screen. Mesa's drivers hand frames across in order themselves. The NVIDIA proprietary driver only does that when the X server has its card. Without that, ZSS_AirLock does it:

  • It gives the program ordinary images on its own GPU.
  • It keeps the window's real swapchain on a small device of its own on the screen's GPU.
  • At each present it reads the frame back, waits for it, copies it in, and presents it there, in order.

This is what fixed text that flickered back to the previous frame while typing in Chromium-based apps. Moves switch between this and direct presenting. ZSS_PRESENT=direct turns it off; copy forces it on.

Housekeeping

  • Frames for a swapchain the program has already replaced are dropped instead of presented. This stops a hang on NVIDIA when Zink changes the swap interval.
  • When a real driver is closed, ZSS_AirLock closes the render node that NVIDIA's driver leaks, so that a moved program doesn't keep the card busy.
  • ZSS_AirLock is linked so that it can't be unloaded, because Chromium unloads its Vulkan driver between probes.

How it works

The daemon

zssd decides and sequences.

Switching off, in seven steps

  1. Who holds the card. Every process with one of the card's device nodes open is found and classed: display server, program under ZSS_AirLock, guest, system process, other.
  2. Move. Each program under ZSS_AirLock is asked to move to another GPU (default_target, or the best one available). One that can't go anywhere is parked.
  3. Stop and freeze. Services listed in stop_services are stopped; other processes still holding the card are frozen, except PID 1, logind, elogind, seatd, recognised display servers and the terminal running zssctl. The card's device nodes are hidden from new programs.
  4. PCI state is saved, in the kernel if ZSS_Interceptor is loaded.
  5. Driver suspended through its own sleep code.
  6. Power cut through the backend: the gmux on the reference laptop.
  7. Watching for wake requests: a display-server call is served by powering the card for a moment.

Switching on runs the same steps backwards, and returns each program to the GPU it started on unless --stay is given.

Rules it keeps

  • A card driving a display isn't switched off by the timer.
  • PID 1, logind, elogind and seatd are never frozen: they hold the session's device nodes on its behalf.
  • While programs are moving it writes /run/zss/moving, so that power managers don't pause them mid-move.
  • Lending is refused, rather than risked, whenever the driver couldn't let go cleanly.
  • Its state survives a restart: a lent card is still known as lent.

Who may ask

zssctl talks to the daemon over a Unix socket (/run/zss/zssd.sock). Moving, switching and lending are allowed for root, members of the zss group, and the user the daemon runs as. Status is open to everyone. The wire protocol is in docs/protocol.md in the repository.

How it works

ZSS_Interceptor

The kernel shim: the part that touches the hardware.

Loaded, it does nothing until the daemon hands it a device. The daemon drives it through one file per device:

/sys/kernel/zss/0000:01:00.0/power     # off · on · lend · unlend · reclaim
DutyHow
Quiesce the driverRuns the driver's own runtime sleep and wake code (quiesce=pm), which also re-runs the card's firmware after power returns. For NVIDIA, the daemon suspends the driver through /proc/driver/nvidia/suspend.
PCI stateSaved before the cut and restored after, by the PCI core. Bus mastering is switched off before the power goes.
PowerThrough a backend: gmux (Apple, port 0x750, verified), acpi (firmware power resources, written, never run), test. A hot-plug-slot backend lives in the daemon.
Power onPower, a bounded wait for the card to answer, state restore, then the driver's resume. A card with no driver is refused: ZSS doesn't run a card's video BIOS itself.
Loss guardNotices a card that has gone silent or left the bus, marks it disconnected, and tells its driver through PCI error handlers, or freezes the patched NVIDIA driver.
LentHands off entirely while a card is lent. On reclaim it power-cycles the card and restores its PCI state; it refuses while any function still has a driver.
System sleepPauses across suspend and resume of the whole machine.
IOMMUReports whether an IOMMU confines the card's memory access (iommu= in zssctl status). It adds no confinement of its own.

It is built through DKMS and loaded when zssd starts, never from the initial ramdisk. rmmod zss, with the daemon stopped, takes it out. Without it, the daemon does a narrower version of the same from user space (NVIDIA and gmux only), and lending isn't available.

How it works

The NVIDIA patch

Needed only for power-off with NVIDIA's proprietary driver.

NVIDIA's driver isn't built to have its card switched off under a running display server. The patch adds two things:

  • Wake on touch. A caller arriving while the driver is suspended asks for a wake and sleeps, instead of spinning forever. That is how the display server can borrow a switched-off card for a moment.
  • Freeze. The driver can be shut to every caller without anything being asked of the card, for the moment a card vanishes, and resumed when the card is back. Revision 5 can also turn callers away at once instead of making them wait (loss_while_busy = refuse).
  • Unplugged. Revision 6: a card whose driver saw it vanish can be marked the way the driver marks an eGPU pulled from its port, so that it can be taken off the bus with programs still holding it and found again by a rescan. That is what brings a card lost mid-frame back without a reboot.
zss-nvidia-patch status     # driver version, whether it's validated, whether the patch is in
zss-nvidia-patch apply
zss-nvidia-patch remove     # put the stock driver back
  • Validated against 470.256.02 so far (revision 6). The installer offers it only for a validated version.
  • It is applied through DKMS on your machine, to the copy of the driver you installed, so it follows kernel updates. A pacman hook re-applies it after a driver update.
  • The stock modules are kept, and restored at boot if the patched driver doesn't load.
  • ZSS doesn't ship or modify NVIDIA's driver itself. The lines the patch adds are under the MIT licence so that they can be built into NVIDIA's driver.

How it works

Device states

What zssctl status reports for a managed card.

Device states: attached goes to powered-off and back, to lost when the card stops answering and back with zssctl on, and to lent and back with reclaim. attached powered-off 0 W detaching: zssctl off · idle timer · on battery attaching: zssctl on · wake request · charger the display server calls: powered for a moment, then off again lost card stops answering, or leaves the bus zssctl on, or the card answers again lent lend reclaim: power cycle, host drivers back, programs returned card on vfio-pci, owned by a virtual machine; ZSS doesn't watch, wake or power it
Detaching and attaching are the moments in between, while programs are moved.
StateCard powerHost driverPrograms that were on itDisplay server
attachedOnBoundRunning on itMay use it
powered-offOff (0 W)SuspendedMoved to another GPU, or frozenServed on demand (patched NVIDIA driver)
lostUnknownFrozen, or told through its error handlersRebuilt on another GPU, or parkedCarries on
lentOnNone (vfio-pci)Moved to another GPUMustn't hold the card
detaching, attachingChangingChangingBeing moved—

zssctl status also shows counters: wakes (times woken by a request), served and serving (powered briefly for the display server), waiting (programs asleep on the driver), driver_frozen, iommu, and the seconds left on the idle timer.

Reference

zssctl

The control tool. PCI is an address such as 0000:01:00.0.

CommandWhat it does
status [PCI]State of each managed card, its backend, wake support, who is using it, and the counters.
off PCI [--to PCI|software] [--console]Move programs away, stop and freeze the rest, suspend the driver, cut power. --console shows it on a text console and waits for a key to power back on.
on PCI [--stay]Restore power, PCI state and the driver; return programs unless --stay.
detach PCI [--to PCI|software]Move programs off the card; leave its power alone.
attach PCIBring back the programs that started on the card.
lend --check PCI [--with-group] [--json]Say what stands in the way of lending and what would be affected. Changes nothing.
lend PCI [--with-group] [--to PCI|software]Hand the card to a virtual machine through vfio-pci.
reclaim PCI [--stay]Take a lent card back: power cycle, host drivers, programs.
resume PIDResume a parked program on a suitable GPU.
monitorPrint the daemon's events as they happen.

Reference

zss-run

zss-run [options] program [args…]

OptionEffect
(none)Start the program under ZSS_AirLock on the dedicated GPU. The other GPUs aren't listed to it, but it can still be moved to them.
--on GPUStart on another GPU: a PCI address, part of a GPU's name, dedicated, or any.
--glRun an OpenGL program through Mesa's Zink, so that it can be moved too.
--plainDon't add the Chromium switches.
--printShow what would be run, and run nothing.

Reference

Configuration

/etc/zss/zssd.conf. Restart the service after editing.

Every setting can also be given on the daemon's command line as --name-with-dashes.

SettingMeaningDefault
gpuThe card to manage, by PCI address. The backend is detected; force one with PCI=apple-gmux, PCI=pciehp-slot or PCI=dry-run. May be repeated.Set by the installer
idle_timeoutPower an unused card off after this many seconds.Unset: only on request
stop_servicesServices that hold the card only to keep it initialised; stopped before a power-off, started again after.nvidia-persistenced
hide_while_offauto (device nodes and the vendor's files), no, or a comma-separated list of extra paths to hide.auto
default_targetWhere programs go when the card goes off: a PCI address or software.Unset: the daemon picks
allow_softwareLet programs fall back to the CPU renderer when no GPU suits them.no
loss_while_busyleave, refuse or freeze; see When a card is lost unexpectedly.leave
groupMembers of this group may switch cards off and on.zss

Reference

Environment variables

Read by ZSS_AirLock in each program.

VariableEffect
ZSS_SOCKETThe daemon's socket (default /run/zss/zssd.sock).
ZSS_DEBUGLog what is loaded and why a device isn't movable.
ZSS_START_ONThe GPU a program starts on: dedicated, a PCI address, part of a name, or any. zss-run --on sets it.
ZSS_PROFILEportable (default): what every GPU has. native: each GPU's own.
ZSS_PRESENTdirect: every swapchain on the drawing GPU. copy: always through the screen's GPU. Default: through the screen's GPU only where needed.
ZSS_VULKAN1.0 offers Vulkan 1.0 only.
ZSS_ALLOW_SOFTWARELet a parked program resume on the CPU renderer.
ZSS_RETAINWhere uploaded textures are kept to survive a lost GPU: disk (default), ram, off.
ZSS_RETAIN_LIMIT_MB, ZSS_RETAIN_QUEUE_MBSize cap of that store (4096) and of data waiting to be written (256).
ZSS_WRITE_WATCH0: copy a program's mapped memory to the GPU whole at every submission. Default: only the pages written since the last one (Linux 6.7 or later).
ZSS_REAL_DRIVER_FILESColon-separated driver manifests to use instead of the system's.
ZSS_TRAPDebugging: a Vulkan command ZSS_AirLock doesn't provide stops the program with that command's name.
ZSS_BIND_PCITest aid: the CPU renderer poses as the PCI device at that address.
ZSS_TEST_LOSE_AT_SUBMIT, ZSS_TEST_LOSE_AFTER_SUBMITTest aids: behave as if the GPU died at, or just after, that submit.
ZSS_TEST_STUCK_MS, ZSS_LOSS_GRACE_MSTest aids: hold a thread inside ZSS_AirLock during a loss; how long recovery waits for such threads (3000).

Reference

Building from source

Meson and Ninja; the Vulkan headers come with the source.

Needs

  • Meson 1.1 or later, Ninja, a C compiler, Python 3, glslang, and the development headers for xcb, X11 and Wayland.
  • Linux headers, for the kernel module.

Assumes

  • The Vulkan headers come with the source (subprojects/): the build always uses that copy, whatever the system has.

Watch out

  • meson test runs the QEMU tests where QEMU is installed. They run in a virtual machine; nothing on your machine is powered off.
  • Building doesn't install anything. install.sh does.
git clone https://github.com/z3r0n3br4instorm/ZrnSelectiveSuspend
cd ZrnSelectiveSuspend
meson setup build && ninja -C build
meson test -C build                 # host and QEMU tests: nothing on this machine is powered off
./packaging/install.sh --check      # what this machine supports; changes nothing
sudo ./packaging/install.sh         # install

On Arch the build needs base-devel meson ninja python glslang vulkan-icd-loader libxcb libx11 wayland libglvnd, plus linux-headers for the kernel module. packaging/make-bundle.sh VERSION DIR makes the same .run files as a release.

Where things are

src/layer/ZSS_AirLock
src/daemon/zssd, the power backends, lending
src/zssctl/The control tool
kmod/ZSS_Interceptor and its DKMS files
patches/The NVIDIA wake-on-touch patch
packaging/Installer, service units, the patch tool, the Arch package, the bundle maker
tester/The tester kit
tests/Test programs, frame comparison, host, QEMU and hardware harnesses
docs/Wire protocol, hardware results, lending, device loss
openspec/changes/Proposal, design, specs and tasks for each milestone
SPEC.mdThe long-term design, and what became of each part

Reference

Troubleshooting

Problems met on the reference laptop, and what fixes them.

zssctl can't reach zssd

sudo systemctl restart zssd, then journalctl -u zssd -b if it still fails. Programs started before the restart don't register with the new daemon; restart them to make them movable again.

A program started from a terminal isn't moved

Only programs started under ZSS_AirLock can be moved. Start it with zss-run (or zss-run --gl for OpenGL), or turn on ZrnLaunchRouter so that everything you start is routed.

zssctl off says a program still has the device open

That program uses the card but wasn't started under ZSS. Either it is frozen (and named in the report), or, for lending, it blocks the operation. Close it, or start it again with zss-run.

The terminal itself uses the card

The command refuses rather than freeze the terminal you typed into. Run it from a terminal that isn't on the card, or from a text console.

A program doesn't start under zss-run

It probably needs Vulkan 1.2 or later. Run ZSS_DEBUG=1 zss-run … to see why; ZSS_TRAP=1 names a Vulkan command it called that ZSS_AirLock doesn't provide. Run it without zss-run meanwhile.

A program fails to move to the integrated GPU

Usually memory: the target has less, and a game that fills the discrete card can't fit. The program is parked or keeps running where it was; the report says which.

A Chromium or Electron app draws on the CPU

It is missing the two Vulkan switches. zss-run --print app shows whether they are added. Without them it draws through OpenGL and isn't movable.

Text flickers back a frame while typing

Frames from the discrete card weren't reaching the screen in order. ZSS_AirLock now presents through the screen's GPU where needed. If you set ZSS_PRESENT=direct, remove it.

Lending is refused: modules still in use

Something still uses nvidia_drm or its neighbours, often the display server or login manager. Install with --vm-passthrough and reboot, so that they stay off the card's display nodes.

QEMU refuses the group (“no interrupt remapping”)

The firmware's interrupt-remapping table is broken. Allow unsafe interrupts for that run only: see On the reference laptop.

The card was lost while a game was running

zssctl status shows state=lost, and the card's device files are hidden. Run zssctl on: the card is replugged and found again (patch revision 6). The game keeps running on the Intel GPU; restart it to use the card again. A program that draws through NVIDIA's own driver rather than ZSS_AirLock needs a reboot.

Something went wrong during installation

The installer saved a report in ~/.zss/ and offered to send it. ~/.zss/debug.log holds the facts and the installer's own messages either way.

Where to look

zssctl status                   # the daemon's view
journalctl -u zssd -b           # the daemon's log
sudo dmesg | grep -E 'zss|NVRM' # ZSS_Interceptor and the driver
ZSS_DEBUG=1 zss-run program     # [ZSS_AirLock] messages on the terminal
zss-nvidia-patch status         # the driver patch

Reference

Licence

Free software.

ZrnSelectiveSuspend is Copyright © 2026 Zerone Laboratories, released under the GNU General Public License, version 2 only. Every source file names it in an SPDX-License-Identifier line. It comes with no warranty: it cuts power to hardware and patches a kernel driver, and you use it at your own risk.

  • The NVIDIA driver patch: the lines it adds are under the MIT licence, so that they can be built into NVIDIA's driver. The lines of NVIDIA's code it quotes remain NVIDIA's.
  • Vulkan-Headers, from Khronos and included in subprojects/, are under their own licences (Apache-2.0 or MIT).
  • The hash in src/common/zss_hash.c is MurmurHash3 by Austin Appleby, which is in the public domain.