Skip to content

platform/surface: Add driver for Surface GPU power controller - #179

Open
Dvaderfun wants to merge 1 commit into
linux-surface:v7.2-surface-develfrom
Dvaderfun:surface-gpu-power
Open

Dvaderfun wants to merge 1 commit into
linux-surface:v7.2-surface-develfrom
Dvaderfun:surface-gpu-power

Conversation

@Dvaderfun

@Dvaderfun Dvaderfun commented Oct 20, 2026 •

Copy link
Copy Markdown

Disclaimer: this driver and the investigation behind it were AI-assisted ("vibe-coded" with Claude Code).
The code is small, every claim below was checked against the ACPI tables and on real hardware, and it works
reliably in our testing — but please review it with that in mind.

Problem

On the Surface Laptop Studio the NVIDIA dGPU is locked to 10 W after its first runtime suspend (D3cold).
It is fine right after boot. Known issue: wiki,
linux-surface/linux-surface#2185 (same on SLS2). Current workarounds cost battery (Runtime D3 off, 35 W only) or
require an old driver (525).

Cause

The firmware has a "Surface IHV GPU Power Controller", \_SB.SGPC (_HID MSHW0216, _DSM a99df1bb-b888-42b7-9ced-12810bfe9d90).
Windows activates it (_DSM func 7 = 1). On Linux nothing binds to it, so it stays inactive, and the dGPU power
resource sends NVIDIA D-notify 0xD5 on every D3cold exit:

