← All blogs

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/root for encrypted setups, not UUID
  • The offset must match btrfs inspect-internal output exactly
  • nvidia_drm.modeset=1 is 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=1
  • NVreg_EnableGpuFirmware=0
  • Enabling nvidia-hibernate.service
  • Removing kms from 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.