fix: add backlight modulation support for hybrid graphics laptops
This commit is contained in:
1 parent
7979acc2eb
commit
f9e21e0f13
6 files changed
+381
-12
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user