Skip to content

nv-acpi: NPCF SSDT returns zeroed power budget — Dynamic Boost stuck at VBIOS default #1345

Description

@Punch-Pain

Problem

On the Lenovo LOQ Essential 15IRX11 (83SC) with RTX 5050 (GB207M, 10DE:2D98), Dynamic Boost never activates. The GPU stays at the VBIOS default 35W instead of boosting to 65W (50W TGP + 15W PPAB).

The root cause is that the stock BIOS NPCF SSDT returns zeroed power budget fields in all NVPCF DSM subfunctions. The driver constructs the PMO successfully (func 0x00 bit 0 IS set), but all downstream budget data is zeroed — so Dynamic Boost offsets are computed as 0W and the GPU never leaves VBIOS defaults.

This was confirmed on Windows 11 with stock BIOS as well — same 45W flatline. The bug is in the ACPI tables, not the driver.

System Specifications

Component Detail
Machine Lenovo LOQ Essential 15IRX11, type 83SC
BIOS SECN22WW (Insyde H2O, 2026-05-21)
GPU NVIDIA GeForce RTX 5050 Laptop GPU, GB207M die
GPU PCI ID 10DE:2D98 (SubSystem 0x8FFE17AA)
GPU Architecture Blackwell
GPU Memory 8151 MiB GDDR7
VBIOS Version 98.07.27.00.43
VBIOS Power Limits TGP 50W / CTGP 35W / PPAB 15W / Max 65W
GPU Max Clocks Graphics 3090 MHz / Memory 12001 MHz
PCIe Gen5 x16 max, Gen1 x8 current
CPU Intel Core i5-13450HX (Raptor Lake-HX, 10C/16T)
CPU power limits PL1 configurable 25–60W, PL2 configurable 40–85W (Lenovo Vantage)
Kernel 7.2.3-1-cachyos (x86_64, CONFIG_ACPI_TABLE_UPGRADE=y)
NVIDIA driver 610.57.04 (open, nvidia-open)
GSP Firmware 610.57.04
nvidia-powerd v2.0 (build 1)
Inforom G005.0000.98.01
Bootloader Limine (UEFI)

ACPI Table Inventory

The firmware exposes 38 CB-01 tables via XSDT. Key tables:

XSDT # Type Size OEM Table ID Contents
1 SSDT 86,813B CB-01 Large peripheral SSDT
2 SSDT 262B CB-01 Small auxiliary
3 SSDT 5,709B CB-01 Peripheral config
4 SSDT 7,946B CB-01 NPCF + GPS + PEGP — the buggy table
12 DSDT 660,250B CB-01 Main DSDT (has External(NPCF) + Notify(NPCF))
5–22 SSDT varies CB-01 Remaining CB-01 tables

All 22 CB-01 SSDTs share the identity tuple (SSDT, LENOVO, CB-01) with OEM Revision 0x01. The NPCF-carrying table is SSDT #4 in XSDT order (0-indexed: #3).

NPCF Table Structure (7,946 bytes)

The NPCF SSDT contains:

  1. PEGP OperationRegions — VBOR, NOPR (0x7387C018, 0x2027 bytes), HGOP
  2. PEG1/PEGP scope — RPCX config space, PEGP methods including GPS
  3. Scope(_SB) Device(NPCF) — NVDA0820, _DSM with UUID 36b49710-2483-11e7-9598-0800200c9a66
  4. PEGP _DOD/MXMX/MXDS/MXM — display output methods

The GPS method (lines 406–536) is inside PEGP scope. The NPCF device (lines 844–1186) is at \_SB scope.

Root Cause Analysis

NPCF _DSM Structure

The NPCF device (\_SB.NPCF) implements \_DSM(UUID=36b49710-2483-11e7-9598-0800200c9a66, rev=0x200) with these subfunctions:

Subfunc Name Purpose
0x00 GET_SUPPORTED Returns bitmask of supported subfunctions
0x01 GET_STATIC_CONFIG_TABLES Static configuration (checksum-validated)
0x02 GET_DYNAMIC_PARAMS Dynamic power budget (TGPA/TPPA/MAGA offsets)
0x08 GET_DC_SYSTEM_POWER_LIMITS DC battery power limits (JPAC)