PowerResource (PRPD) { Method (_ON) {
    ...
    If (SGPC.SACT == 1) { Notify (SGPC, 0x81) } Else { Notify (\_SB.PC00.RP09.PXSX, 0xD5) }

Direct proof on hardware: calling func 7 = 0 (sends 0xD5) drops the limit to 10 W instantly; func 7 = 1 (sends
0xD1) restores 35 W instantly, and afterwards _ON no longer sends 0xD5.

Fix

surface_gpu_power, a platform driver bound to ACPI MSHW0216. In the acpidumps this device exists on the SLS 1,
SLS 2 and Surface Book 3; the SB3 one has no _DSM, and the driver does not bind without _DSM func 7.

  • probe/resume: _DSM func 7 = 1 (activate), then func 1 = 1. Activation sets ENBL = 0, which makes the NVPCF
    parameters report Dynamic Boost as disabled; func 1 = 1 restores the firmware default.
  • remove: func 7 = 0 (firmware default).
  • logs the GPU power spec (func 5) once: AC 35 W (max 50 W), DC 30 W (max 35 W) on SLS1.

New patchset surface-gpu-power; config is enabled in linux-surface/linux-surface#2315.

Testing

Surface Laptop Studio (RTX A2000 Laptop GPU, UEFI 33.104.143), Fedora 44, kernels 7.2.11 (Fedora) and
7.2.11-1.surface, NVIDIA 615.71.09 (open modules), Runtime D3 enabled (DynamicPowerManagement=3).

Scenario Without driver With driver
First wake from D3cold after boot (AC) 10 W 35 W
Same on battery 10 W 35 W
20+ D3cold cycles, s2idle suspend/resume, module reload — 35 W every time
Power draw under CUDA load — 34.7 W of 35 W
Same, AC, with NVIDIA/open-gpu-kernel-modules#1468 — 50 W limit, ~40 W draw (= Windows "Best performance")

Builds without warnings (W=1); checkpatch reports no code issues.

Not covered

  • Dynamic Boost (35 -> 50 W) additionally needs the NVIDIA driver to know that AC is connected. It only reads ACPI
    _PSR, which Surface devices don't have, so it applies its battery policy. Fixed on the NVIDIA side in
    nv-acpi: Get AC state from power_supply when there is no _PSR NVIDIA/open-gpu-kernel-modules#1468. With that patch and this driver the SLS matches Windows: 50 W on AC
    ("Best performance" mode, same measured draw under the same load) and 35 W on battery.
  • In its other power modes Windows lowers the AC limit to ~40 W (probably via _DSM func 2/3). Not implemented; Linux
    behaves like "Best performance".
  • SLS2 has the same controller and _ON logic (spec 60 W rated / 80 W max) but is untested here.

Proposed wiki update

GitHub wikis can't take pull requests, so here is the replacement for the "NVIDIA GPU locked at 10W power limit"
section of the SLS page, to apply once
this is released:

Markdown
## NVIDIA GPU locked at 10W power limit

### Cause

The firmware has a "Surface IHV GPU Power Controller" (ACPI `MSHW0216`) that Windows activates at boot. While it is
inactive, the firmware tells the NVIDIA driver to limit the GPU to 10 W every time the GPU wakes up from D3cold
(runtime suspend). So the GPU is fine right after boot and drops to 10 W after it has been idle once.

### Fix

The linux-surface kernel includes the `surface_gpu_power` driver, which activates the controller like Windows does.
It is loaded automatically; Runtime D3 can stay enabled. Check that it is loaded:

```bash
lsmod | grep surface_gpu_power
```

Let the GPU go idle (`cat /sys/bus/pci/devices/0000:f3:00.0/power_state` shows `D3cold`), wake it up (e.g. run
`nvidia-smi`) and check the limit:

```bash
nvidia-smi -q -d POWER | grep 'Current Power Limit'
```

It should show 35 W (or 50 W with Dynamic Boost, see below) instead of 10 W.

If you use a different kernel, build the driver as an out-of-tree module from
[`drivers/platform/surface/surface_gpu_power.c`](https://github.com/linux-surface/kernel/blob/v7.2-surface/drivers/platform/surface/surface_gpu_power.c)
(e.g. with DKMS, `obj-m := surface_gpu_power.o`). With Secure Boot, sign it with a key enrolled in MOK.

### Optional: Dynamic Boost (35 W -> 50 W)

The remaining 15 W are provided by NVIDIA Dynamic Boost (`nvidia-powerd`). It does not engage on the SLS because the
NVIDIA driver cannot detect that the charger is connected (the SLS has no ACPI `_PSR`; AC state is reported by the
Surface Aggregator Module) and therefore applies its battery policy. A fix for the open kernel modules is proposed in
[NVIDIA/open-gpu-kernel-modules#1468](https://github.com/NVIDIA/open-gpu-kernel-modules/pull/1468). Until it is part
of a driver release you can apply it yourself. With it, the GPU behaves like on Windows: 50 W on AC (as in the "Best
performance" power mode) and 35 W on battery. No module options are needed.

Requirements: the NVIDIA **open** kernel modules (the default for this GPU), `nvidia-powerd` running
(`sudo systemctl enable --now nvidia-powerd`) and the `surface_gpu_power` driver above. The patch changes the driver
source, so repeat the steps after every NVIDIA driver update.

**Fedora (RPM Fusion `akmod-nvidia`)** — rebuilds the installed akmod with the patch (`sudo dnf install rpm-build`
first), then reboot:

<details><summary>akmod-nvidia-psrfix.sh</summary>

```bash
#!/usr/bin/env bash
# Rebuild RPM Fusion's akmod-nvidia with NVIDIA/open-gpu-kernel-modules#1468 applied.
# Run again after every NVIDIA driver update. Needs: sudo dnf install rpm-build
set -euo pipefail
PATCH_URL=https://github.com/NVIDIA/open-gpu-kernel-modules/pull/1468.patch
SRPM=${SRPM:-$(readlink -f /usr/src/akmods/nvidia-kmod.latest)}
[[ $SRPM == *psrfix* ]] && { echo "akmod-nvidia is already patched"; exit 0; }

TOP=$(mktemp -d)
trap 'rm -rf "$TOP"' EXIT
mkdir -p "$TOP"/{SPECS,SOURCES}
(cd "$TOP/SOURCES" && rpm2cpio "$SRPM" | cpio -idm --quiet)
mv "$TOP/SOURCES/nvidia-kmod.spec" "$TOP/SPECS/"
curl -fsSL -o "$TOP/SOURCES/nvidia-psr-fallback.patch" "$PATCH_URL"

# Apply the patch after Patch0 and mark the release
SPEC=$TOP/SPECS/nvidia-kmod.spec
sed -i -e 's/^\(Release:\s*[^%]*\)\(%{?dist}\)/\1.psrfix\2/' \
       -e '/^Patch0:/a Patch1:        nvidia-psr-fallback.patch' \
       -e '/^%patch -P0 -p1$/{n;s/^%endif$/%endif\n%patch -P1 -p1/}' "$SPEC"
grep -q '^%patch -P1 -p1' "$SPEC" && grep -q '^Release:.*psrfix' "$SPEC" ||
	{ echo "Unexpected nvidia-kmod.spec layout, nothing changed" >&2; exit 1; }

rpmbuild -bb --nodeps --define "_topdir $TOP" --define "buildforkernels akmod" "$SPEC"
sudo dnf install -y "$TOP"/RPMS/*/akmod-nvidia-*.rpm
sudo akmods --force --kernels "$(uname -r)"
echo "Done. Reboot to load the patched driver."
```

</details>

**DKMS (e.g. Arch `nvidia-open-dkms`, Ubuntu/Debian `nvidia-dkms-*-open`)** — not tested, the patch applies to the
DKMS source tree:

```bash
ver=$(modinfo -F version nvidia)
curl -fsSL https://github.com/NVIDIA/open-gpu-kernel-modules/pull/1468.patch | sudo patch -d /usr/src/nvidia-$ver -p2
sudo dkms remove nvidia/$ver --all
sudo dkms install nvidia/$ver
```

Regenerate the initramfs if it contains the NVIDIA modules (`sudo update-initramfs -u` / `sudo mkinitcpio -P`) and
reboot.

Check: `nvidia-settings -q GPUPowerSource -t` prints `0` on AC and `1` on battery, and under GPU load on AC
`nvidia-smi -q -d POWER | grep 'Current Power Limit'` shows 50 W.

The old workarounds (disabling Runtime D3, downgrading to driver 525) are no longer needed.

🤖 Generated with Claude Code

Surface Laptop Studio devices with a discrete NVIDIA GPU have an ACPI
"Surface IHV GPU Power Controller" device (MSHW0216). On Windows, a
Microsoft driver activates it via _DSM function 7. On Linux nothing does,
and while it is inactive, the power resource of the dGPU sends NVIDIA
D-notify 0xD5 to the GPU on every D3cold exit. This locks the GPU to a
10 W power limit after its first runtime suspend.

Activate the controller on probe and on resume. This sends D-notify 0xD1
(no limit) once and stops the 0xD5 notifications. Activation also turns
off Dynamic Boost in the NVPCF parameters reported to the GPU driver, so
re-enable it via _DSM function 1, which is the firmware default.

Tested on a Surface Laptop Studio (RTX A2000 Laptop GPU): the GPU keeps
its 35 W rated limit across runtime suspend, s2idle, AC and battery,
instead of dropping to 10 W.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Danylo Dermenzhy <dvaderfun@gmail.com>
Patchset: surface-gpu-power
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant