fix: add backlight modulation support for hybrid graphics laptops

This commit is contained in:
daniel committed 2026-10-02 14:54:00 +02:00
1 parent 7979acc2eb
commit f9e21e0f13
6 files changed
+381 -12

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
/*
* AcpiWmi.cpp
* ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call
* its WMxx method
* Copyright (c) 2026 Daniel Hammer
*/
#include "AcpiWmi.hpp"
#include "AcpiDevices.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <Libraries/Memory.hpp>
#include <CppLib/Stream.hpp>
#include <Terminal/Terminal.hpp>
using namespace Kt;
namespace Hal {
namespace AcpiWmi {
static constexpr const char* HidWmi = "PNP0C14";
static constexpr uint32_t WdgEntrySize = 20; // GUID, id, count, flags
static int HexDigit(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
bool ParseGuid(const char* text, uint8_t out[16]) {
// Bytes in text order, then the first three fields byte-swapped.
uint8_t bytes[16];
int count = 0;
for (int i = 0; text[i]; i++) {
if (text[i] == '-') continue;
int hi = HexDigit(text[i]);
int lo = text[i + 1] ? HexDigit(text[i + 1]) : -1;
if (hi < 0 || lo < 0 || count == 16) return false;
bytes[count++] = (uint8_t)(hi << 4 | lo);
i++;
}
if (count != 16) return false;
static constexpr int order[16] = {3, 2, 1, 0, 5, 4, 7, 6,
8, 9, 10, 11, 12, 13, 14, 15};
for (int i = 0; i < 16; i++) out[i] = bytes[order[i]];
return true;
}
static bool SearchDevice(int32_t device, const uint8_t guid[16], Block& out) {
auto& interp = AML::GetInterpreter();
AML::Object wdg{};
if (!interp.EvaluateChild(device, "_WDG", nullptr, 0, wdg)
|| wdg.Type != AML::ObjectType::Buffer)
return false;
// The interpreter caps buffers; a _WDG that fills the cap may have
// lost entries past it.
if (wdg.Buffer.Length >= (uint32_t)AML::MaxBufferLen) {
char path[128];
interp.GetNamespace().GetNodePath(device, path, sizeof(path));
KernelLogStream(WARNING, "WMI") << path << "._WDG may be truncated ("
<< base::dec << (uint64_t)wdg.Buffer.Length << " bytes read)";
}
for (uint32_t off = 0; off + WdgEntrySize <= wdg.Buffer.Length; off += WdgEntrySize) {
const uint8_t* entry = wdg.Buffer.Data + off;
if (memcmp(entry, guid, 16) != 0) continue;
out.Device = device;
out.ObjectId[0] = (char)entry[16];
out.ObjectId[1] = (char)entry[17];
out.InstanceCount = entry[18];
out.Flags = entry[19];
return true;
}
return false;
}
bool Find(const uint8_t guid[16], Block& out) {
auto& interp = AML::GetInterpreter();
if (!interp.IsInitialized()) return false;
auto& ns = interp.GetNamespace();
bool found = false;
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
[&](int32_t idx, const AML::NamespaceNode*) {
if (found) return;
char hid[16];
if (!AcpiDevices::EvaluateHid(idx, hid, sizeof(hid))) return;
if (memcmp(hid, HidWmi, sizeof("PNP0C14")) != 0) return;
if (!(AcpiDevices::EvaluateSta(idx) & AcpiDevices::STA_PRESENT)) return;
found = SearchDevice(idx, guid, out);
});
return found;
}
bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId,
const uint8_t* input, uint32_t inputLength,
AML::Object& result) {
if (!(block.Flags & FlagMethod) || instance >= block.InstanceCount)
return false;
AML::Object args[3];
args[0].Type = AML::ObjectType::Integer;
args[0].Integer = instance;
args[1].Type = AML::ObjectType::Integer;
args[1].Integer = methodId;
if (block.Flags & FlagString) {
uint32_t n = inputLength;
if (n >= (uint32_t)AML::MaxStringLen) n = AML::MaxStringLen - 1;
args[2].Type = AML::ObjectType::String;
if (n) memcpy(args[2].String.Data, input, n);
args[2].String.Data[n] = '\0';
args[2].String.Length = (uint16_t)n;
} else {
uint32_t n = inputLength;
if (n > (uint32_t)AML::MaxBufferLen) n = AML::MaxBufferLen;
args[2].Type = AML::ObjectType::Buffer;
if (n) memcpy(args[2].Buffer.Data, input, n);
args[2].Buffer.Length = n;
}
char method[5] = {'W', 'M', block.ObjectId[0], block.ObjectId[1], '\0'};
return AML::GetInterpreter().EvaluateChild(block.Device, method, args, 3, result);
}
};
};
+44
View File
@@ -0,0 +1,44 @@
/*
* AcpiWmi.hpp
* ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call
* its WMxx method
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <ACPI/AML/AmlNamespace.hpp>
namespace Hal {
namespace AcpiWmi {
// _WDG flags (Microsoft "Windows Instrumentation: WMI and ACPI").
static constexpr uint8_t FlagExpensive = 0x1;
static constexpr uint8_t FlagMethod = 0x2;
static constexpr uint8_t FlagString = 0x4;
static constexpr uint8_t FlagEvent = 0x8;
// One _WDG entry, bound to the PNP0C14 device that declared it.
struct Block {
int32_t Device; // namespace node of the PNP0C14 device
char ObjectId[2]; // names the WQxx / WSxx / WMxx methods
uint8_t InstanceCount;
uint8_t Flags;
};
// Parse "603E9613-EF25-4338-A3D0-C46177516DB7" into the byte order
// _WDG stores (first three fields little-endian). False if malformed.
bool ParseGuid(const char* text, uint8_t out[16]);
// Search every present PNP0C14 device's _WDG for `guid`. The first
// declaration wins, as on Linux.
bool Find(const uint8_t guid[16], Block& out);
// Evaluate WMxx(instance, methodId, input) on a method block. The input
// is passed as a String instead of a Buffer when the block says so.
bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId,
const uint8_t* input, uint32_t inputLength,
AML::Object& result);
};
};
+160
View File
@@ -0,0 +1,160 @@
/*
* WmiEcBacklight.cpp
* Firmware (EC) panel backlight behind the WMI interface NVIDIA specified
* for display-mux laptops
* Copyright (c) 2026 Daniel Hammer
*/
#include "WmiEcBacklight.hpp"
#include "AcpiWmi.hpp"
#include "AcpiPower.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <CppLib/Spinlock.hpp>
#include <CppLib/Stream.hpp>
#include <Terminal/Terminal.hpp>
using namespace Kt;
namespace Hal {
namespace WmiEcBacklight {
// ============================================================================
// Interface
// ============================================================================
static constexpr const char* Guid = "603E9613-EF25-4338-A3D0-C46177516DB7";
// WMxx method IDs.
static constexpr uint32_t MethodLevel = 1;
static constexpr uint32_t MethodSource = 2;
// `mode` values in the argument block.
static constexpr uint32_t ModeGet = 0;
static constexpr uint32_t ModeSet = 1;
static constexpr uint32_t ModeGetMaxLevel = 2;
static constexpr uint64_t SourceEc = 2; // 1 = GPU, 3 = eDP AUX
// Lowest level driven, so the panel never goes fully dark.
static constexpr int MinPercent = 1;
// ============================================================================
// State
// ============================================================================
static kcp::Mutex g_lock;
static bool g_probed = false;
static bool g_available = false;
static AcpiWmi::Block g_block{};
static uint32_t g_maxLevel = 0;
// Argument block: { u32 mode; u32 val; u32 ret; u32 reserved[3]; }.
// The method answers with an Integer. Linux reads it back as the
// `ret` field of the same block, which is where ACPICA's copy-out of
// an Integer puts the value; a firmware returning the block itself as
// a Buffer is read at that same offset.
static bool Call(uint32_t method, uint32_t mode, uint32_t value, uint32_t& out) {
uint8_t args[24] = {};
args[0] = (uint8_t)mode;
args[1] = (uint8_t)(mode >> 8);
args[2] = (uint8_t)(mode >> 16);
args[3] = (uint8_t)(mode >> 24);
args[4] = (uint8_t)value;
args[5] = (uint8_t)(value >> 8);
args[6] = (uint8_t)(value >> 16);
args[7] = (uint8_t)(value >> 24);
AML::Object result{};
if (!AcpiWmi::EvaluateMethod(g_block, 0, method, args, sizeof(args), result))
return false;
if (result.Type == AML::ObjectType::Integer) {
out = (uint32_t)result.Integer;
return true;
}
if (result.Type == AML::ObjectType::Buffer && result.Buffer.Length >= 12) {
const uint8_t* r = result.Buffer.Data + 8;
out = r[0] | (uint32_t)r[1] << 8 | (uint32_t)r[2] << 16 | (uint32_t)r[3] << 24;
return true;
}
// A set may legitimately return nothing.
out = 0;
return mode == ModeSet;
}
static void Probe() {
g_probed = true;
uint8_t guid[16];
if (!AcpiWmi::ParseGuid(Guid, guid) || !AcpiWmi::Find(guid, g_block)) return;
if (!(g_block.Flags & AcpiWmi::FlagMethod) || g_block.InstanceCount == 0) return;
char path[128];
AML::GetInterpreter().GetNamespace().GetNodePath(g_block.Device, path, sizeof(path));
// The interface also exists on machines where the GPU drives the
// backlight; there it must be left alone.
uint32_t source = 0;
if (!Call(MethodSource, ModeGet, 0, source)) {
KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path
<< ": source query failed";
return;
}
if (source != SourceEc) {
KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path
<< " reports source " << base::dec << (uint64_t)source << ", not EC";
return;
}
uint32_t maximum = 0;
if (!Call(MethodLevel, ModeGetMaxLevel, 0, maximum) || maximum == 0) {
KernelLogStream(WARNING, "Backlight") << "WMI EC backlight at " << path
<< ": no maximum level";
return;
}
g_maxLevel = maximum;
g_available = true;
char method[5] = {'W', 'M', g_block.ObjectId[0], g_block.ObjectId[1], '\0'};
KernelLogStream(OK, "Backlight") << "Panel brightness via EC (WMI "
<< path << "." << method << ", " << base::dec << (uint64_t)maximum
<< " levels)";
}
static int ToPercent(uint32_t level) {
if (level > g_maxLevel) level = g_maxLevel;
return (int)(((uint64_t)level * 100 + g_maxLevel / 2) / g_maxLevel);
}
// ============================================================================
// Public API
// ============================================================================
int Brightness(int percent) {
g_lock.Acquire();
if (!g_probed) Probe();
int result = -1;
if (g_available) {
uint32_t level = 0;
if (percent >= 0) {
if (percent > 100) percent = 100;
if (percent < MinPercent) percent = MinPercent;
uint32_t target = (uint32_t)(((uint64_t)g_maxLevel * (uint32_t)percent + 50) / 100);
if (target == 0) target = 1;
uint32_t ignored = 0;
if (!Call(MethodLevel, ModeSet, target, ignored)) {
result = -2;
} else {
result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level)
: ToPercent(target);
}
} else {
result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level) : -2;
}
}
g_lock.Release();
return result;
}
};
};
+28
View File
@@ -0,0 +1,28 @@
/*
* WmiEcBacklight.hpp
* Firmware (EC) panel backlight behind the WMI interface NVIDIA specified
* for display-mux laptops
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal {
namespace WmiEcBacklight {
// On a laptop whose panel can be switched between GPUs, firmware may
// wire the backlight to the embedded controller instead of either
// GPU and expose it through WMI GUID 603E9613-EF25-4338-A3D0-
// C46177516DB7. Despite the name the interface is pure firmware: it
// works with the discrete GPU powered off. It is used only when the
// firmware itself reports the EC as the brightness source, which is
// also when GPU PWM writes stop reaching the panel.
//
// percent < 0 queries. Returns 0..100, -1 when this interface is not
// the panel's brightness source, -2 when the firmware call failed.
// Discovery runs on first use. May sleep (AML).
int Brightness(int percent);
};
};