What the Driver Does With Each Subfunction

func 0x00 (client_resource.c:2942-3002):

  • Returns Buffer(4) — a bitmask of supported subfunctions
  • The driver tests only bit 0 (FLD_TEST_DRF(..._FUNC_GET_SUPPORTED_IS_SUPPORTED, _NO, ...))
  • If bit 0 = 1 → PMO constructs, driver proceeds to func 0x02
  • The stock BIOS returns Buffer(4){0xBF,0x06,0x00,0x00} — bit 0 IS set
  • So the driver enters the dynamic boost path, but all data is zeroed

func 0x02 (client_resource.c:3100-3173):

  • Returns a 49-byte buffer with this memory layout:
Offset  Size  Field         Description
------  ----  ------------  -----------
0x00    1     headerSize    Must be 0x05
0x01    1     commonSize    Must be 0x10
0x02    1     entrySize     Must be 0x1C
0x03    1     version       Must match driver's requested version
0x04    1     entryCount    Number of controller entries (1 on LOQ)
--- common section (0x10 bytes) ---
0x05    2     TGPA          CTGP AC offset in 125mW units
                            Driver: pParams->ctgpOffsetmW = 125 * TGPA
0x07    2     TGPD          CTGP DC offset
--- entry section (0x1C bytes per entry) ---
0x19    2     TPPA          Target PP AC offset
0x1B    2     TPPD          Target PP DC offset
0x1D    2     MAGA          Max AC GPU offset
0x21    2     MIGA          Max idle GPU offset
0x25    2     DROP          Delta response offset
0x29    4     LTBC          Long-term budget cap
0x2D    4     STBC          Short-term budget cap

The driver validates at client_resource.c:3121-3135:

if ((headerOut.headerSize != 0x05) ||
    (headerOut.commonSize != 0x10) ||
    (headerOut.entrySize  != 0x1C))
{
    status = NV_ERR_INVALID_DATA;
}

Stock BIOS returns all-zeroed fields — TGPA=0x0000, TPPA=0x0000, MAGA=0x0000. The driver computes:

  • ctgpOffsetmW = 125 * 0 = 0W — no CTGP boost
  • targetTppOffsetmW = 125 * 0 = 0W — no PP offset
  • maxOutputOffsetmW = 125 * 0 = 0W — no max output boost

Result: Dynamic Boost is "supported" (bit 0 set) but all offsets are 0W. The GPU stays at VBIOS default 35W.

func 0x01 — 14-byte static config with two's complement checksum:

  • Sum all bytes except the last
  • Negate: (~sum) + 1 (one-byte two's complement)
  • Must equal the last byte
  • Stock BIOS: checksum 0xAB, data correct

func 0x08 — 22-byte DC system power limits (only used on battery, not relevant to AC dynamic boost)

GPS Pipeline Issues (Secondary)

The GPS (GPU Power Steering) method in the same SSDT has three additional problems:

  1. GPSS guard blocks GPS calls: PEGP's _DSM checks If ((GPSS != Zero)) before dispatching to GPS(). GPSS is a field in an OperationRegion (NOPR at 0x7387C018) that may not be mapped when _INI runs. The nvidia driver probes GPS at boot before NPCF's _INI sets GPSS=1.

  2. Missing GETPPL (func 0x24): The GPS switch statement has no Case(0x24), so it falls through to Default → Return(0x80000002). The driver's platform_request_handler_ctrl.c:629-641 expects a 12-byte buffer {Version=0x00010000, PL1, PL2}. Without it, NV_ERR_INVALID_DATA fires at line 2174.

  3. GPSP buffer too small: The SSDT allocates Buffer(0x28) (40 bytes), but the driver's PFM_REQ_HNDLR_PSHAREDATA struct is 44 bytes (0x2C). The ppmd field at offset 0x28 is outside the buffer. The PPMD sensor is never populated, causing NV_ERR_INVALID_DATA at line 2117.

Evidence

Stock BIOS func 0x02 return (49 bytes):

TGPA (offset 0x05): 0x0000 = 0W    ← should be 65W (0x0208)
TGPD (offset 0x07): 0x0000 = 0W
TPPA (offset 0x19): 0x0000 = 0W    ← should be 135W (0x0438)
MAGA (offset 0x1D): 0x0000 = 0W    ← should be 120W (0x03C0)

func 0x00 return: Buffer(4){0xBF,0x06,0x00,0x00} — bit 0 IS set, so PMO constructs. But all downstream func 0x02/0x01 fields are zeroed.

nvidia-smi after boot (stock):

Current Power Limit:  35.00W  (VBIOS default — never boosts)
Default Power Limit:  35.00W
Max Power Limit:      65.00W  (hardware maximum, never reached)
Requested Power Limit: N/A    (nvidia-powerd never requests boost)

nvidia-powerd: Runs, DBus connected, zero activity logs. Falls back to "SBIOS support not found for NVPCF GET_SUPPORTED" → default TPP.

dmesg:

NVRM: GPU0 PlatformRequestHandler failed to get target temp from SBIOS @ platform_request_handler_ctrl.c:2174
NVRM: GPU0 PlatformRequestHandler failed to get platform power mode from SBIOS @ platform_request_handler_ctrl.c:2117

GPS func 0x24 (GETPPL): Returns 0x80000002 (not implemented — Case(0x24) missing from switch)

GPS func 0x2A (PSHAREPARAMS): Returns 40-byte buffer but RETN=0x00000000 — no sensors declared, PPMD never populated

Fix

ACPI SSDT override via CONFIG_ACPI_TABLE_UPGRADE initrd mechanism. Two changes:

1. NPCF Device Embedded in DSDT

Embed Device(NPCF) at Scope(\_SB) in the DSDT. The nvidia-acpi driver hardcodes pathname "\\_SB.NPCF._DSM" (nv-acpi.c:785-793) — NPCF must be at \_SB scope.

Complete NPCF device with corrected budgets:

Scope (\_SB)
{
    Device (NPCF)
    {
        Name (_HID, "NVDA0820")
        Name (_UID, "NPCF")
        Name (_STA, 0x0F)

        Method (_INI, 0, NotSerialized)
        {
            If (CondRefOf (\_SB.PC00.PEG1.PEGP.GPSS))
                ^^PC00.PEG1.PEGP.GPSS = One
            If (CondRefOf (\_SB.PC00.PEG1.PEGP.NPCS))
                ^^PC00.PEG1.PEGP.NPCS = One
        }

        Method (_DSM, 4, Serialized)
        {
            If ((Arg0 == ToUUID ("36b49710-2483-11e7-9598-0800200c9a66")))
            {
                Return (NPCF (Arg0, Arg1, Arg2, Arg3))
            }
            Return (Buffer (One) { 0x00 })
        }

        Method (NPCF, 4, NotSerialized)
        {
            Switch (ToInteger (Arg2))
            {
                Case (Zero)
                {
                    Return (Buffer (4)
                    {
                        0x07, 0x01, 0x00, 0x00   // bit0=PMO, bit8=DC support
                    })
                }
                Case (One)
                {
                    // 14-byte static config — checksum 0xAB
                    Return (Buffer (0x0E)
                    {
                        0x20, 0x03, 0x01, 0x00, 0x25, 0x04, 0x05,
                        0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0xAB
                    })
                }
                Case (0x02)
                {
                    // 49-byte dynamic params
                    Name (PBD2, Buffer (0x31)
                    {
                        0x25, 0x05, 0x10, 0x1C, 0x01  // header: ver=0x25, hdr=5, com=16, ent=28, count=1
                    })
                    CreateWordField (PBD2, 0x05, TGPA)  // CTGP AC offset
                    CreateWordField (PBD2, 0x07, TGPD)  // CTGP DC offset
                    CreateWordField (PBD2, 0x19, TPPA)  // Target PP AC
                    CreateWordField (PBD2, 0x1B, TPPD)  // Target PP DC
                    CreateWordField (PBD2, 0x1D, MAGA)  // Max AC GPU
                    CreateWordField (PBD2, 0x21, MIGA)  // Max Idle GPU
                    TGPA = 0x0208   // 65W (520 * 125mW)
                    TPPA = 0x0438   // 135W (1080 * 125mW)
                    MAGA = 0x03C0   // 120W (960 * 125mW)
                    Return (PBD2)
                }
                Case (0x08)
                {
                    // 22-byte DC system power limits
                    Return (Buffer (0x16)
                    {
                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                        0x00
                    })
                }
            }
            Return (Buffer (One) { 0x00 })
        }
    }
}

Budget values explained:

  • TGPA = 0x0208 → 520 × 125mW = 65,000mW = 65W (TGP + CTGP + PPAB)
  • TPPA = 0x0438 → 1080 × 125mW = 135,000mW = 135W (total platform power budget)
  • MAGA = 0x03C0 → 960 × 125mW = 120,000mW = 120W (max AC GPU allocation)

The driver extracts these at client_resource.c:3160-3173:

pParams->ctgpOffsetmW = (NvS32)125 * (NvS16)DRF_VAL(..._CMD0_CTGP_AC_OFFSET, commonOut.param0);
pParams->targetTppOffsetmW = (NvS32)125 * (NvS16)DRF_VAL(..._CMD0_SIGNED0, entriesOut.param1);
pParams->maxOutputOffsetmW = (NvS32)125 * (NvS16)DRF_VAL(..._CMD0_SIGNED0, entriesOut.param2);

2. GPS Pipeline Fix (SSDT#4)

The GPS method in the NPCF SSDT needs three patches:

a) Remove GPSS guard from _DSM dispatcher (line 232):

// BEFORE:
If ((\_SB.PC00.PEG1.PEGP.GPSS != Zero))
{ Return (\_SB.PC00.PEG1.PEGP.GPS (Arg0, Arg1, Arg2, Arg3)) }

// AFTER: (guard removed)
Return (\_SB.PC00.PEG1.PEGP.GPS (Arg0, Arg1, Arg2, Arg3))

b) Add Case(0x24) for GETPPL:

Case (0x24)  // GPS_FUNC_GETPPL
{
    Return (Buffer (0x0C)
    {
        0x00, 0x00, 0x01, 0x00,  // Version: Major=1 (0x00010000)
        0xC8, 0xAF, 0x00, 0x00,  // PL1: 45000 mW (45W)
        0xE8, 0xFD, 0x00, 0x00   // PL2: 65000 mW (65W)
    })
}

The driver validates at platform_request_handler_ctrl.c:629-641:

if (FLD_TEST_DRF(0000_CTRL_PFM_REQ_HNDLR, _PPL_ARG0_VERSION, _MAJOR, _V1,
                 acpiParamsEx.output[0]))  // output[0] bits 31:16 must == 1

c) Enlarge GPSP buffer from 0x28 to 0x2C and set PPMD status bit:

// BEFORE: Buffer(0x28) with 9 DWordFields (missing CTGP, PPMD)
// AFTER:  Buffer(0x2C) with 11 DWordFields

Name (GPSP, Buffer (0x2C) {})  // was 0x28 (40 bytes)
CreateDWordField (GPSP, 0x00, RETN)
CreateDWordField (GPSP, 0x04, VRV1)
CreateDWordField (GPSP, 0x08, TGPU)
CreateDWordField (GPSP, 0x0C, PDTS)
CreateDWordField (GPSP, 0x10, SFAN)
CreateDWordField (GPSP, 0x14, SKNT)
CreateDWordField (GPSP, 0x18, CPUE)
CreateDWordField (GPSP, 0x1C, TMP1)
CreateDWordField (GPSP, 0x20, TMP2)
CreateDWordField (GPSP, 0x24, CTGP)  // NEW: maps to driver ctgp at offset 0x24
CreateDWordField (GPSP, 0x28, PPMD)  // NEW: maps to driver ppmd at offset 0x28

