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

+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;
}
};
};