- Shell 70.3%
- C++ 17.5%
- QML 10.3%
- CMake 1.9%
On battery, nvidia.ko tells RM the system is on DC and the closed RM applies the VBIOS battery policy, which locks the 1060 to P8. Patch the DKMS source to add a force_ac_power module parameter that reports AC regardless of _PSR, enable it via modprobe.d, and re-apply the patch from a pacman hook that runs before DKMS builds each new nvidia source tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| applets/gpu-launch | ||
| home | ||
| pkgbuilds | ||
| system | ||
| .gitignore | ||
| install.sh | ||
| README.md | ||
surface-mods
Customizations for a Surface Book 2 (Arch + linux-surface, KDE Plasma 6 Wayland, nvidia-580xx for the GTX 1060 in the base).
pkgbuilds/ patched packages, built and installed under -local names
system/ files that live under / (mirrors absolute paths)
home/ files that live under ~ (mirrors paths relative to $HOME)
install.sh deploys everything; safe to re-run
Install / deploy
./install.sh # copy system/ to / (root-owned), symlink home/ into ~,
# add IgnorePkg, reload systemd/udev, enable services
./install.sh --packages # also build and install the -local packages (fresh machine)
This repo is the source of truth.
home/files are symlinked, so editing them here takes effect right away.system/files are copied, because root must never run files that a normal user can edit. After editing one, re-run./install.sh.
The bbdx-rebuild service builds out of pkgbuilds/kwin-effects-better-blur-dx-local
in place, so the repo must stay at ~/repos/surface-mods. If you move it, update
PKGDIR in system/usr/local/bin/bbdx-rebuild.
Build artefacts and cached sources are gitignored, as is pkgbuilds/old-builds/:
the pre-rename -1.1 packages, kept locally for rollback only.
| Customization | Files |
|---|---|
| Hybrid GPU: KWin on iGPU, apps on GTX 1060 | home/.config/environment.d/gpu.conf, home/.config/systemd/user/*/igpu-only.conf, system/etc/udev/rules.d/61-intel-igpu.rules, system/etc/mkinitcpio.conf.d/surface.conf, system/etc/modprobe.d/nvidia-no-fbdev.conf, home/Scripts/kitty.zsh, home/.local/share/applications/kitty.desktop, system/etc/surface-dtx/{detach,attach}.sh, system/usr/local/bin/dgpu-{detach-guard,relaunch-offer,window-state,as-user} |
| GPU launch switch (Plasma widget) | applets/gpu-launch/, pkgbuilds/plasma-applet-gpu-launch-local/, system/usr/local/bin/gpu-launch, home/.config/systemd/user/app-.service.d/gpu-launch.conf, home/.config/systemd/user/gpu-launch-sync.service, home/.config/systemd/user/graphical-session-pre.target.d/gpu-launch.conf |
| GTX 1060 full speed on battery | system/usr/local/share/surface-mods/nvidia-force-ac.patch, system/usr/local/bin/nvidia-force-ac-patch, system/etc/pacman.d/hooks/69-nvidia-force-ac.hook, system/etc/modprobe.d/nvidia-force-ac.conf |
| egl-wayland PRIME offload fix | pkgbuilds/egl-wayland-local/ |
| Better Blur DX repaint-loop fix | pkgbuilds/kwin-effects-better-blur-dx-local/, system/usr/local/bin/bbdx-rebuild, system/etc/systemd/system/bbdx-rebuild.service, system/etc/pacman.d/hooks/90-bbdx-rebuild.hook |
| Studio-mode lid switch guard | system/usr/local/bin/surface-studio-lid, system/etc/systemd/system/surface-studio-lid.service |
| iptsd restart on boot race | system/etc/systemd/system/iptsd@.service.d/restart.conf |
| Audio pin retask | system/etc/modprobe.d/hda-jack-retask.conf, system/usr/lib/firmware/hda-jack-retask.fw |
| Other pacman hooks | system/etc/pacman.d/hooks/{kmix-cleanup,odin-build-stb}.hook |
Hybrid GPU
Goal: run Wayland-native apps on the GTX 1060 while the desktop stays on the Intel UHD 620, so detaching the clipboard from the base never takes the session down.
- KWin is pinned to the iGPU.
environment.d/gpu.confsetsKWIN_DRM_DEVICES=/dev/dri/intel-igpu. That path is a colon-free symlink created by61-intel-igpu.rules; it has to be colon-free becauseKWIN_DRM_DEVICESis a colon-separated list, so a/dev/dri/by-path/pci-0000:00:02.0-cardpath would break it. - Apps are offloaded to the 1060, unless switched to the iGPU with the
GPU launch switch. In nvidia mode,
gpu-launchsets__NV_PRIME_RENDER_OFFLOAD=1and__GLX_VENDOR_LIBRARY_NAME=nvidia, but only while the 1060 is bound to nvidia.- It deliberately doesn't set
__VK_LAYER_NV_optimus=NVIDIA_only, which would hide the iGPU from Vulkan after a detach.
- It deliberately doesn't set
- The desktop itself stays on the iGPU. The
igpu-only.confdrop-ins onplasma-*,xdg-desktop-portalandxdg-desktop-portal-*UnsetEnvironmentthose variables. Without them, plasmashell never recovers from a detach.- The drop-ins also cover KWin, the lock screen, plasmashell, KRunner and the portals.
- Unsetting the variables isn't enough on its own. glvnd tries the NVIDIA EGL vendor
(
10_nvidia.json) before Mesa, and loading it opens/dev/nvidia*either way: KWin ended up holding an NVIDIA context and detaching failed. So the drop-ins also set__EGL_VENDOR_LIBRARY_FILENAMESto Mesa only andVK_DRIVER_FILESto the Intel ICDs. - To check,
nvidia-smimust not listkwin_waylandorplasmashell, andgrep nvidia /proc/$(pgrep -x plasmashell)/mapsmust print nothing. - Apps started from Plasma run as their own
app-*.serviceunits. They inherit plasmashell's environment, including the Mesa/Intel-only restriction, soapp-.service.d/gpu-launch.confoverrides it per launch (see GPU launch switch).
- kitty follows the launch mode wherever it is started from.
kitty.desktopruns~/Scripts/kitty.zsh, which takes the variables fromgpu-launch env, so shells in it (and what they start) get the same mode. - Nothing pins the 1060 from boot.
mkinitcpio.conf.d/surface.confkeeps the nvidia modules out of the initramfs. udev loads them from the root fs when the base appears, including on reattach.install.shrerunsmkinitcpio -Pwhen this file changes.nvidia-no-fbdev.confsetsnvidia_drm fbdev=0. The default is 1, so droppingnvidia_drm.fbdev=1from the kernel command line isn't enough. Anvidia_drm.fbdev=there also overrides this file, so leave it out of the boot entry.nvidia-persistencedis disabled (install.sh). It holds/dev/nvidia0open, so on detachnv_pci_removewaits forever ("non-zero usage count"), wedging pciehp, and the 1060 is never re-enumerated on reattach.
- Detach guard.
surface-dtx/detach.shis stock plus a call todgpu-detach-guard.- First, it switches new launches to the iGPU (
gpu-launch suspend), so nothing new starts on the 1060 during the prompt or the kill. If the detach is aborted, it undoes that (gpu-launch resume). - It looks for processes holding
/dev/nvidia*or the 1060's/dev/drinodes open. Any of them would leavenv_pci_removestuck the same way persistenced does. - If there are some, it shows a critical notification listing them with Kill and
detach / Cancel. Cancel, or no answer within 40 s, aborts the detach (the latch
stays shut). The daemon kills handlers after 60 s, so keep
PROMPT_SECSunder that. - Kill closes whole apps, not just the GPU-holding processes. For each holder it walks
up to the topmost ancestor below the systemd user manager (the app's main process)
and takes that plus all its descendants and every cgroup they live in. This
matters for Electron: VS Code's main process moves itself out of the
app-code\x2doss@….servicePlasma started intoapp-code-oss-<pid>.scope, so killing one cgroup left a half-dead instance that the relaunch then handed off to. Only apps underapp.sliceare treated this way; anything else only has the holder itself killed. - It first sends SIGTERM to each app's main process, so it can quit cleanly and save
its session. If the GPU is still held or any of the app is still running 3 s later
(including a respawned GPU process), every one of its cgroups is killed via
cgroup.kill, plus SIGKILL for its processes. The detach goes ahead once nothing holds the GPU, or is aborted if something still does after another 2 s (stuck in the kernel). - After a kill, it starts
dgpu-relaunch-offerin a transientsystemd --userunit. That waits for the detach to finish (or be canceled), then offers to Relaunch the killed apps viakstart --application, after the 1060 is gone. Only apps started from Plasma are offered (desktop id from theirapp-<id>@….service, or from anapp-<id>-<pid>.scopethey moved into); programs started from a terminal close with their terminal, and the terminal is offered instead. - Before the kill,
dgpu-window-state saveasks KWin (through a throwaway KWin script whose output is read back from the journal) which virtual desktops, activities and screen each of those apps' windows were on, and whether they were minimized, maximized, fullscreen or kept above. On relaunch,dgpu-window-state restoreloads a KWin script for 60 s that puts each new window of those apps back. Geometry isn't restored, because Krohnkite retiles the windows anyway. - It only kills processes; custom scripts that unloaded the nvidia modules didn't work.
- The attach handler (
surface-dtx/attach.sh, also run when a detach is aborted after the latch opened) only runsgpu-launch resume. dgpu-as-userruns a command as the seat0 user, in their session bus and user manager. The handlers use it to reach the desktop.
- First, it switches new launches to the iGPU (
- Native Wayland offload also depends on the egl-wayland patch below.
GPU launch switch
A Plasma widget picks the GPU that apps launched from then on render on: the GTX 1060 (green circle) or the Intel UHD 620 (blue circle). Click it to switch. Apps that are already running keep their GPU.
-
Widget.
applets/gpu-launch/is a C++ Plasma applet (local.surfacemods.gpulaunch, "GPU Launch Mode") with a QML front end. It appears in the system tray by itself (EnabledByDefault) after plasmashell restarts. To hide it, use System Tray Settings → Entries. It can also go on the desktop or a panel, where it shows the name as well. Right-click for explicit Launch New Apps on NVIDIA / Intel.-
A ring in the other colour means that GPU is picked, but apps can't launch on it yet. A blue circle with a green ring means "NVIDIA once the base is back".
-
The applet is only the UI and a watcher. It reads
gpu-launch's state files (an inotify watch on their directories) and runsgpu-launchto switch. -
It checks every 5 s whether nvidia has the 1060, and every second for a minute after surface-dtx reports the base or latch changing. When the result disagrees with the current mode (the 1060 came back after a reattach), it runs
gpu-launch sync. -
pkgbuilds/plasma-applet-gpu-launch-localbuilds it from the repo, so pacman owns the plugin in/usr/lib/qt6/plugins/plasma/applets/. It links only public libPlasma/Qt APIs, so it doesn't need rebuilding for every Plasma or Qt update the way the KWin effect does. Its QML is built withNO_CACHEGEN, because ahead-of-time compiled QML links Qt private API. Rebuild after editing it, then restart plasmashell (systemctl --user restart plasma-plasmashell):cd pkgbuilds/plasma-applet-gpu-launch-local && makepkg -f && sudo pacman -U "$(makepkg --packagelist | head -n1)"
-
-
gpu-launchdoes the switching, and works without the widget:gpu-launch # mode=, preferred=, available=, detaching= gpu-launch nvidia|intel # pick a GPU (kept across logins) gpu-launch toggle- The preference is kept in
~/.local/state/gpu-launch/preferred, defaulting to nvidia. Apps launch on nvidia only when it's preferred, the 1060 is bound to nvidia, and no detach is under way. Otherwise they launch on intel. - Each mode is applied in two places:
$XDG_RUNTIME_DIR/gpu-launch/app.env, theEnvironmentFile=of everyapp-*.service(app-.service.d/gpu-launch.conf), so every app Plasma starts. Those units inherit plasmashell's iGPU-only restriction throughEnvironment=, andEnvironmentFile=is the only setting that overrides it. (UnsetEnvironment=runs last, so it would also drop the file's own values.) In nvidia mode the file sets the offload variables and lists every EGL vendor and Vulkan ICD manifest. In intel mode it sets the same Mesa/Intel-only lists asigpu-only.conf. It also setsVK_LOADER_DRIVERS_SELECT=*intel*, overriding the*nvidia*filter that switcheroo-control has KIO add for apps that prefer the dedicated GPU (Steam'sPrefersNonDefaultGPU, Run on dedicated GPU). Without that override, such apps would get no Vulkan driver at all. systemd reads the file at each start, so a switch applies to the next launch.- The systemd user manager environment, for everything else it starts. That means D-Bus-activated apps and services: nvidia mode sets the offload variables there, and intel mode sets the iGPU-only restriction instead.
gpu-launch-sync.serviceapplies the mode at login, before Plasma starts apps. It's pulled in by agraphical-session-pre.targetdrop-in, so it doesn't need enabling. It replaces the old50-nvidia-offloaduser environment generator.- Detach and reattach.
dgpu-detach-guardrunsgpu-launch suspendwhen a detach starts. That creates$XDG_RUNTIME_DIR/gpu-launch/detachingand switches to intel, while keeping the preference.- An aborted detach runs
gpu-launch resume, either from the guard or fromattach.shvia the daemon'sdetach_aborthandler. - After a real detach, the marker is dropped by the next
synconce the 1060 is gone, or byattach.shon reattach. - When nvidia binds the 1060 again, the widget's poll notices and syncs back to nvidia (a few seconds after the latch closes).
- An aborted detach runs
- Terminals and shells keep the variables they started with. Run
eval "$(gpu-launch env)"to bring a shell up to date.
- The preference is kept in
GTX 1060 full speed on battery
Problem: on battery, the 1060 is locked to P8 (139 MHz core, 405 MHz memory) even at
full load. nvidia-smi -q -d PERFORMANCE reports SW Power Cap and SW Thermal Slowdown,
at about 6 W and 40 °C against a 72 W limit.
- The cap comes from NVIDIA's own driver, not from the Surface firmware.
nvidia.koreads_PSRon the ACPI AC device (ADP1, answered by SAM) at init and on every AC change. When it reads 0, it tells RM the GPU is on battery (NV2080_CTRL_CMD_PERF_SET_POWERSTATE). - For Pascal, the part of RM that handles this is closed source. It applies the VBIOS battery policy, and that policy is P8.
- Ruled out: the ACPI tables push no cap (no D-notifiers, and GPS
_DSMonly shares CPU P-state limits). The Surface platform profile and the 1060's temperature don't affect it either. - The Arch wiki blames this throttle on fan control. That doesn't fit a GPU at 40 °C.
Patch: nvidia-force-ac.patch adds a force_ac_power parameter to nvidia.ko. When
set, nv_acpi_get_powersource() reports AC whatever _PSR says, so RM never enters
battery mode. nvidia-force-ac.conf turns it on. The kernel, upower and Plasma still
see the real power source.
- With the parameter on, the 1060 boosts to P0 (1404 MHz core, 4006 MHz memory) on battery.
- Battery cost: the 1060 can draw up to 72 W, and the CPU adds about 25 W, so heavy GPU use drains the batteries fast. The hardware power brake still works as a backstop.
nv-acpi.cis part of the source that DKMS builds, not the closed binary blob, so no package fork is needed.
Keeping it applied: 69-nvidia-force-ac.hook runs nvidia-force-ac-patch on every
new /usr/src/nvidia-* tree. It sorts before 70-dkms-install.hook, which then builds
the patched tree.
- If the patch stops applying, the script sends a critical notification and DKMS builds stock. The modprobe option then only logs "unknown parameter".
- Kernel updates rebuild from the already-patched tree.
pacman -Qkk nvidia-580xx-dkmsreportsnv-acpi.cas altered. That's expected.
cat /sys/module/nvidia/parameters/force_ac_power # Y = battery cap bypassed
echo N | sudo tee /sys/module/nvidia/parameters/force_ac_power # battery policy back, live
sudo nvidia-force-ac-patch --rebuild # patch + DKMS rebuild by hand
Writing the parameter re-sends the power source to RM immediately if the GPU is in use. If the GPU is idle, the new value takes effect when it is next opened.
Patched packages
Both patched packages are renamed <pkg>-local, with provides=/conflicts= on the
original name:
- Dependents are still satisfied (for example
nvidia-580xx-utils→egl-wayland). pacman -Syuandparu -Suanever "upgrade" them to an unpatched build.
Both names are also in IgnorePkg in /etc/pacman.conf (install.sh adds it), so an
AUR upload under the same name can't replace them. That line also lists
plasma-applet-gpu-launch-local, which is not a fork but is built from this repo
(GPU launch switch). kwin-effects-better-blur-dx-patched
already exists in the AUR, which is why the suffix is -local. Note that IgnorePkg
doesn't affect pacman -U.
Rebuild and reinstall one by hand:
cd pkgbuilds/<name> && makepkg -f && sudo pacman -U "$(makepkg --packagelist | head -n1)"
Upstream packaging, for diffing against future releases:
- egl-wayland: https://gitlab.archlinux.org/archlinux/packaging/packages/egl-wayland
- Better Blur DX: https://aur.archlinux.org/kwin-effects-better-blur-dx.git
egl-wayland-local: NVIDIA/egl-wayland#203
Problem: egl-wayland 1.1.23 added linux-dmabuf v6 support and broke PRIME detection on compositors with dmabuf ≤ v5, which includes KWin 6.7.
- Wayland clients offloaded to the GTX 1060 send NVIDIA-tiled dmabufs to the Intel-driven KWin.
- KWin fails with
importing the supplied dmabufs failed, and the window renders black. - Upstream issue: NVIDIA/egl-wayland#202.
Patch: pr203.patch is NVIDIA's own fix (PR #203, Austin Shafer). It restores a
primeRenderOffload flag that dmabuf < 6 uses to pick PRIME mode when the render
device differs from the compositor's.
Why it's frozen rather than handled by a hook: egl-wayland is an Arch [extra]
package, so the next Arch bump would replace a pkgrel 1.1 build with an unpatched one.
- A pacman hook can't re-patch it, because hooks can't run pacman.
- A hook that aborts the transaction would block every other update.
- nvidia-580xx is end-of-life, so nothing should need a newer egl-wayland. The package is renamed and frozen instead.
Returning to stock once an extra/egl-wayland release contains #203:
sudo pacman -S --ask=4 egl-wayland # --ask=4 = yes, remove the conflicting egl-wayland-local
Then drop egl-wayland-local from IgnorePkg. If something ever needs a newer
egl-wayland, rebase pr203.patch onto the new tag instead, unless the fix is already in it.
kwin-effects-better-blur-dx-local: BlurCache damage-tracking fix
Problem: Better Blur DX's BlurCache marked every painted region as dirty and re-injected it 33 ms later. With buffer-age repaints the loop sustained itself: the whole 3240×2160 screen recomposited at 60 Hz and re-blurred at 30 Hz, forever.
| GPU cost on the UHD 620 | Frame rate | |
|---|---|---|
| Before the fix | 22–72 Mcycles/frame | 16–47 fps |
| With the fix | about 5 Mcycles/frame | steady 60 fps |
Patch: damage-tracking.patch tracks real damage via Item::hasRepaints() in
prePaintScreen. The source is branch fix/damage-tracking (based on tag v2.5.1) in
ashe-dextop:~/repos/kwin-effects-better-blur-dx.
Interference 1, AUR bumps: the rename freezes it, so paru won't rebuild it from the unpatched AUR PKGBUILD.
Interference 2, every kwin upgrade breaks it: this happens with or without the patch.
- KWin embeds its exact version in the effect plugin interface ID
(
org.kde.kwin.EffectPluginFactory6.7.5) and won't load a plugin built for another version. - That includes Arch pkgrel rebuilds of kwin.
This is handled automatically:
90-bbdx-rebuild.hookruns on kwin install or upgrade and callsbbdx-rebuild --queue. That startsbbdx-rebuild.servicewithout blocking, since pacman still holds its lock while hooks run.bbdx-rebuild, run by the service as root, works like this:- Waits for pacman's lock to clear.
- Compares the installed kwin version with
/var/lib/bbdx-rebuild/kwin-version. - If they differ, runs
makepkgasashein this repo's package directory, thenpacman -U. - Writes the stamp and sends a desktop notification. A failure sends a critical notification, with details in the journal.
- The service is also enabled at boot, which catches a rebuild interrupted by a reboot.
The source tarball is cached in the package directory (gitignored), so rebuilds work
offline. The build deps cmake extra-cmake-modules qt6-tools are marked explicitly
installed, so orphan cleanup won't remove them.
After a rebuild, log out and back in: Qt never unloads a plugin library.
sudo journalctl -u bbdx-rebuild -b # what the last rebuild did
sudo bbdx-rebuild --force # rebuild now
If upstream releases a version newer than 2.5.1, check whether the BlurCache loop is fixed upstream:
- Fixed upstream: switch back.
- Run
paru -S --ask=4 kwin-effects-better-blur-dx. - Disable the service and delete the hook.
- Drop the name from
IgnorePkg.
- Run
- Still broken: rebase the patch on dextop, bump
pkgver, and rebuild.
Display settings that go with this: "Color power preference" is set to prefer efficiency, because prefer accuracy costs about 2.5× the compositor work (FP16 offscreen buffer plus a shader color pass).
Studio-mode lid switch guard
With the clipboard attached backwards (studio mode), folding the hinge shut still fires an ACPI lid-close event, which suspends the machine.
- A logind inhibitor wouldn't help, because PowerDevil already holds a block inhibitor on handle-lid-switch.
- Instead,
surface-studio-lidwatches surface-dtx'sDeviceModeover D-Bus.- In studio mode it unbinds the ACPI lid button (
PNP0C0D:00). - In laptop or tablet mode it rebinds it, and it always rebinds on exit.
- In studio mode it unbinds the ACPI lid button (
- Deliberate suspends keep working.
iptsd boot race
iptsd exits 1 if it sends its SET_MODE feature report while the IPTS sensor is inside the
1 s restart window in ipts_control_restart(), where the driver returns -ENODEV. The
shipped unit has Restart=no, so losing that race at boot was permanent (no touch or pen
until iptsd was restarted by hand). The restart.conf drop-in retries past the window.
Audio pin retask
The hda-jack-retask GUI generated these files:
hda-jack-retask.fwoverrides the Realtek ALC298 (0x10ec0298) pin configuration.hda-jack-retask.confloads the override viasnd-hda-intel patch=.
Changes take effect after a reboot. To revert, delete both files from the system, remove them here, and reboot.
Other pacman hooks
kmix-cleanup.hookdeletes kmix's XDG autostart entry after every kmix install or upgrade.odin-build-stb.hookbuilds Odin's vendored stb after odin updates.
Not in this repo
- VS Code
product.jsonandsharedProcessMain.jsare patched by the pacman hooks of the AURcode-marketplace/code-features-insiderspackages, which re-apply them after everycodeupgrade. Nothing to track. - KDE settings, such as kwinrc effects and blur parameters, and display color
preference, are left in place in
~/.config.