feat: add laptop/display brightness modulation

This commit is contained in:
daniel committed 2026-10-02 12:14:40 +02:00
1 parent ef1e69ae83
commit 7979acc2eb
34 files changed
+1308 -78

No files matched your search

+212
View File
@@ -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;
}
};
};
+23
View File
@@ -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);
};
};