Getting hibernate working with Omarchy
The Problem
My Surface Laptop Studio 2 was running hot even when “sleeping”. For a while, I dismissed the issue as a byproduct of trying to force linux on Windows hardware, but eventually I decided that it was time to try to fix it seriously. For context, Surface devices only support S0ix (Modern Standby) rather than traditional S3 deep sleep, and Linux doesn’t play nice with it, so any state other than full shutdown would be become a portable heater.
The solution is to support hibernate, such as with suspend-then-hibernate — suspend briefly for quick wake, then hibernate after a timeout for actual power savings.
The Start
What I thought would be a quick config change turned into a comprehensive debugging session. Here’s everything that needed to be fixed:
1. Swapfile Setup (Btrfs Edition)
Apparently, btrfs swapfiles need special handling, which affected the offset I used for resume information. I initially used filefrag which gave me the wrong offset.
# Don't use dd or fallocate — use this
sudo btrfs filesystem mkswapfile --size 68G /swap/swapfile
# Get the offset (btrfs-specific command, NOT filefrag)
sudo btrfs inspect-internal map-swapfile -r /swap/swapfile
Additionally, I did not allocate enough space for the swapfile at first. For 64GB of RAM, I thus need to use at least 64G for a full write.
2. Kernel Parameters
On Omarchy with Limine bootloader, the kernel cmdline lives in /etc/default/limine:
KERNEL_CMDLINE[default]="... resume=/dev/mapper/root resume_offset=34933929 nvidia_drm.modeset=1"
Key learnings:
- Use
/dev/mapper/rootfor encrypted setups, not UUID - The offset must match
btrfs inspect-internaloutput exactly nvidia_drm.modeset=1is critical (more on this later)- The source of truth is
/etc/default/limine, not/boot/limine.conf
3. mkinitcpio Hook Ordering
The resume hook must come after encrypt (so it can access the decrypted swap) but before filesystems:
HOOKS=(base udev autodetect microcode modconf keyboard keymap consolefont block encrypt resume filesystems fsck)
I initially had resume at the end, as it makes most sense to easily recognize new / ordering of additions. The kernel would find the hibernate image but couldn’t access it because the encrypted volume wasn’t unlocked yet.
4. NVIDIA Nuisances
After fixing everything above, I still got:
PM: Image successfully loaded
nvidia 0000:01:00.0: PM: pci_pm_freeze(): nv_pmops_freeze [nvidia] returns -5
PM: hibernation: resume failed (-5)
The image loaded fine. NVIDIA just refused to freeze/thaw properly.
I tried:
NVreg_PreserveVideoMemoryAllocations=1NVreg_EnableGpuFirmware=0- Enabling
nvidia-hibernate.service - Removing
kmsfrom HOOKS
But the actual fix was removing nvidia modules from MODULES in /etc/mkinitcpio.conf.
# Before (broken)
MODULES=(nvidia nvidia_modeset nvidia_uvm nvidia_drm ...)
# After (working)
MODULES=(btrfs surface_aggregator ...)
Early KMS (loading nvidia in initramfs) has a bad interaction with PreserveVideoMemoryAllocations during hibernate resume.[^2] The solution is to let nvidia load later via the kernel parameter nvidia_drm.modeset=1 instead of baking it into initramfs.
Final Configuration
/etc/default/limine:
KERNEL_CMDLINE[default]="quiet splash cryptdevice=PARTUUID=...:root root=/dev/mapper/root zswap.enabled=0 rootflags=subvol=@ rw rootfstype=btrfs resume=/dev/mapper/root resume_offset=34933929 nvidia_drm.modeset=1"
/etc/mkinitcpio.conf:
HOOKS=(base udev autodetect microcode modconf keyboard keymap consolefont block encrypt resume filesystems fsck)
MODULES=(btrfs surface_aggregator surface_aggregator_registry surface_aggregator_hub surface_hid_core surface_hid 8250_dw intel_lpss intel_lpss_pci pinctrl_tigerlake)
/etc/modprobe.d/nvidia-power.conf:
options nvidia NVreg_PreserveVideoMemoryAllocations=1
options nvidia NVreg_TemporaryFilePath=/var/tmp
options nvidia NVreg_EnableGpuFirmware=0
/etc/systemd/logind.conf.d/lid.conf:
[Login]
HandleLidSwitch=suspend-then-hibernate
HandleLidSwitchExternalPower=suspend-then-hibernate
/etc/systemd/sleep.conf.d/omarchy.conf:
[Sleep]
HibernateDelaySec=2min
Enable NVIDIA power services:
sudo systemctl enable nvidia-suspend.service nvidia-hibernate.service nvidia-resume.service
Rebuild and reboot:
sudo limine-mkinitcpio
reboot
Helpful Debugging Commands
# Check hibernate/resume logs
journalctl -b | grep -i "PM:" | tail -30
# Verify kernel received resume params
cat /proc/cmdline | grep resume
# Check swap status
swapon --show
# Get btrfs swapfile offset
sudo btrfs inspect-internal map-swapfile -r /swap/swapfile
End
Hibernate now works, though it of course isn’t as native as Window’s default behavior. Closing the lid suspends immediately, then hibernates after 2 minutes.
The root cause — early KMS conflicting with NVIDIA power management — took the longest to diagnose. The error message nv_pmops_freeze returns -5 is pretty generic, and most search results pointed to wrong solutions, but I have gained a newfound appreciation for people that work with low-level OS primitives.