feat: add laptop/display brightness modulation
This commit is contained in:
1 parent
ef1e69ae83
commit
7979acc2eb
34 files changed
+1308
-78
No files matched your search
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* AcpiVideo.cpp
|
||||
* ACPI video extensions (_BCL/_BCM/_BQC) panel brightness fallback
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "AcpiVideo.hpp"
|
||||
#include "AcpiPower.hpp"
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace AcpiVideo {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// State
|
||||
|
||||
// ============================================================================
|
||||
static constexpr int MaxLevels = 101;
|
||||
|
||||
static kcp::Mutex g_lock;
|
||||
static bool g_probed = false;
|
||||
static int32_t g_output = -1; // output device carrying _BCM
|
||||
static bool g_hasBqc = false;
|
||||
static uint32_t g_levels[MaxLevels]; // ascending, de-duplicated
|
||||
static int g_levelCount = 0;
|
||||
static int g_percent = -1; // last level set, when no _BQC
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Discovery
|
||||
|
||||
// ============================================================================
|
||||
static bool HasMethod(int32_t node, const char* name) {
|
||||
return AML::GetInterpreter().FindChild(node, name) >= 0;
|
||||
}
|
||||
|
||||
// _ADR display output IDs (ACPI 6.x, B.4.2): bits 11:8 are the type,
|
||||
// 4 = internal flat panel. The panel is the output to drive.
|
||||
static int32_t FindOutput() {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
auto& ns = interp.GetNamespace();
|
||||
|
||||
int32_t fallback = -1;
|
||||
auto& devices = Pci::GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.ClassCode != 0x03) continue;
|
||||
int32_t gpu = AcpiPower::FindPciDevice(d.Bus, d.Device, d.Function);
|
||||
if (gpu < 0) continue;
|
||||
|
||||
int32_t panel = -1;
|
||||
ns.ForEachChild(gpu, AML::ObjectType::Device,
|
||||
[&](int32_t idx, const AML::NamespaceNode*) {
|
||||
if (!HasMethod(idx, "_BCM") || !HasMethod(idx, "_BCL")) return true;
|
||||
AML::Object adr{};
|
||||
uint64_t id = interp.EvaluateChild(idx, "_ADR", nullptr, 0, adr)
|
||||
? AcpiPower::ResultInteger(adr) : 0;
|
||||
if (((id >> 8) & 0xF) == 0x4) {
|
||||
panel = idx;
|
||||
return false;
|
||||
}
|
||||
if (fallback < 0) fallback = idx;
|
||||
return true;
|
||||
});
|
||||
if (panel >= 0) return panel;
|
||||
}
|
||||
if (fallback >= 0) return fallback;
|
||||
|
||||
// Some firmware hangs the output device off a bridge the PCI scan
|
||||
// could not map; any _BCM in the namespace is the only one there is.
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Method,
|
||||
[&](int32_t idx, const AML::NamespaceNode* n) {
|
||||
if (fallback >= 0 || n->Name[0] != '_' || n->Name[1] != 'B'
|
||||
|| n->Name[2] != 'C' || n->Name[3] != 'M')
|
||||
return;
|
||||
const AML::NamespaceNode* node = ns.GetNode(idx);
|
||||
if (node && node->ParentIndex >= 0 && HasMethod(node->ParentIndex, "_BCL"))
|
||||
fallback = node->ParentIndex;
|
||||
});
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// _BCL: { level on AC, level on battery, supported levels... }. Only
|
||||
// the supported list counts; firmware often repeats or misorders it.
|
||||
static bool ReadLevels() {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
AML::Object bcl{};
|
||||
if (!interp.EvaluateChild(g_output, "_BCL", nullptr, 0, bcl)
|
||||
|| bcl.Type != AML::ObjectType::Package)
|
||||
return false;
|
||||
|
||||
g_levelCount = 0;
|
||||
int count = interp.GetPackageCount(bcl);
|
||||
for (int i = 2; i < count; i++) {
|
||||
AML::Object elem{};
|
||||
if (!interp.GetPackageElement(bcl, i, elem)
|
||||
|| elem.Type != AML::ObjectType::Integer)
|
||||
continue;
|
||||
uint32_t level = (uint32_t)elem.Integer;
|
||||
int pos = 0;
|
||||
while (pos < g_levelCount && g_levels[pos] < level) pos++;
|
||||
if (pos < g_levelCount && g_levels[pos] == level) continue;
|
||||
if (g_levelCount == MaxLevels) break;
|
||||
for (int j = g_levelCount; j > pos; j--) g_levels[j] = g_levels[j - 1];
|
||||
g_levels[pos] = level;
|
||||
g_levelCount++;
|
||||
}
|
||||
return g_levelCount >= 2 && g_levels[g_levelCount - 1] > 0;
|
||||
}
|
||||
|
||||
static void Probe() {
|
||||
g_probed = true;
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return;
|
||||
|
||||
g_output = FindOutput();
|
||||
if (g_output < 0) return;
|
||||
if (!ReadLevels()) {
|
||||
g_output = -1;
|
||||
return;
|
||||
}
|
||||
g_hasBqc = HasMethod(g_output, "_BQC");
|
||||
// Without _BQC the level is only known once we set one; until
|
||||
// then firmware's boot level is assumed to be full brightness.
|
||||
if (!g_hasBqc) g_percent = 100;
|
||||
|
||||
char path[128];
|
||||
interp.GetNamespace().GetNodePath(g_output, path, sizeof(path));
|
||||
KernelLogStream(INFO, "AcpiVideo") << "Brightness via " << path
|
||||
<< "._BCM, " << base::dec << (uint64_t)g_levelCount << " levels"
|
||||
<< (g_hasBqc ? ", _BQC" : "");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Levels
|
||||
|
||||
// ============================================================================
|
||||
static int LevelToPercent(uint32_t level) {
|
||||
uint32_t top = g_levels[g_levelCount - 1];
|
||||
if (level > top) level = top;
|
||||
return (int)(((uint64_t)level * 100 + top / 2) / top);
|
||||
}
|
||||
|
||||
// The supported level closest to the requested share of the top one.
|
||||
static uint32_t PercentToLevel(int percent) {
|
||||
uint64_t target = (uint64_t)g_levels[g_levelCount - 1] * (uint32_t)percent / 100;
|
||||
uint32_t best = g_levels[0];
|
||||
uint64_t bestDistance = ~0ULL;
|
||||
for (int i = 0; i < g_levelCount; i++) {
|
||||
uint64_t distance = g_levels[i] > target ? g_levels[i] - target
|
||||
: target - g_levels[i];
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = g_levels[i];
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static int Query() {
|
||||
if (g_hasBqc) {
|
||||
AML::Object result{};
|
||||
if (AML::GetInterpreter().EvaluateChild(g_output, "_BQC", nullptr, 0, result)
|
||||
&& result.Type == AML::ObjectType::Integer)
|
||||
return LevelToPercent((uint32_t)result.Integer);
|
||||
}
|
||||
return g_percent;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Public API
|
||||
|
||||
// ============================================================================
|
||||
int Brightness(int percent) {
|
||||
g_lock.Acquire();
|
||||
if (!g_probed) Probe();
|
||||
if (g_output < 0) {
|
||||
g_lock.Release();
|
||||
return -1;
|
||||
}
|
||||
|
||||
int result;
|
||||
if (percent < 0) {
|
||||
result = Query();
|
||||
} else {
|
||||
if (percent > 100) percent = 100;
|
||||
uint32_t level = PercentToLevel(percent);
|
||||
AML::Object arg = AcpiPower::IntegerArg(level);
|
||||
AML::Object ignored{};
|
||||
if (AML::GetInterpreter().EvaluateChild(g_output, "_BCM", &arg, 1, ignored)) {
|
||||
g_percent = LevelToPercent(level);
|
||||
int reported = Query();
|
||||
result = reported >= 0 ? reported : g_percent;
|
||||
} else {
|
||||
result = -2;
|
||||
}
|
||||
}
|
||||
g_lock.Release();
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* AcpiVideo.hpp
|
||||
* ACPI video extensions (_BCL/_BCM/_BQC) panel brightness fallback
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
namespace AcpiVideo {
|
||||
|
||||
// Panel brightness through the firmware's ACPI video output device,
|
||||
// for machines no native backlight driver covers (a panel wired to a
|
||||
// discrete GPU, a GPU Montauk has no driver for). percent < 0 queries;
|
||||
// otherwise the nearest level the firmware lists is selected. Returns
|
||||
// the resulting 0..100, -1 when the firmware has no brightness
|
||||
// methods, -2 when evaluating them failed. Discovery runs on first
|
||||
// use. May sleep (AML).
|
||||
int Brightness(int percent);
|
||||
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user