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

- 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, Mesahasvk) - 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.
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.
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 onbrings 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
sudorights.- 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.
--checkchanges nothing at all.
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:
- Unpacks itself into a temporary folder and asks for your password (through
sudo). - Installs the daemon, the control tool and the launcher, creates the
zssgroup and adds you to it, and sets up thezssdservice. - 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.
- 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
| Option | Effect |
|---|---|
--check | Report what this machine supports. Changes nothing, needs no password. |
--patch-driver | Apply the NVIDIA wake-on-touch patch without asking. |
--no-patch-driver | Never touch the GPU driver. |
--kernel-module | Build and install ZSS_Interceptor without asking. |
--no-kernel-module | Don't install the kernel module. The daemon then does a narrower version of its work from user space (NVIDIA and gmux only). |
--vm-passthrough | Also prepare the GPU for lending: a udev rule keeps the display server and the login manager off the card's display nodes. |
--no-start | Install everything but leave the service alone (when a reboot is next anyway). |
--build DIR | From source only: the Meson build folder (default ./build). |
--destdir DIR | From source only: install into a staging root. Skips everything that acts on the running system. |
What gets installed
| What | Where |
|---|---|
| Daemon, control tool, launcher | zssd, zssctl, zss-run under /usr/local |
| ZSS_AirLock | libzss_airlock.so and its Vulkan driver manifest, used only by programs started under it |
| Service | zssd.service, settings in /etc/zss/zssd.conf |
| Group | zss: members may switch GPUs off and on |
| Helpers | zss-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.
| Part | Works with | Tested on |
|---|---|---|
| Moving programs between GPUs (ZSS_AirLock) | Any Vulkan driver: NVIDIA, Mesa (Intel, AMD, nouveau), software | NVIDIA 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 them | NVIDIA 470, Intel hasvk (OpenGL 3.2) |
| Showing frames from one GPU on a screen driven by another, in order | Drivers 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 back | Any PCI GPU behind an IOMMU, with ZSS_Interceptor; the display server must not hold the card | MacBookPro9,1 with a QEMU guest; QEMU with an emulated IOMMU |
| Recovering programs when a GPU vanishes | Any Vulkan driver | QEMU, by pulling a virtual card |
Daemon, zssctl, rules, freezing, idle timer | Any GPU | Host 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 card | Any PCI GPU | QEMU; MacBookPro9,1 |
| Cutting power: Apple classic gmux | Apple laptops with a classic gmux | MacBookPro9,1 |
Cutting power: firmware power resources (acpi) | Most hybrid laptops since about 2015 | Nowhere yet: written, never run |
| Cutting power: PCIe hot-plug slot | Any driver, on a hot-plug slot | QEMU only |
| Power-off under a running display server | NVIDIA 470.256.02 with the patch only | MacBookPro9,1 |
| Recovering card and driver after a sudden power loss, card idle | NVIDIA 470.256.02 with the patch; drivers with PCI error handlers in principle | MacBookPro9,1 |
| The same, with a program rendering on the card | NVIDIA 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 programs | Device nodes: any driver. Loader and /proc files: NVIDIA only | MacBookPro9,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
acpibackend 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
zssdrunning:zssctl statusanswers.- 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-runor 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=vulkanmakes ANGLE, which gives Chromium its OpenGL ES, draw through Vulkan.--enable-features=Vulkanmakes 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 withzss-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 orzss-run --gl %command%for an OpenGL one. A game that Steam runs inside its runtime container may not see ZSS_AirLock; ifZSS_DEBUG=1prints nothing, start the game's own binary withzss-runinstead. - 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:
| Program | Recognised by | What it gets |
|---|---|---|
| Built on Chromium | icudtl.dat and a V8 snapshot beside the binary, and a binary that knows --use-angle | The ZSS environment and the two Vulkan switches |
| Uses Vulkan | Links or names libvulkan.so.1 | The ZSS environment |
| Unity game with Vulkan | A <name>_Data folder beside the binary | The ZSS environment and -force-vulkan |
| Anything else | Nothing |
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
acpibackend is untested. - With a display server running: the patched NVIDIA 470 driver. Without the patch,
zssctl offis refused while the display server holds the card. - ZSS_Interceptor loaded (recommended):
zssctl statusshowsbackend=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 statusbefore your firstoff. Every process it lists that isn't under ZSS_AirLock, a recognised display server or astop_servicesentry is frozen untilzssctl 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 offfrom 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-smisays 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 statuscounts these asserved=. - If the card has started driving a display by then, it stays on.
hide_while_off = noin the settings turns hiding off. A program that then reaches the driver waits untilzssctl on;zssctl statusshows it aswaiting=.
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 runningzssctl off. Your login manager is not on that list. zssctl off --consoleshows 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
zssdrunning, and programs started under ZSS_AirLock.- A second GPU, or
allow_software = yesfor the CPU renderer.
Assumes
- The target GPU has enough free memory.
- Nothing about the card's power changes:
detachandattachonly move programs.
Watch out
- A program that can't go anywhere suitable is parked: its window stops updating until
zssctl resume PIDor until its GPU comes back. --to softwareruns 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-eventon 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/movingexists.
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 checkzssctl statusfor anything that would be frozen. - A card that is lent stays lent: the battery event's
offis 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-dialogwhen that is installed. aconly 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_perfdcalls 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=onoramd_iommu=on) andvfio-pciin 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 --stayfirst 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 --checkand read every line. Every obstacle it reports is something that would otherwise break the lend or the session. --with-grouptakes 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
- 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.
- 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. - 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. - Marks the card lent. ZSS no longer watches, wakes or powers it, and refuses
off,on,detachandattachfor 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
vfiorefuses a group until told to allow unsafe interrupts. For one run:echo Y | sudo tee /sys/module/vfio_iommu_type1/parameters/allow_unsafe_interrupts, andNafterwards. This is acceptable for a guest you trust, not for one you don't. - Display. Arch's
qemu-basehas no window; installqemu-ui-gtkfor-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_busyis left atleaveunless you know you want otherwise.
Watch out
- Until
zssctl on, the card's device files are hidden: a monitor such asnvtopornvidia-smithat 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 offandlendrefuse and say so. loss_while_busy = freezestops the display server until the card is back. Don't set it on a machine you use.- If
zssctl onrefuses, 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 was | The desktop | Programs on the card | Getting the card back |
|---|---|---|---|
| Idle | Carries on | None | zssctl on: power, PCI state and driver restored, no reboot |
| Being rendered on | Carries on | Rebuilt on the Intel GPU in about 3.5 s, nothing lost | zssctl 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:
- marks the card as unplugged (
/proc/driver/nvidia/zss_hold); - takes its functions off the PCI bus while its power is still off, each step with a time limit, so the driver lets go;
- switches the power rail on and rescans the bridge above the card;
- waits for
nvidiato 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
| Value | What 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). |
refuse | The 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. |
freeze | The 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:
- 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.
- 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.
- Saves the report as a folder and a
.tar.gzfile 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.
| Part | What it is | Its job |
|---|---|---|
| ZSS_AirLock | User-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 |
| zssd | The daemon, a system service | Decides 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_Interceptor | The kernel shim, zss.ko (its file, /sys entries and kernel messages keep the name zss). Optional | The 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 patch | A small patch to NVIDIA's driver, applied on your machine | Lets 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
- The daemon asks the program, over its control socket, to move off a GPU.
- 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.
- It creates each object again on the new GPU, uploads the contents, and replays the recorded command buffers.
- 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.
- 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:
| Program | Without ZSS_AirLock | Under ZSS_AirLock |
|---|---|---|
| vkcube on Intel | 1004 | 947 |
| vkcube on NVIDIA, shown on the Intel screen | about 1530 | 861 |
| glxgears through Zink on Intel | 1175 | 549 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
- 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.
- 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. - Stop and freeze. Services listed in
stop_servicesare stopped; other processes still holding the card are frozen, except PID 1, logind, elogind, seatd, recognised display servers and the terminal runningzssctl. The card's device nodes are hidden from new programs. - PCI state is saved, in the kernel if ZSS_Interceptor is loaded.
- Driver suspended through its own sleep code.
- Power cut through the backend: the gmux on the reference laptop.
- 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
| Duty | How |
|---|---|
| Quiesce the driver | Runs 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 state | Saved before the cut and restored after, by the PCI core. Bus mastering is switched off before the power goes. |
| Power | Through 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 on | Power, 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 guard | Notices 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. |
| Lent | Hands 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 sleep | Pauses across suspend and resume of the whole machine. |
| IOMMU | Reports 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.
| State | Card power | Host driver | Programs that were on it | Display server |
|---|---|---|---|---|
| attached | On | Bound | Running on it | May use it |
| powered-off | Off (0 W) | Suspended | Moved to another GPU, or frozen | Served on demand (patched NVIDIA driver) |
| lost | Unknown | Frozen, or told through its error handlers | Rebuilt on another GPU, or parked | Carries on |
| lent | On | None (vfio-pci) | Moved to another GPU | Mustn't hold the card |
| detaching, attaching | Changing | Changing | Being 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.
| Command | What 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 PCI | Bring 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 PID | Resume a parked program on a suitable GPU. |
monitor | Print the daemon's events as they happen. |
Reference
zss-run
zss-run [options] program [args…]
| Option | Effect |
|---|---|
| (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 GPU | Start on another GPU: a PCI address, part of a GPU's name, dedicated, or any. |
--gl | Run an OpenGL program through Mesa's Zink, so that it can be moved too. |
--plain | Don't add the Chromium switches. |
--print | Show 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.
| Setting | Meaning | Default |
|---|---|---|
gpu | The 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_timeout | Power an unused card off after this many seconds. | Unset: only on request |
stop_services | Services that hold the card only to keep it initialised; stopped before a power-off, started again after. | nvidia-persistenced |
hide_while_off | auto (device nodes and the vendor's files), no, or a comma-separated list of extra paths to hide. | auto |
default_target | Where programs go when the card goes off: a PCI address or software. | Unset: the daemon picks |
allow_software | Let programs fall back to the CPU renderer when no GPU suits them. | no |
loss_while_busy | leave, refuse or freeze; see When a card is lost unexpectedly. | leave |
group | Members of this group may switch cards off and on. | zss |
Reference
Environment variables
Read by ZSS_AirLock in each program.
| Variable | Effect |
|---|---|
ZSS_SOCKET | The daemon's socket (default /run/zss/zssd.sock). |
ZSS_DEBUG | Log what is loaded and why a device isn't movable. |
ZSS_START_ON | The GPU a program starts on: dedicated, a PCI address, part of a name, or any. zss-run --on sets it. |
ZSS_PROFILE | portable (default): what every GPU has. native: each GPU's own. |
ZSS_PRESENT | direct: every swapchain on the drawing GPU. copy: always through the screen's GPU. Default: through the screen's GPU only where needed. |
ZSS_VULKAN | 1.0 offers Vulkan 1.0 only. |
ZSS_ALLOW_SOFTWARE | Let a parked program resume on the CPU renderer. |
ZSS_RETAIN | Where uploaded textures are kept to survive a lost GPU: disk (default), ram, off. |
ZSS_RETAIN_LIMIT_MB, ZSS_RETAIN_QUEUE_MB | Size cap of that store (4096) and of data waiting to be written (256). |
ZSS_WRITE_WATCH | 0: 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_FILES | Colon-separated driver manifests to use instead of the system's. |
ZSS_TRAP | Debugging: a Vulkan command ZSS_AirLock doesn't provide stops the program with that command's name. |
ZSS_BIND_PCI | Test aid: the CPU renderer poses as the PCI device at that address. |
ZSS_TEST_LOSE_AT_SUBMIT, ZSS_TEST_LOSE_AFTER_SUBMIT | Test aids: behave as if the GPU died at, or just after, that submit. |
ZSS_TEST_STUCK_MS, ZSS_LOSS_GRACE_MS | Test 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 testruns 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.shdoes.
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.md | The long-term design, and what became of each part |
Reference
Troubleshooting
Problems met on the reference laptop, and what fixes them.
zssctlcan't reachzssdsudo systemctl restart zssd, thenjournalctl -u zssd -bif 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(orzss-run --glfor OpenGL), or turn on ZrnLaunchRouter so that everything you start is routed.zssctl offsays a program still has the device openThat 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=1names a Vulkan command it called that ZSS_AirLock doesn't provide. Run it withoutzss-runmeanwhile.- 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 appshows 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_drmor its neighbours, often the display server or login manager. Install with--vm-passthroughand 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 statusshowsstate=lost, and the card's device files are hidden. Runzssctl 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.logholds 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.cis MurmurHash3 by Austin Appleby, which is in the public domain.