// In Case(Zero) — SUPPORTED_FIELDS query:
RETN = 0x00010100  // bit 8 = TGPU, bit 16 = PPMD
// was 0x00000000 — no sensors declared

The driver reads PPMD at platform_request_handler_ctrl.c:1208-1219:

if (0 != DRF_VAL(..._PSHARE_PARAMS_STATUS, _PPMD, pPShareParams->status))
{
    _pfmreqhndlrUpdatePpmdLimit(pPlatformRequestHandler, pGpu, NV_TRUE);
}

3. OEM Revision Bump (All 22 CB-01 SSDTs)

Every CB-01 SSDT must have OEM Revision ≥ 0x02. The kernel's acpi_table_initrd_override() silently skips if firmware_rev >= initrd_rev. Factory SSDTs have rev 0x01. Hex-patch byte offset 24-27 from 01 00 00 00 to 02 00 00 00 and recalculate the ACPI checksum.

After Fix

Current Power Limit:  65.00W  (Dynamic Boost active)
Synthetic load:       65W sustained (cuda-matmul 30s)
Genshin Impact avg:   53.1W (with CPU -75mV undervolt on shared heatsink)
AE_ALREADY_EXISTS:    0 (was 242)
Touchpad:             working (preserved)
GPS GETPPL:           {V1, PL1=45W, PL2=65W}
GPS PSHAREPARAMS:     44-byte buffer with PPMD=7 sensor

Key Driver Paths

  • nv-acpi.c:785-793 — hardcodes pathname "\\_SB.NPCF._DSM" for all NVPCF DSM calls
  • nv-acpi.c:361-365 — namespace walk discovers NPCF, overwrites globals unconditionally (last-wins)
  • client_resource.c:2942-3002cliresCtrlCmdSystemNVPCFGetPowerModeInfo_IMPL dispatches via osCallACPI_DSM
  • client_resource.c:2992-2994 — GET_SUPPORTED checks only bit 0 of supportedFuncs
  • client_resource.c:3100-3173 — func 0x02 validates header (0x05/0x10/0x1C), extracts TGPA/TPPA/MAGA
  • client_resource.c:2898-2940 — func 0x01 _validateConfigStaticTable_2x two's complement checksum
  • dynamic-power.c:1202-1223os_get_dynamic_boost_support probes via func 0x00
  • platform_request_handler_ctrl.c:601-643 — GETPPL (func 0x24) validates Major version = 1
  • platform_request_handler_ctrl.c:1060-1088pfmreqhndlrCallACPI_EX copies input→output, calls osCallACPI_DSM
  • platform_request_handler_ctrl.c:2174 — TGPU sensor update fails when PSHAREPARAMS incomplete
  • platform_request_handler_ctrl.c:2117 — PPMD sensor update fails when ppmd field missing
  • os.c:2934-3048osCallACPI_DSM maps function IDs to GUIDs and revisions

Question for Maintainers

  1. Driver-side fallback: When func 0x02 returns all-zero budget fields, the driver silently falls back to VBIOS defaults with no warning. A pr_warn when ctgpOffsetmW == 0 && maxOutputOffsetmW == 0 (after func 0x00 reports support) would help users diagnose this class of firmware bug. Is this intentional?

  2. Should the driver validate func 0x02 budget values? Currently client_resource.c validates the header structure (sizes, version) but not whether the extracted TGPA/TPPA/MAGA are non-zero when bit 0 of func 0x00 indicates support. Adding a check like if (pParams->ctgpOffsetmW == 0 && pParams->maxOutputOffsetmW == 0) { pr_warn(NV, "NPCF: zeroed power budget detected"); } would surface Lenovo's firmware bug at boot.

  3. PPMD sensor initialization: The PlatformRequestHandler NV_ERR_INVALID_DATA errors at lines 2174/2117 fire when PSHAREPARAMS doesn't populate the PPMD field. Is this expected for laptops without platform power mode support, or should the driver handle the missing PPMD gracefully?


Full patched SSDT source, scripts, and power measurement data available at loq-npcf.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions