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);
};
};
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 283 #define MONTAUK_BUILD_NUMBER 287
+29 -2
View File
@@ -11,6 +11,7 @@
#include <Memory/HHDM.hpp> #include <Memory/HHDM.hpp>
#include <Graphics/Framebuffer.hpp> #include <Graphics/Framebuffer.hpp>
#include <Drivers/Graphics/IntelGPU.hpp> #include <Drivers/Graphics/IntelGPU.hpp>
#include <ACPI/AcpiVideo.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include "Syscall.hpp" #include "Syscall.hpp"
@@ -128,8 +129,30 @@ namespace montauk::abi {
return GPU::Flip((int)index, (flags & 1) != 0, proc->pid); return GPU::Flip((int)index, (flags & 1) != 0, proc->pid);
} }
// The firmware's ACPI brightness methods drive the built-in panel, so
// they stand in only when that panel is what is on screen (or no native
// driver knows what is): never for an external monitor without DDC/CI.
static bool AcpiBrightnessApplies() {
if (!Drivers::Graphics::IntelGPU::IsInitialized()) return true;
uint8_t type = Drivers::Graphics::IntelGPU::ActiveConnectorType();
return type == DISPLAY_CONNECTOR_EDP || type == DISPLAY_CONNECTOR_LVDS;
}
static int DisplayBrightness(int percent) {
int rc = Drivers::Graphics::IntelGPU::Brightness(percent);
if (rc == -1 && AcpiBrightnessApplies())
rc = Hal::AcpiVideo::Brightness(percent);
return rc;
}
static int64_t Sys_DisplayInfo(DisplayInfo* out) { static int64_t Sys_DisplayInfo(DisplayInfo* out) {
return Drivers::Graphics::IntelGPU::GetDisplayInfo(out) ? Success : NoDevice; if (!Drivers::Graphics::IntelGPU::GetDisplayInfo(out)) return NoDevice;
if (out->brightness < 0 && AcpiBrightnessApplies()) {
out->brightness = Hal::AcpiVideo::Brightness(-1);
if (out->brightness >= 0) out->capabilities |= DISPLAY_CAP_BRIGHTNESS;
else out->brightness = -1;
}
return Success;
} }
static int64_t Sys_DisplayModes(DisplayModeInfo* out, int maxCount) { static int64_t Sys_DisplayModes(DisplayModeInfo* out, int maxCount) {
@@ -145,8 +168,12 @@ namespace montauk::abi {
return rc; return rc;
} }
// percent < 0 queries. Returns 0..100, NoDevice when the active output
// has no controllable backlight, IoFailure when the hardware refused.
static int64_t Sys_DisplayBrightness(int percent) { static int64_t Sys_DisplayBrightness(int percent) {
return Drivers::Graphics::IntelGPU::Brightness(percent); int rc = DisplayBrightness(percent);
if (rc >= 0) return rc;
return rc == -1 ? NoDevice : IoFailure;
} }
static uint64_t Sys_TermSize() { static uint64_t Sys_TermSize() {
+1 -1
View File
@@ -207,7 +207,7 @@ namespace montauk::abi {
return Sys_DisplaySetMode((int)frame->arg1); return Sys_DisplaySetMode((int)frame->arg1);
case SYS_DISPLAYBRIGHTNESS: case SYS_DISPLAYBRIGHTNESS:
if ((int64_t)frame->arg1 >= 0 && if ((int64_t)frame->arg1 >= 0 &&
!Sched::Consent::Require(CAP_DISPLAY_ADMIN)) return CapabilityDenied; !Sched::Consent::Require(CAP_BRIGHTNESS)) return CapabilityDenied;
return Sys_DisplayBrightness((int)frame->arg1); return Sys_DisplayBrightness((int)frame->arg1);
case SYS_GETEXECPATH: case SYS_GETEXECPATH:
if (!UserMemory::Range(frame->arg2 ? frame->arg1 : frame->arg1, frame->arg2, true)) return BadPointer; if (!UserMemory::Range(frame->arg2 ? frame->arg1 : frame->arg1, frame->arg2, true)) return BadPointer;
+9 -3
View File
@@ -385,8 +385,14 @@ namespace montauk::abi {
never part of a session mask, never part of "all", and never never part of a session mask, never part of "all", and never
delegated by the compositor to what it launches. */ delegated by the compositor to what it launches. */
static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13; static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13;
static constexpr uint64_t CAP_ALL = (1ULL << 14) - 1; /* Set the backlight of the active display (SYS_DISPLAYBRIGHTNESS).
static constexpr uint64_t CAP_STANDARD_SESSION = CAP_POWER_REQUEST | CAP_SUSPEND; A seat preference rather than display administration, so every
session holds it; CAP_DISPLAY_ADMIN keeps mode setting. Reading the
level needs nothing. */
static constexpr uint64_t CAP_BRIGHTNESS = 1ULL << 14;
static constexpr uint64_t CAP_ALL = (1ULL << 15) - 1;
static constexpr uint64_t CAP_STANDARD_SESSION =
CAP_POWER_REQUEST | CAP_SUSPEND | CAP_BRIGHTNESS;
static constexpr uint64_t CAP_ADMIN_SESSION = static constexpr uint64_t CAP_ADMIN_SESSION =
CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN | CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN |
CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN | CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN |
@@ -438,7 +444,7 @@ namespace montauk::abi {
deferred delegation, not amplification. */ deferred delegation, not amplification. */
static constexpr uint64_t CAP_CONSENT_ELIGIBLE = static constexpr uint64_t CAP_CONSENT_ELIGIBLE =
CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME |
CAP_DISPLAY_ADMIN | CAP_LOG_READ; CAP_DISPLAY_ADMIN | CAP_LOG_READ | CAP_BRIGHTNESS;
static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0, static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0,
"consent can only hand out authority an admin session holds"); "consent can only hand out authority an admin session holds");
+602 -47
View File
@@ -82,8 +82,7 @@ namespace Drivers::Graphics::IntelGPU {
static uint8_t g_connectorType = montauk::abi::DISPLAY_CONNECTOR_UNKNOWN; static uint8_t g_connectorType = montauk::abi::DISPLAY_CONNECTOR_UNKNOWN;
static bool g_connectorConnected = false; static bool g_connectorConnected = false;
static bool g_edidValid = false; static bool g_edidValid = false;
static bool g_pwmSupported = false; static int g_ddiPort = -1; // DDI port driving the output, -1 unknown
static uint32_t g_pwmCtlReg = 0;
static char g_connectorName[32] = "Unknown"; static char g_connectorName[32] = "Unknown";
static char g_monitorName[64] = "Unknown display"; static char g_monitorName[64] = "Unknown display";
static uint8_t g_auxEdid[256] = {}; static uint8_t g_auxEdid[256] = {};
@@ -556,10 +555,11 @@ namespace Drivers::Graphics::IntelGPU {
} }
static bool AuxI2cTransfer(uint8_t request, const uint8_t* writeData, static bool AuxI2cTransfer(uint8_t request, const uint8_t* writeData,
int writeSize, uint8_t* readData, int readSize) { int writeSize, uint8_t* readData, int readSize,
uint8_t i2cAddress = 0x50) {
for (int retry = 0; retry < 16; retry++) { for (int retry = 0; retry < 16; retry++) {
uint8_t reply = 0xFF; uint8_t reply = 0xFF;
int result = AuxTransfer(request, 0x50, writeData, writeSize, int result = AuxTransfer(request, i2cAddress, writeData, writeSize,
readData, readSize, &reply); readData, readSize, &reply);
uint8_t nativeReply = reply & 0x3; uint8_t nativeReply = reply & 0x3;
uint8_t i2cReply = reply & 0xC; uint8_t i2cReply = reply & 0xC;
@@ -600,7 +600,8 @@ namespace Drivers::Graphics::IntelGPU {
static bool ReadDpEdid() { static bool ReadDpEdid() {
g_auxEdidSize = 0; g_auxEdidSize = 0;
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP if ((g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP
&& g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP)
|| g_auxControlReg == 0) || g_auxControlReg == 0)
return false; return false;
@@ -789,9 +790,16 @@ namespace Drivers::Graphics::IntelGPU {
g_connectorConnected = (ReadReg(trans.conf) & PIPECONF_ENABLE) != 0; g_connectorConnected = (ReadReg(trans.conf) & PIPECONF_ENABLE) != 0;
uint32_t ddi = ReadReg(trans.ddi); uint32_t ddi = ReadReg(trans.ddi);
g_auxControlReg = 0; g_auxControlReg = 0;
g_ddiPort = -1;
if (g_gpuGen <= 7) { if (g_gpuGen <= 7) {
if (ReadReg(LVDS) & LVDS_PORT_ENABLE) { // Haswell (listed as Gen 7) has the eDP transcoder; Ironlake to
// Ivy Bridge drive eDP from the CPU's port A.
if (trans.edp || (ReadReg(DP_A) & (1u << 31))) {
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP;
CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1");
g_ddiPort = 0;
} else if (ReadReg(LVDS) & LVDS_PORT_ENABLE) {
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_LVDS; g_connectorType = montauk::abi::DISPLAY_CONNECTOR_LVDS;
CopyText(g_connectorName, sizeof(g_connectorName), "LVDS-1"); CopyText(g_connectorName, sizeof(g_connectorName), "LVDS-1");
} else if (ReadReg(ADPA) & ADPA_DAC_ENABLE) { } else if (ReadReg(ADPA) & ADPA_DAC_ENABLE) {
@@ -807,14 +815,30 @@ namespace Drivers::Graphics::IntelGPU {
} else if (trans.edp) { } else if (trans.edp) {
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP; g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP;
CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1"); CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1");
g_ddiPort = 0;
g_auxControlReg = DP_AUX_A_CTL;
} else { } else {
uint32_t mode = (ddi & TRANS_DDI_MODE_MASK) >> 24; uint32_t mode = (ddi & TRANS_DDI_MODE_MASK) >> 24;
uint32_t port = (ddi & TRANS_DDI_PORT_MASK) >> 27; // Gen12 widened the port select to bits 30:27 and stores port + 1
const char* suffix = port == 0 ? "A" : port == 1 ? "B" // (0 = none); earlier DDI parts store the port in bits 30:28.
: port == 2 ? "C" : port == 3 ? "D" // Gen12 numbering puts the Type-C ports from D (index 3) onward.
: port == 8 ? "TC1" : port == 9 ? "TC2" int port;
: port == 10 ? "TC3" : port == 11 ? "TC4" if (g_gpuGen >= 12) {
: "unknown"; uint32_t field = (ddi & TRANS_DDI_PORT_MASK) >> 27;
port = field ? (int)field - 1 : -1;
} else {
port = (int)((ddi >> 28) & 0x7);
}
g_ddiPort = port;
static const char* const legacyNames[] = {"A", "B", "C", "D", "E", "F"};
static const char* const tcNames[] = {"TC1", "TC2", "TC3", "TC4", "TC5", "TC6"};
const char* suffix = "unknown";
if (port >= 0 && g_gpuGen >= 12) {
if (port < 3) suffix = legacyNames[port];
else if (port - 3 < 6) suffix = tcNames[port - 3];
} else if (port >= 0 && port < 6) {
suffix = legacyNames[port];
}
if (mode == 0) { if (mode == 0) {
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_HDMI; g_connectorType = montauk::abi::DISPLAY_CONNECTOR_HDMI;
CopyText(g_connectorName, sizeof(g_connectorName), "HDMI-"); CopyText(g_connectorName, sizeof(g_connectorName), "HDMI-");
@@ -824,11 +848,9 @@ namespace Drivers::Graphics::IntelGPU {
} else { } else {
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_DP; g_connectorType = montauk::abi::DISPLAY_CONNECTOR_DP;
CopyText(g_connectorName, sizeof(g_connectorName), "DP-"); CopyText(g_connectorName, sizeof(g_connectorName), "DP-");
static constexpr uint32_t auxControls[] = { // AUX channels are laid out per port, Type-C ones included.
DP_AUX_A_CTL, DP_AUX_B_CTL, DP_AUX_C_CTL, DP_AUX_D_CTL, if (port >= 0 && port <= 8)
}; g_auxControlReg = DP_AUX_A_CTL + (uint32_t)port * 0x100;
if (port < sizeof(auxControls) / sizeof(auxControls[0]))
g_auxControlReg = auxControls[port];
} }
uint64_t len = 0; uint64_t len = 0;
while (len + 1 < sizeof(g_connectorName) && g_connectorName[len]) len++; while (len + 1 < sizeof(g_connectorName) && g_connectorName[len]) len++;
@@ -913,25 +935,544 @@ namespace Drivers::Graphics::IntelGPU {
} }
} }
static void DiscoverPwm() { // =========================================================================
g_pwmSupported = false; // Backlight
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP // =========================================================================
&& g_connectorType != montauk::abi::DISPLAY_CONNECTOR_LVDS)
return;
uint32_t pch = ReadReg(BLC_PWM_PCH_CTL2); // South-display family. The PWM register layout and the GMBUS pin map
if ((pch >> 16) != 0) { // follow the PCH, not the GPU: a Coffee Lake on Cannon Point and a Kaby
g_pwmCtlReg = BLC_PWM_PCH_CTL2; // Lake on Sunrise Point are both Gen 9 but have incompatible PWM blocks.
g_pwmSupported = true; enum class PchFamily : uint8_t { Unknown, Ibx, Lpt, Cnp, Icp };
enum class BacklightMethod : uint8_t { None, PwmIlk, PwmLpt, PwmCnp, DpcdAux, DdcCi };
static PchFamily g_pch = PchFamily::Unknown;
static BacklightMethod g_blMethod = BacklightMethod::None;
static uint32_t g_blFreqReg = 0; // PWM period register
static uint32_t g_blDutyReg = 0; // PWM duty register
static uint32_t g_blMax = 0; // DPCD / DDC full-scale value
static int g_blPercent = -1; // last level read or written
static bool g_dpcdModePending = false; // panel still on its PWM pin
static bool g_ddcCandidate = false; // external monitor, DDC/CI untried
static uint64_t g_ddcRetryMs = 0; // earliest re-probe after a failure
static uint64_t g_ddcLastMs = 0; // MCCS needs 50ms between commands
// Serializes backlight access: DDC/CI and DPCD changes are multi-step
// AUX/GMBUS transactions that must not interleave.
static kcp::Mutex g_blLock;
// Lowest level a panel is driven to. A zero duty cycle turns some panels
// fully dark, which leaves nothing on screen to turn them back up with.
static constexpr int MinPanelPercent = 1;
// MCCS DDC/CI (VESA Monitor Control Command Set over I2C address 0x37).
static constexpr uint8_t DdcAddress = 0x37;
static constexpr uint8_t DdcHostAddress = 0x51;
static constexpr uint8_t DdcVcpBrightness = 0x10;
static constexpr uint64_t DdcCommandGapMs = 50;
static constexpr uint64_t DdcReplyDelayMs = 40;
static constexpr uint64_t DdcRetryIntervalMs = 60000;
static constexpr uint32_t SOUTH_DSPCLK_GATE_D = 0xC2020;
static constexpr uint32_t PCH_GMBUSUNIT_CLOCK_GATE_DISABLE = (1u << 31);
static PchFamily DetectPch() {
auto& devices = Pci::GetDevices();
for (uint64_t i = 0; i < devices.size(); i++) {
const auto& d = devices[i];
if (d.VendorId != VendorIntel || d.Bus != 0 || d.Device != 0x1F
|| d.Function != 0)
continue;
// Device-ID families as i915 groups them (id & 0xFF80).
switch (d.DeviceId & 0xFF80) {
case 0x3B00: case 0x1C00: case 0x1E00:
return PchFamily::Ibx;
case 0x8C00: case 0x9C00: case 0x8C80: case 0x9C80: // LPT/WPT
case 0xA100: case 0x9D00: case 0xA280: case 0xA380: // SPT/KBP/CMP-V
return PchFamily::Lpt;
case 0xA300: case 0x9D80: case 0x0280: case 0x0680: // CNP/CMP
return PchFamily::Cnp;
default:
break;
}
break;
}
// Ice Point and everything after it share the Cannon Point PWM block
// and the Ice Point pin map, and new PCH IDs keep appearing.
return g_gpuGen >= 11 ? PchFamily::Icp : PchFamily::Unknown;
}
static int RawToPercent(uint32_t raw, uint32_t maximum) {
if (maximum == 0) return -1;
if (raw > maximum) raw = maximum;
return (int)(((uint64_t)raw * 100 + maximum / 2) / maximum);
}
static uint32_t PercentToRaw(int percent, uint32_t maximum) {
return (uint32_t)(((uint64_t)maximum * (uint32_t)percent + 50) / 100);
}
static void ShortSpin() {
for (int i = 0; i < 20000; i++) asm volatile("pause");
}
// ---- Panel PWM ----------------------------------------------------------
static bool PwmState(uint32_t& maximum, uint32_t& duty) {
uint32_t freq = ReadReg(g_blFreqReg);
uint32_t value = ReadReg(g_blDutyReg);
if (freq == 0xFFFFFFFF) return false;
switch (g_blMethod) {
case BacklightMethod::PwmCnp:
maximum = freq;
duty = value;
break;
case BacklightMethod::PwmLpt:
maximum = value >> 16;
duty = value & 0xFFFF;
break;
case BacklightMethod::PwmIlk:
maximum = freq >> 16;
duty = value & 0xFFFF;
break;
default:
return false;
}
return maximum != 0;
}
static void PwmWriteDuty(uint32_t duty) {
if (g_blMethod == BacklightMethod::PwmCnp) {
WriteReg(g_blDutyReg, duty);
} else { } else {
uint32_t cpu = ReadReg(BLC_PWM_CPU_CTL); uint32_t value = ReadReg(g_blDutyReg);
if ((cpu >> 16) != 0) { WriteReg(g_blDutyReg, (value & 0xFFFF0000u) | (duty & 0xFFFF));
g_pwmCtlReg = BLC_PWM_CPU_CTL; }
g_pwmSupported = true; (void)ReadReg(g_blDutyReg);
}
// Adopt the PWM controller firmware left driving the panel. A controller
// that is not enabled is not what lights this panel, so it is skipped.
static bool DiscoverPwmBacklight() {
bool cnpLayout = g_pch == PchFamily::Cnp || g_pch == PchFamily::Icp;
if (g_pch == PchFamily::Unknown && g_gpuGen >= 8) {
// Unrecognized bridge: the LPT layout has nothing at DUTY1, so a
// plausible duty there identifies the CNP block.
uint32_t freq = ReadReg(BXT_BLC_PWM_FREQ1);
uint32_t duty = ReadReg(BXT_BLC_PWM_DUTY1);
cnpLayout = duty != 0 && duty != 0xFFFFFFFF && duty <= freq;
}
if (cnpLayout) {
// BXT/CNP firmware may route the panel through either controller.
for (uint32_t c = 0; c < 2; c++) {
uint32_t delta = c * BXT_BLC_PWM_CONTROLLER_DELTA;
uint32_t ctl = ReadReg(BXT_BLC_PWM_CTL1 + delta);
uint32_t freq = ReadReg(BXT_BLC_PWM_FREQ1 + delta);
if (ctl == 0xFFFFFFFF || !(ctl & BLC_PWM_ENABLE)
|| freq == 0 || freq == 0xFFFFFFFF)
continue;
g_blMethod = BacklightMethod::PwmCnp;
g_blFreqReg = BXT_BLC_PWM_FREQ1 + delta;
g_blDutyReg = BXT_BLC_PWM_DUTY1 + delta;
return true;
}
return false;
}
if (g_pch == PchFamily::Lpt || g_gpuGen >= 8) {
uint32_t ctl1 = ReadReg(BLC_PWM_PCH_CTL1);
uint32_t ctl2 = ReadReg(BLC_PWM_PCH_CTL2);
if (ctl1 == 0xFFFFFFFF || !(ctl1 & BLC_PWM_ENABLE) || (ctl2 >> 16) == 0)
return false;
g_blMethod = BacklightMethod::PwmLpt;
g_blFreqReg = BLC_PWM_PCH_CTL2;
g_blDutyReg = BLC_PWM_PCH_CTL2;
return true;
}
uint32_t pch2 = ReadReg(BLC_PWM_PCH_CTL2);
uint32_t cpu2 = ReadReg(BLC_PWM_CPU_CTL2);
if (pch2 == 0xFFFFFFFF || (pch2 >> 16) == 0 || !(cpu2 & BLC_PWM_ENABLE))
return false;
g_blMethod = BacklightMethod::PwmIlk;
g_blFreqReg = BLC_PWM_PCH_CTL2;
g_blDutyReg = BLC_PWM_CPU_CTL;
return true;
}
// ---- eDP DPCD backlight -------------------------------------------------
// Native AUX read or write of DPCD registers, retrying sink DEFERs.
static bool DpcdAccess(bool write, uint32_t address, uint8_t* data, int size) {
for (int attempt = 0; attempt < 8; attempt++) {
uint8_t reply = 0xFF;
int result = write
? AuxTransfer(0x8, address, data, size, nullptr, 0, &reply)
: AuxTransfer(0x9, address, nullptr, 0, data, size, &reply);
if (result == size && (reply & 0x3) == 0) return true;
if (result < 0 || (reply & 0x3) != 0x2) return false;
ShortSpin();
}
return false;
}
// Before Gen 12 the eDP output has its own transcoder; from Gen 12 it is
// an ordinary DDI in DP mode, so ask the sink. Only eDP sinks implement
// the 0x700 eDP register block or EDP_CONFIGURATION_CAP.
static void DetectEmbeddedDp() {
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP || g_auxControlReg == 0)
return;
uint8_t edpRev = 0;
uint8_t edpConfig = 0;
bool edp = DpcdAccess(false, DPCD_EDP_REV, &edpRev, 1) && edpRev != 0;
if (!edp) edp = DpcdAccess(false, 0x00D, &edpConfig, 1) && edpConfig != 0;
if (!edp) return;
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP;
CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1");
KernelLogStream(INFO, "IntelGPU") << "DDI output identified as eDP (eDP DPCD rev "
<< base::hex << (uint64_t)edpRev << ")";
}
static int DpcdBrightnessBytes() {
return g_blMax > 0xFF ? 2 : 1;
}
// Returns 0 when the panel offers no AUX brightness control, 1 when it
// does but is still driven from its PWM pin, 2 when DPCD already owns it.
static int ProbeDpcdBacklight() {
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP || g_auxControlReg == 0)
return 0;
uint8_t caps[3] = {};
if (!DpcdAccess(false, DPCD_EDP_REV, caps, 3)) return 0;
if (caps[0] == 0 || !(caps[2] & EDP_BLCAP_AUX_SET)) return 0;
uint8_t mode = 0;
if (!DpcdAccess(false, DPCD_EDP_BACKLIGHT_MODE, &mode, 1)) return 0;
g_blMax = (caps[2] & EDP_BLCAP_BYTE_COUNT_16) ? 0xFFFF : 0xFF;
return (mode & EDP_BLMODE_MASK) == EDP_BLMODE_DPCD ? 2 : 1;
}
static int DpcdReadPercent() {
uint8_t bytes[2] = {};
int count = DpcdBrightnessBytes();
if (!DpcdAccess(false, DPCD_EDP_BRIGHTNESS_MSB, bytes, count)) return -1;
uint32_t raw = count == 2 ? ((uint32_t)bytes[0] << 8 | bytes[1]) : bytes[0];
return RawToPercent(raw, g_blMax);
}
static bool DpcdWritePercent(int percent) {
uint32_t raw = PercentToRaw(percent, g_blMax);
if (raw == 0) raw = 1;
uint8_t bytes[2];
int count = DpcdBrightnessBytes();
if (count == 2) {
bytes[0] = (uint8_t)(raw >> 8);
bytes[1] = (uint8_t)raw;
} else {
bytes[0] = (uint8_t)raw;
}
if (!DpcdAccess(true, DPCD_EDP_BRIGHTNESS_MSB, bytes, count)) return false;
if (!g_dpcdModePending) return true;
// First change on a panel that was on its PWM pin: the level is
// already programmed, so switching the source cannot flash it dark.
uint8_t mode = 0;
if (!DpcdAccess(false, DPCD_EDP_BACKLIGHT_MODE, &mode, 1)) return false;
mode = (uint8_t)((mode & ~EDP_BLMODE_MASK) | EDP_BLMODE_DPCD);
if (!DpcdAccess(true, DPCD_EDP_BACKLIGHT_MODE, &mode, 1)) return false;
g_dpcdModePending = false;
return true;
}
// ---- GMBUS (HDMI/DVI DDC) -----------------------------------------------
static int GmbusPinForPort(int port) {
if (port < 0) return -1;
switch (g_pch) {
case PchFamily::Icp: {
// Combo PHY ports use pins 1-3; Type-C ports start at pin 9.
// Gen 11 numbers its first Type-C port C, Gen 12 numbers it D.
int firstTc = g_gpuGen >= 12 ? 3 : 2;
if (port < firstTc) return port + 1;
int pin = 9 + (port - firstTc);
return pin <= 14 ? pin : -1;
}
case PchFamily::Cnp:
return port == 1 ? 1 : port == 2 ? 2 : port == 3 ? 4 : -1;
default:
return port == 1 ? 5 : port == 2 ? 4 : port == 3 ? 6 : -1;
}
}
static bool GmbusWait(uint32_t bit) {
uint64_t start = Timekeeping::GetMilliseconds();
for (;;) {
uint32_t status = ReadReg(GMBUS2);
if (status & GMBUS_SATOER) return false;
if (status & bit) return true;
if (Timekeeping::GetMilliseconds() - start > 50) return false;
asm volatile("pause");
}
}
static bool GmbusWaitIdle() {
uint64_t start = Timekeeping::GetMilliseconds();
while (ReadReg(GMBUS2) & GMBUS_ACTIVE) {
if (Timekeeping::GetMilliseconds() - start > 50) return false;
asm volatile("pause");
}
return true;
}
// One complete I2C message (START .. STOP) at 100 kHz.
static bool GmbusTransfer(int pin, uint8_t address, bool read,
uint8_t* data, int size) {
if (pin < 0 || size <= 0 || size > 256) return false;
uint32_t gate = 0;
if (g_pch == PchFamily::Cnp) {
// Cannon Point drops GMBUS transfers while the unit clock gates.
gate = ReadReg(SOUTH_DSPCLK_GATE_D);
WriteReg(SOUTH_DSPCLK_GATE_D, gate | PCH_GMBUSUNIT_CLOCK_GATE_DISABLE);
}
WriteReg(GMBUS0, (uint32_t)pin);
uint32_t command = GMBUS_SW_RDY | GMBUS_CYCLE_WAIT
| ((uint32_t)size << GMBUS_BYTE_COUNT_SHIFT)
| ((uint32_t)address << 1) | (read ? GMBUS_SLAVE_READ : 0);
bool ok = true;
int pos = 0;
if (read) {
WriteReg(GMBUS1, command);
while (ok && pos < size) {
ok = GmbusWait(GMBUS_HW_RDY);
if (!ok) break;
uint32_t value = ReadReg(GMBUS3);
for (int i = 0; i < 4 && pos < size; i++, pos++)
data[pos] = (uint8_t)(value >> (8 * i));
}
} else {
// The first dword is loaded before the command starts the cycle.
uint32_t value = 0;
for (int i = 0; i < 4 && pos < size; i++, pos++)
value |= (uint32_t)data[pos] << (8 * i);
WriteReg(GMBUS3, value);
WriteReg(GMBUS1, command);
while (ok && pos < size) {
ok = GmbusWait(GMBUS_HW_RDY);
if (!ok) break;
value = 0;
for (int i = 0; i < 4 && pos < size; i++, pos++)
value |= (uint32_t)data[pos] << (8 * i);
WriteReg(GMBUS3, value);
} }
} }
KernelLogStream(INFO, "IntelGPU") << "Panel PWM " if (ok) ok = GmbusWait(GMBUS_HW_WAIT_PHASE);
<< (g_pwmSupported ? "available" : "not exposed");
if (ok) {
WriteReg(GMBUS1, GMBUS_SW_RDY | GMBUS_CYCLE_STOP);
ok = GmbusWaitIdle();
} else {
// NAK or timeout: clear the error, let the bus go idle.
WriteReg(GMBUS1, GMBUS_SW_CLR_INT);
(void)GmbusWaitIdle();
WriteReg(GMBUS1, 0);
}
WriteReg(GMBUS0, 0);
if (g_pch == PchFamily::Cnp) WriteReg(SOUTH_DSPCLK_GATE_D, gate);
return ok;
}
// ---- DDC/CI (external monitors) -----------------------------------------
static bool DdcUsesAux() {
return g_connectorType == montauk::abi::DISPLAY_CONNECTOR_DP;
}
static bool DdcWrite(uint8_t* data, int size) {
if (DdcUsesAux()) {
if (size > 16) return false;
bool ok = AuxI2cTransfer(0x4, data, size, nullptr, 0, DdcAddress);
// Address-only write without MOT ends the I2C transaction.
bool stopped = AuxI2cTransfer(0x0, nullptr, 0, nullptr, 0, DdcAddress);
return ok && stopped;
}
return GmbusTransfer(GmbusPinForPort(g_ddiPort), DdcAddress, false, data, size);
}
static bool DdcRead(uint8_t* data, int size) {
if (DdcUsesAux()) {
if (size > 16) return false;
if (!AuxI2cTransfer(0x5, nullptr, 0, nullptr, 0, DdcAddress)) return false;
return AuxI2cTransfer(0x1, nullptr, 0, data, size, DdcAddress);
}
return GmbusTransfer(GmbusPinForPort(g_ddiPort), DdcAddress, true, data, size);
}
static void DdcPace() {
uint64_t now = Timekeeping::GetMilliseconds();
if (g_ddcLastMs != 0 && now - g_ddcLastMs < DdcCommandGapMs)
Timekeeping::Sleep(DdcCommandGapMs - (now - g_ddcLastMs));
}
// Send an MCCS command: host source address, length, payload, checksum.
// The checksum covers the monitor's 8-bit write address (0x6E) too.
static bool DdcCommand(const uint8_t* payload, int length) {
uint8_t packet[16];
if (length + 3 > (int)sizeof(packet)) return false;
packet[0] = DdcHostAddress;
packet[1] = (uint8_t)(0x80 | length);
uint8_t checksum = (uint8_t)(DdcAddress << 1) ^ packet[0] ^ packet[1];
for (int i = 0; i < length; i++) {
packet[2 + i] = payload[i];
checksum ^= payload[i];
}
packet[2 + length] = checksum;
DdcPace();
bool ok = DdcWrite(packet, length + 3);
g_ddcLastMs = Timekeeping::GetMilliseconds();
return ok;
}
static bool DdcGetBrightness(uint32_t& current, uint32_t& maximum) {
uint8_t request[2] = {0x01, DdcVcpBrightness}; // Get VCP Feature
if (!DdcCommand(request, 2)) return false;
Timekeeping::Sleep(DdcReplyDelayMs);
// Reply: 6E 88 02 rc 10 type maxH maxL curH curL checksum. The
// checksum covers the host's virtual address 0x50.
uint8_t reply[11] = {};
bool ok = DdcRead(reply, sizeof(reply));
g_ddcLastMs = Timekeeping::GetMilliseconds();
if (!ok) return false;
uint8_t checksum = 0x50;
for (int i = 0; i < 10; i++) checksum ^= reply[i];
if (checksum != reply[10] || reply[1] != 0x88 || reply[2] != 0x02
|| reply[3] != 0x00 || reply[4] != DdcVcpBrightness)
return false;
maximum = (uint32_t)reply[6] << 8 | reply[7];
current = (uint32_t)reply[8] << 8 | reply[9];
return maximum != 0;
}
static bool DdcSetBrightness(uint32_t value) {
uint8_t request[4] = {0x03, DdcVcpBrightness, // Set VCP Feature
(uint8_t)(value >> 8), (uint8_t)value};
return DdcCommand(request, 4);
}
// DDC/CI takes tens of milliseconds and needs the scheduler, so it is
// probed on first use from a syscall rather than during boot.
static bool EnsureDdc() {
if (g_blMethod == BacklightMethod::DdcCi) return true;
if (!g_ddcCandidate) return false;
uint64_t now = Timekeeping::GetMilliseconds();
if (g_ddcRetryMs != 0 && now < g_ddcRetryMs) return false;
for (int attempt = 0; attempt < 3; attempt++) {
uint32_t current = 0, maximum = 0;
if (!DdcGetBrightness(current, maximum)) continue;
g_blMethod = BacklightMethod::DdcCi;
g_blMax = maximum;
g_blPercent = RawToPercent(current, maximum);
KernelLogStream(OK, "IntelGPU") << "Monitor brightness via DDC/CI ("
<< base::dec << (uint64_t)current << "/" << (uint64_t)maximum << ")";
return true;
}
g_ddcRetryMs = Timekeeping::GetMilliseconds() + DdcRetryIntervalMs;
KernelLogStream(INFO, "IntelGPU") << "Monitor did not answer DDC/CI brightness query";
return false;
}
// ---- Discovery and dispatch ---------------------------------------------
static void DiscoverBacklight() {
g_blMethod = BacklightMethod::None;
g_blPercent = -1;
g_dpcdModePending = false;
g_ddcCandidate = false;
g_pch = DetectPch();
DetectEmbeddedDp();
bool panel = g_connectorType == montauk::abi::DISPLAY_CONNECTOR_EDP
|| g_connectorType == montauk::abi::DISPLAY_CONNECTOR_LVDS;
if (panel) {
// A panel firmware already put in DPCD mode ignores its PWM pin.
// Otherwise PWM is what firmware lit it with; take DPCD over only
// when there is no live PWM controller.
int dpcd = ProbeDpcdBacklight();
if (dpcd == 2) {
g_blMethod = BacklightMethod::DpcdAux;
} else if (!DiscoverPwmBacklight() && dpcd == 1) {
g_blMethod = BacklightMethod::DpcdAux;
g_dpcdModePending = true;
g_blPercent = 100;
}
} else if (g_gpuGen >= 8 && g_ddiPort >= 0
&& (g_connectorType == montauk::abi::DISPLAY_CONNECTOR_HDMI
|| g_connectorType == montauk::abi::DISPLAY_CONNECTOR_DVI
|| (g_connectorType == montauk::abi::DISPLAY_CONNECTOR_DP
&& g_auxControlReg != 0))) {
g_ddcCandidate = DdcUsesAux() || GmbusPinForPort(g_ddiPort) >= 0;
}
const char* pch = g_pch == PchFamily::Ibx ? "IBX" : g_pch == PchFamily::Lpt ? "LPT"
: g_pch == PchFamily::Cnp ? "CNP" : g_pch == PchFamily::Icp ? "ICP+"
: "unknown";
if (g_blMethod != BacklightMethod::None) {
KernelLogStream(INFO, "IntelGPU") << "Backlight: " << BrightnessMethod()
<< (g_dpcdModePending ? " (on first change)" : "")
<< ", PCH " << pch;
} else {
KernelLogStream(INFO, "IntelGPU") << "Backlight: "
<< (g_ddcCandidate ? "DDC/CI probe deferred" : "not exposed")
<< ", PCH " << pch;
}
}
static int BrightnessLocked(int percent) {
if (percent > 100) percent = 100;
switch (g_blMethod) {
case BacklightMethod::PwmIlk:
case BacklightMethod::PwmLpt:
case BacklightMethod::PwmCnp: {
uint32_t maximum = 0, duty = 0;
if (!PwmState(maximum, duty)) return -1;
if (percent >= 0) {
if (percent < MinPanelPercent) percent = MinPanelPercent;
uint32_t raw = PercentToRaw(percent, maximum);
PwmWriteDuty(raw ? raw : 1);
if (!PwmState(maximum, duty)) return -2;
}
g_blPercent = RawToPercent(duty, maximum);
return g_blPercent;
}
case BacklightMethod::DpcdAux: {
if (percent < 0) {
if (g_dpcdModePending) return g_blPercent;
int level = DpcdReadPercent();
if (level >= 0) g_blPercent = level;
return g_blPercent;
}
if (percent < MinPanelPercent) percent = MinPanelPercent;
if (!DpcdWritePercent(percent)) return -2;
int level = DpcdReadPercent();
g_blPercent = level >= 0 ? level : percent;
return g_blPercent;
}
case BacklightMethod::None:
if (!EnsureDdc()) return -1;
[[fallthrough]];
case BacklightMethod::DdcCi: {
// Monitors answer queries slowly, and the OSD is the only
// other writer, so the last known level stands in.
if (percent < 0) return g_blPercent;
if (!DdcSetBrightness(PercentToRaw(percent, g_blMax))) return -2;
g_blPercent = percent;
return g_blPercent;
}
}
return -1;
} }
static void InitializeDisplayManagement() { static void InitializeDisplayManagement() {
@@ -942,7 +1483,7 @@ namespace Drivers::Graphics::IntelGPU {
DiscoverActiveTranscoder(); DiscoverActiveTranscoder();
ParseEdid(); ParseEdid();
SelectCurrentMode(); SelectCurrentMode();
DiscoverPwm(); DiscoverBacklight();
} }
// ========================================================================= // =========================================================================
@@ -1660,7 +2201,6 @@ namespace Drivers::Graphics::IntelGPU {
out->capabilities = montauk::abi::DISPLAY_CAP_MODESET; out->capabilities = montauk::abi::DISPLAY_CAP_MODESET;
if (g_edidValid) out->capabilities |= montauk::abi::DISPLAY_CAP_EDID; if (g_edidValid) out->capabilities |= montauk::abi::DISPLAY_CAP_EDID;
if (g_pwmSupported) out->capabilities |= montauk::abi::DISPLAY_CAP_BRIGHTNESS;
if (g_flipSupported) out->capabilities |= montauk::abi::DISPLAY_CAP_PAGE_FLIP; if (g_flipSupported) out->capabilities |= montauk::abi::DISPLAY_CAP_PAGE_FLIP;
for (int i = 0; i < g_modeCount; i++) { for (int i = 0; i < g_modeCount; i++) {
if (i != g_currentMode if (i != g_currentMode
@@ -1672,6 +2212,7 @@ namespace Drivers::Graphics::IntelGPU {
out->modeCount = (uint32_t)g_modeCount; out->modeCount = (uint32_t)g_modeCount;
out->currentMode = g_currentMode; out->currentMode = g_currentMode;
out->brightness = Brightness(-1); out->brightness = Brightness(-1);
if (out->brightness >= 0) out->capabilities |= montauk::abi::DISPLAY_CAP_BRIGHTNESS;
out->deviceId = g_gpuInfo.deviceId; out->deviceId = g_gpuInfo.deviceId;
out->generation = g_gpuInfo.gen; out->generation = g_gpuInfo.gen;
out->connectorType = g_connectorType; out->connectorType = g_connectorType;
@@ -1737,20 +2278,28 @@ namespace Drivers::Graphics::IntelGPU {
} }
int Brightness(int percent) { int Brightness(int percent) {
if (!g_initialized || !g_pwmSupported) return -1; if (!g_initialized) return -1;
uint32_t value = ReadReg(g_pwmCtlReg); if (g_blMethod == BacklightMethod::None && !g_ddcCandidate) return -1;
uint32_t maximum = value >> 16; g_blLock.Acquire();
if (maximum == 0) return -1; int result = BrightnessLocked(percent);
if (percent < 0) { g_blLock.Release();
uint32_t duty = value & 0xFFFF; return result;
if (duty > maximum) duty = maximum; }
return (int)(((uint64_t)duty * 100 + maximum / 2) / maximum);
uint8_t ActiveConnectorType() {
return g_connectorType;
}
const char* BrightnessMethod() {
switch (g_blMethod) {
case BacklightMethod::PwmIlk:
case BacklightMethod::PwmLpt:
case BacklightMethod::PwmCnp: return "PWM";
case BacklightMethod::DpcdAux: return "eDP AUX";
case BacklightMethod::DdcCi: return "DDC/CI";
case BacklightMethod::None: break;
} }
if (percent > 100) percent = 100; return nullptr;
uint32_t duty = (uint32_t)(((uint64_t)maximum * (uint32_t)percent + 50) / 100);
WriteReg(g_pwmCtlReg, (value & 0xFFFF0000u) | duty);
(void)ReadReg(g_pwmCtlReg);
return Brightness(-1);
} }
// ========================================================================= // =========================================================================
@@ -1993,6 +2542,12 @@ namespace Drivers::Graphics::IntelGPU {
<< base::dec << (uint64_t)g_frontBuffer << ")"; << base::dec << (uint64_t)g_frontBuffer << ")";
} }
// 7. Firmware POST resets the PWM duty to its own default. Put back
// the level the user chose (register writes only; no AUX here).
uint32_t pwmMax = 0, pwmDuty = 0;
if (g_blPercent > 0 && PwmState(pwmMax, pwmDuty))
PwmWriteDuty(PercentToRaw(g_blPercent, pwmMax));
KernelLogStream(OK, "IntelGPU") << "Display restored after S3 resume"; KernelLogStream(OK, "IntelGPU") << "Display restored after S3 resume";
} }
+55 -10
View File
@@ -237,6 +237,7 @@ namespace Drivers::Graphics::IntelGPU {
static constexpr uint32_t DVOB = 0x61140; // DVO-B static constexpr uint32_t DVOB = 0x61140; // DVO-B
static constexpr uint32_t DVOC = 0x61160; // DVO-C static constexpr uint32_t DVOC = 0x61160; // DVO-C
static constexpr uint32_t LVDS = 0x61180; // LVDS panel static constexpr uint32_t LVDS = 0x61180; // LVDS panel
static constexpr uint32_t DP_A = 0x64000; // CPU eDP (ILK-IVB)
static constexpr uint32_t DP_B = 0x64100; // DisplayPort B static constexpr uint32_t DP_B = 0x64100; // DisplayPort B
static constexpr uint32_t DP_C = 0x64200; // DisplayPort C static constexpr uint32_t DP_C = 0x64200; // DisplayPort C
static constexpr uint32_t DP_D = 0x64300; // DisplayPort D static constexpr uint32_t DP_D = 0x64300; // DisplayPort D
@@ -277,12 +278,35 @@ namespace Drivers::Graphics::IntelGPU {
static constexpr uint32_t DP_AUX_MESSAGE_SHIFT = 20; static constexpr uint32_t DP_AUX_MESSAGE_SHIFT = 20;
// PCH/CPU panel PWM. Firmware owns the frequency; the driver only changes // PCH/CPU panel PWM. Firmware owns the frequency; the driver only changes
// the duty field and preserves enable/polarity/reserved bits. // the duty field and preserves enable/polarity/reserved bits. Three
// layouts share these addresses:
// Ibex Peak..Panther Point (ILK-IVB): period = PCH_CTL2[31:16], duty
// = CPU_CTL[15:0].
// Lynx Point..Kaby Point (HSW-KBL): PCH_CTL2 = period << 16 | duty.
// Cannon Point and later (and BXT/GLK): CTL1 enable, then full 32-bit
// FREQ and DUTY registers, per controller at +0x100. Reading these as
// the LPT layout corrupts the period, so the PCH must be known.
static constexpr uint32_t BLC_PWM_CPU_CTL2 = 0x48250; static constexpr uint32_t BLC_PWM_CPU_CTL2 = 0x48250;
static constexpr uint32_t BLC_PWM_CPU_CTL = 0x48254; static constexpr uint32_t BLC_PWM_CPU_CTL = 0x48254;
static constexpr uint32_t BLC_PWM_PCH_CTL1 = 0xC8250; static constexpr uint32_t BLC_PWM_PCH_CTL1 = 0xC8250;
static constexpr uint32_t BLC_PWM_PCH_CTL2 = 0xC8254; static constexpr uint32_t BLC_PWM_PCH_CTL2 = 0xC8254;
static constexpr uint32_t BLC_PWM_ENABLE = (1u << 31); static constexpr uint32_t BLC_PWM_ENABLE = (1u << 31);
static constexpr uint32_t BXT_BLC_PWM_CTL1 = 0xC8250;
static constexpr uint32_t BXT_BLC_PWM_FREQ1 = 0xC8254;
static constexpr uint32_t BXT_BLC_PWM_DUTY1 = 0xC8258;
static constexpr uint32_t BXT_BLC_PWM_CONTROLLER_DELTA = 0x100;
// eDP DPCD backlight (VESA eDP 1.2+ "AUX backlight control").
static constexpr uint32_t DPCD_EDP_REV = 0x700;
static constexpr uint32_t DPCD_EDP_GENERAL_CAP_1 = 0x701;
static constexpr uint32_t DPCD_EDP_BACKLIGHT_CAP = 0x702;
static constexpr uint32_t DPCD_EDP_BACKLIGHT_MODE = 0x721;
static constexpr uint32_t DPCD_EDP_BRIGHTNESS_MSB = 0x722;
static constexpr uint8_t EDP_CAP1_AUX_ENABLE = (1u << 2);
static constexpr uint8_t EDP_BLCAP_AUX_SET = (1u << 1);
static constexpr uint8_t EDP_BLCAP_BYTE_COUNT_16 = (1u << 2);
static constexpr uint8_t EDP_BLMODE_MASK = 0x3;
static constexpr uint8_t EDP_BLMODE_DPCD = 0x2;
// ADPA bits // ADPA bits
static constexpr uint32_t ADPA_DAC_ENABLE = (1u << 31); static constexpr uint32_t ADPA_DAC_ENABLE = (1u << 31);
@@ -294,13 +318,25 @@ namespace Drivers::Graphics::IntelGPU {
static constexpr uint32_t LVDS_PORT_ENABLE = (1u << 31); static constexpr uint32_t LVDS_PORT_ENABLE = (1u << 31);
static constexpr uint32_t LVDS_PIPE_B_SELECT = (1u << 30); static constexpr uint32_t LVDS_PIPE_B_SELECT = (1u << 30);
// --- GMBUS (I2C for EDID) --- // --- GMBUS (display I2C: DDC for HDMI/DVI) ---
static constexpr uint32_t GMBUS0 = 0x5100; // Clock/port select // Every supported generation is PCH-split, which moves GMBUS from the
static constexpr uint32_t GMBUS1 = 0x5104; // Command/status // legacy 0x5100 block into the PCH display range.
static constexpr uint32_t GMBUS2 = 0x5108; // Status static constexpr uint32_t GMBUS0 = 0xC5100; // Clock/pin select
static constexpr uint32_t GMBUS3 = 0x510C; // Data buffer static constexpr uint32_t GMBUS1 = 0xC5104; // Command/status
static constexpr uint32_t GMBUS4 = 0x5110; // Interrupt mask static constexpr uint32_t GMBUS2 = 0xC5108; // Status
static constexpr uint32_t GMBUS5 = 0x5120; // 2-byte index static constexpr uint32_t GMBUS3 = 0xC510C; // Data buffer
static constexpr uint32_t GMBUS4 = 0xC5110; // Interrupt mask
static constexpr uint32_t GMBUS5 = 0xC5120; // 2-byte index
static constexpr uint32_t GMBUS_SW_CLR_INT = (1u << 31);
static constexpr uint32_t GMBUS_SW_RDY = (1u << 30);
static constexpr uint32_t GMBUS_CYCLE_WAIT = (1u << 25);
static constexpr uint32_t GMBUS_CYCLE_STOP = (4u << 25);
static constexpr uint32_t GMBUS_BYTE_COUNT_SHIFT = 16;
static constexpr uint32_t GMBUS_SLAVE_READ = 1u;
static constexpr uint32_t GMBUS_HW_WAIT_PHASE = (1u << 14);
static constexpr uint32_t GMBUS_HW_RDY = (1u << 11);
static constexpr uint32_t GMBUS_SATOER = (1u << 10); // NAK
static constexpr uint32_t GMBUS_ACTIVE = (1u << 9);
// --- Hardware status page --- // --- Hardware status page ---
static constexpr uint32_t HWS_PGA = 0x02080; static constexpr uint32_t HWS_PGA = 0x02080;
@@ -496,10 +532,19 @@ namespace Drivers::Graphics::IntelGPU {
int SetDisplayMode(int modeIndex); int SetDisplayMode(int modeIndex);
// percent < 0 queries the current brightness. Otherwise sets 0..100 and // percent < 0 queries the current brightness. Otherwise sets 0..100 and
// returns the resulting percentage. Returns -1 when no PWM backlight is // returns the resulting percentage. Returns -1 when the active output has
// exposed by the active output. // no controllable backlight, -2 when the hardware rejected the change.
// Backends, in order: panel PWM, eDP DPCD (AUX) backlight, and DDC/CI
// (MCCS VCP 0x10) for an external monitor on HDMI/DVI/DP. May sleep.
int Brightness(int percent); int Brightness(int percent);
// DisplayConnectorType of the output the driver adopted from firmware.
uint8_t ActiveConnectorType();
// Short name of the backlight method in use ("PWM", "eDP AUX", "DDC/CI"),
// or nullptr when there is none.
const char* BrightnessMethod();
// ========================================================================= // =========================================================================
// Page flipping (double-buffered scanout) // Page flipping (double-buffered scanout)
// ========================================================================= // =========================================================================
+1
View File
@@ -32,6 +32,7 @@ namespace Sched::Consent {
{"set_time", CAP_SET_TIME}, {"set_time", CAP_SET_TIME},
{"display_admin", CAP_DISPLAY_ADMIN}, {"display_admin", CAP_DISPLAY_ADMIN},
{"log_read", CAP_LOG_READ}, {"log_read", CAP_LOG_READ},
{"brightness", CAP_BRIGHTNESS},
}; };
constexpr uint64_t EligibleMask() { constexpr uint64_t EligibleMask() {
+6 -3
View File
@@ -3,7 +3,8 @@
# #
# Capability names: process_admin, power_request, power_control, suspend, # Capability names: process_admin, power_request, power_control, suspend,
# storage_admin, raw_storage, network_admin, set_time, user_admin, # storage_admin, raw_storage, network_admin, set_time, user_admin,
# display_admin, device_admin, log_read, system_image, compositor and "all". # display_admin, device_admin, log_read, system_image, compositor, brightness
# and "all".
# #
# ==== System services (started by init) ==== # ==== System services (started by init) ====
@@ -26,6 +27,7 @@ delegable = [
"power_request", "suspend", "process_admin", "storage_admin", "power_request", "suspend", "process_admin", "storage_admin",
"raw_storage", "network_admin", "set_time", "user_admin", "raw_storage", "network_admin", "set_time", "user_admin",
"display_admin", "device_admin", "log_read", "compositor", "display_admin", "device_admin", "log_read", "compositor",
"brightness",
] ]
[grant.sshd] [grant.sshd]
@@ -38,11 +40,12 @@ effective = [
"power_request", "suspend", "process_admin", "storage_admin", "power_request", "suspend", "process_admin", "storage_admin",
"raw_storage", "network_admin", "set_time", "user_admin", "raw_storage", "network_admin", "set_time", "user_admin",
"display_admin", "device_admin", "log_read", "compositor", "display_admin", "device_admin", "log_read", "compositor",
"brightness",
] ]
delegable = [ delegable = [
"power_request", "suspend", "process_admin", "storage_admin", "power_request", "suspend", "process_admin", "storage_admin",
"raw_storage", "network_admin", "set_time", "user_admin", "raw_storage", "network_admin", "set_time", "user_admin",
"display_admin", "device_admin", "log_read", "display_admin", "device_admin", "log_read", "brightness",
] ]
[grant.terminal] [grant.terminal]
@@ -68,7 +71,7 @@ delegable = ["network_admin"]
[grant.display] [grant.display]
path = "0:/apps/display/display.elf" path = "0:/apps/display/display.elf"
effective = ["display_admin"] effective = ["display_admin", "brightness"]
[grant.disks] [grant.disks]
path = "0:/apps/disks/disks.elf" path = "0:/apps/disks/disks.elf"
+9 -3
View File
@@ -298,8 +298,14 @@ namespace montauk::abi {
never part of a session mask, never part of "all", and never never part of a session mask, never part of "all", and never
delegated by the compositor to what it launches. */ delegated by the compositor to what it launches. */
static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13; static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13;
static constexpr uint64_t CAP_ALL = (1ULL << 14) - 1; /* Set the backlight of the active display (SYS_DISPLAYBRIGHTNESS).
static constexpr uint64_t CAP_STANDARD_SESSION = CAP_POWER_REQUEST | CAP_SUSPEND; A seat preference rather than display administration, so every
session holds it; CAP_DISPLAY_ADMIN keeps mode setting. Reading the
level needs nothing. */
static constexpr uint64_t CAP_BRIGHTNESS = 1ULL << 14;
static constexpr uint64_t CAP_ALL = (1ULL << 15) - 1;
static constexpr uint64_t CAP_STANDARD_SESSION =
CAP_POWER_REQUEST | CAP_SUSPEND | CAP_BRIGHTNESS;
static constexpr uint64_t CAP_ADMIN_SESSION = static constexpr uint64_t CAP_ADMIN_SESSION =
CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN | CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN |
CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN | CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN |
@@ -351,7 +357,7 @@ namespace montauk::abi {
deferred delegation, not amplification. */ deferred delegation, not amplification. */
static constexpr uint64_t CAP_CONSENT_ELIGIBLE = static constexpr uint64_t CAP_CONSENT_ELIGIBLE =
CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME |
CAP_DISPLAY_ADMIN | CAP_LOG_READ; CAP_DISPLAY_ADMIN | CAP_LOG_READ | CAP_BRIGHTNESS;
static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0, static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0,
"consent can only hand out authority an admin session holds"); "consent can only hand out authority an admin session holds");
+12
View File
@@ -227,6 +227,18 @@ struct DesktopState {
montauk::abi::BatteryInfo battery_info; montauk::abi::BatteryInfo battery_info;
uint64_t battery_last_poll; uint64_t battery_last_poll;
// Display brightness. Present only while the active output has a
// controllable backlight (laptop panel, or a monitor answering DDC/CI).
SvgIcon icon_brightness;
bool brightness_present;
bool brightness_popup_open;
Rect brightness_icon_rect;
int brightness_level; // 0-100, as shown on the slider
bool brightness_dragging;
int brightness_pending; // drag sample not yet sent, -1 none
uint64_t brightness_last_send;
uint64_t brightness_last_poll;
bool vol_popup_open; bool vol_popup_open;
Rect vol_icon_rect; Rect vol_icon_rect;
int vol_level; // 0-100 int vol_level; // 0-100
+1
View File
@@ -38,6 +38,7 @@ namespace caps {
{"log_read", montauk::abi::CAP_LOG_READ}, {"log_read", montauk::abi::CAP_LOG_READ},
{"system_image", montauk::abi::CAP_SYSTEM_IMAGE}, {"system_image", montauk::abi::CAP_SYSTEM_IMAGE},
{"compositor", montauk::abi::CAP_COMPOSITOR}, {"compositor", montauk::abi::CAP_COMPOSITOR},
{"brightness", montauk::abi::CAP_BRIGHTNESS},
}; };
// Never implied by "all": each must be named explicitly in a grant. // Never implied by "all": each must be named explicitly in a grant.
+3 -1
View File
@@ -31,10 +31,12 @@ namespace consent {
"0:/os/icons/network-wired.svg"}, "0:/os/icons/network-wired.svg"},
{montauk::abi::CAP_SET_TIME, "change the system clock and time zone", {montauk::abi::CAP_SET_TIME, "change the system clock and time zone",
"0:/os/icons/preferences-system-time.svg"}, "0:/os/icons/preferences-system-time.svg"},
{montauk::abi::CAP_DISPLAY_ADMIN, "change the screen resolution and brightness", {montauk::abi::CAP_DISPLAY_ADMIN, "change the screen resolution",
"0:/os/icons/preferences-desktop-display.svg"}, "0:/os/icons/preferences-desktop-display.svg"},
{montauk::abi::CAP_LOG_READ, "read the system log, which can reveal what other apps and devices are doing", {montauk::abi::CAP_LOG_READ, "read the system log, which can reveal what other apps and devices are doing",
"0:/os/icons/utilities-log-viewer.svg"}, "0:/os/icons/utilities-log-viewer.svg"},
{montauk::abi::CAP_BRIGHTNESS, "change the screen brightness",
"0:/os/icons/preferences-desktop-display.svg"},
}; };
// Fallback for a capability without its own icon. // Fallback for a capability without its own icon.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -20,7 +20,7 @@ extern "C" {
using namespace gui; using namespace gui;
static constexpr int WIN_W = 760; static constexpr int WIN_W = 760;
static constexpr int WIN_H = 560; static constexpr int WIN_H = 582; // one grid row per capability
static constexpr int TAB_H = 36; static constexpr int TAB_H = 36;
static constexpr int FOOTER_H = 44; static constexpr int FOOTER_H = 44;
static constexpr int PAD = 18; static constexpr int PAD = 18;
@@ -60,11 +60,12 @@ static const char* const kCapDescriptions[CAP_COUNT] = {
"Configure network interfaces, DHCP and Wi-Fi", "Configure network interfaces, DHCP and Wi-Fi",
"Set the system clock", "Set the system clock",
"Manage user accounts and session identities", "Manage user accounts and session identities",
"Change display modes and panel brightness", "Change display modes",
"Manage hardware devices (Bluetooth, radio)", "Manage hardware devices (Bluetooth, radio)",
"Read the kernel log", "Read the kernel log",
"Write to 0:/os and 0:/apps (never part of \"all\")", "Write to 0:/os and 0:/apps (never part of \"all\")",
"Read and send input to other windows (never part of \"all\")", "Read and send input to other windows (never part of \"all\")",
"Set the display backlight",
}; };
// ==== Grant table model ==== // ==== Grant table model ====
+1 -1
View File
@@ -56,7 +56,7 @@ LDFLAGS := \
# ---- C++ source files ---- # ---- C++ source files ----
CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp battery.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp consent.cpp font_data.cpp stb_truetype_impl.cpp CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp battery.cpp brightness.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp consent.cpp font_data.cpp stb_truetype_impl.cpp
APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print)) APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print))
SRCS := $(CORE_SRCS) $(APP_SRCS) SRCS := $(CORE_SRCS) $(APP_SRCS)
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
+263
View File
@@ -0,0 +1,263 @@
/*
* brightness.cpp
* Panel brightness control: the icon that appears when the active display
* has a controllable backlight (laptop panel or DDC/CI monitor), and the
* slider popout that drives it.
*
* Copyright (c) 2026 Daniel Hammer
*/
#include "desktop_internal.hpp"
#include <gui/mtk.hpp>
using namespace gui;
// ============================================================================
// Geometry
// ============================================================================
static constexpr int BRIGHT_POPUP_W = 220;
static constexpr int BRIGHT_SLIDER_INSET = 16;
static constexpr int BRIGHT_SLIDER_H = 8;
static constexpr int BRIGHT_KNOB_R = 8;
static constexpr int BRIGHT_STEP = 5; // [-]/[+] and one wheel notch
static constexpr int BRIGHT_MIN = 5; // keep the screen readable
static constexpr int BRIGHT_POLL_MS = 2000; // follow the Display app
static constexpr int BRIGHT_ABSENT_POLL_MS = 30000;
// A DDC/CI monitor needs ~50ms per command, so a drag is sampled rather than
// forwarding every mouse move; the release always sends the final level.
static constexpr int BRIGHT_DRAG_SEND_MS = 100;
static const char* DISPLAY_APP_PATH = "0:/apps/display/display.elf";
static int brightness_popup_x(const DesktopState* ds) {
int x = ds->brightness_icon_rect.x + ds->brightness_icon_rect.w - BRIGHT_POPUP_W;
return x < 4 ? 4 : x;
}
static int brightness_header_y(const DesktopState* ds) {
(void)ds;
return PANEL_HEIGHT + 2 + 12;
}
static Rect brightness_slider_rect(const DesktopState* ds) {
int y = brightness_header_y(ds) + system_font_height() + 18;
return {brightness_popup_x(ds) + BRIGHT_SLIDER_INSET, y,
BRIGHT_POPUP_W - 2 * BRIGHT_SLIDER_INSET, BRIGHT_SLIDER_H};
}
// [-] [+] row under the slider.
static Rect brightness_minus_rect(const DesktopState* ds) {
Rect s = brightness_slider_rect(ds);
return {brightness_popup_x(ds) + (BRIGHT_POPUP_W - (36 * 2 + 8)) / 2,
s.y + s.h + 16, 36, 24};
}
static Rect brightness_plus_rect(const DesktopState* ds) {
Rect m = brightness_minus_rect(ds);
return {m.x + m.w + 8, m.y, m.w, m.h};
}
static Rect brightness_settings_rect(const DesktopState* ds) {
Rect m = brightness_minus_rect(ds);
return {brightness_popup_x(ds) + 8, m.y + m.h + 12,
BRIGHT_POPUP_W - 16, system_font_height() + 10};
}
static Rect brightness_popup_rect(const DesktopState* ds) {
Rect settings = brightness_settings_rect(ds);
int y = PANEL_HEIGHT + 2;
return {brightness_popup_x(ds), y, BRIGHT_POPUP_W, settings.y + settings.h + 8 - y};
}
// ============================================================================
// Hardware access
// ============================================================================
static int clamp_level(int level) {
if (level < BRIGHT_MIN) return BRIGHT_MIN;
if (level > 100) return 100;
return level;
}
// Send `level` now. The kernel reports what the hardware ended up at, which
// for a panel with coarse steps is not always exactly what was asked.
static void brightness_apply(DesktopState* ds, int level) {
ds->brightness_pending = -1;
ds->brightness_last_send = montauk::get_milliseconds();
int result = montauk::display_brightness(level);
if (result >= 0 && !ds->brightness_dragging) ds->brightness_level = result;
}
// While dragging, the slider tracks the pointer immediately and the hardware
// catches up at most every BRIGHT_DRAG_SEND_MS.
static void brightness_request(DesktopState* ds, int level) {
level = clamp_level(level);
ds->brightness_level = level;
uint64_t now = montauk::get_milliseconds();
if (ds->brightness_dragging && now - ds->brightness_last_send < BRIGHT_DRAG_SEND_MS) {
ds->brightness_pending = level;
return;
}
brightness_apply(ds, level);
}
static int brightness_from_x(const DesktopState* ds, int mx) {
Rect s = brightness_slider_rect(ds);
int v = ((mx - s.x) * 100) / s.w;
return clamp_level(v);
}
// ============================================================================
// Startup and polling
// ============================================================================
void desktop_brightness_init(DesktopState* ds) {
ds->icon_brightness = svg_load("0:/os/icons/display-brightness-symbolic.svg",
16, 16, colors::PANEL_TEXT);
ds->brightness_popup_open = false;
ds->brightness_icon_rect = {0, 0, 0, 0};
ds->brightness_dragging = false;
ds->brightness_pending = -1;
ds->brightness_last_send = 0;
ds->brightness_last_poll = montauk::get_milliseconds();
int level = montauk::display_brightness(-1);
ds->brightness_present = level >= 0;
ds->brightness_level = level >= 0 ? level : 100;
}
bool desktop_brightness_poll(DesktopState* ds, uint64_t now) {
// Flush a drag sample that was held back by the send throttle.
if (ds->brightness_pending >= 0 && now - ds->brightness_last_send >= BRIGHT_DRAG_SEND_MS)
brightness_apply(ds, ds->brightness_pending);
if (ds->brightness_dragging) return false;
// An absent backlight is re-checked now and then: an external monitor
// can be switched to DDC/CI in its menu, and the kernel retries that.
uint64_t interval = ds->brightness_present ? BRIGHT_POLL_MS : BRIGHT_ABSENT_POLL_MS;
if (now - ds->brightness_last_poll < interval) return false;
ds->brightness_last_poll = now;
int level = montauk::display_brightness(-1);
bool present = level >= 0;
bool changed = present != ds->brightness_present;
ds->brightness_present = present;
if (!present) {
if (changed) ds->brightness_popup_open = false;
return changed;
}
if (level != ds->brightness_level) {
ds->brightness_level = level;
changed = true;
}
return changed;
}
bool desktop_brightness_dragging(const DesktopState* ds) {
return ds->brightness_dragging;
}
// ============================================================================
// Popout
// ============================================================================
static void draw_button(DesktopState* ds, const Rect& r, const char* label) {
Framebuffer& fb = ds->fb;
Color bg = r.contains(ds->mouse.x, ds->mouse.y) ? Color::from_rgb(0xD0, 0xD0, 0xD0)
: Color::from_rgb(0xE0, 0xE0, 0xE0);
fill_rounded_rect(fb, r.x, r.y, r.w, r.h, 6, bg);
int tw = text_width(label);
draw_text(fb, r.x + (r.w - tw) / 2, r.y + (r.h - system_font_height()) / 2,
label, colors::TEXT_COLOR);
}
void desktop_draw_brightness_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
Rect popup = brightness_popup_rect(ds);
Color accent = ds->settings.accent_color;
draw_shadow(fb, popup.x, popup.y, popup.w, popup.h, 4, colors::SHADOW);
fill_rounded_rect(fb, popup.x, popup.y, popup.w, popup.h, 8, colors::MENU_BG);
draw_rect(fb, popup.x, popup.y, popup.w, popup.h, colors::BORDER);
int ty = brightness_header_y(ds);
draw_text(fb, popup.x + 14, ty, "Brightness", colors::TEXT_COLOR);
char value[8];
snprintf(value, sizeof(value), "%d%%", ds->brightness_level);
draw_text(fb, popup.x + popup.w - 14 - text_width(value), ty, value, accent);
Rect s = brightness_slider_rect(ds);
fill_rounded_rect(fb, s.x, s.y, s.w, s.h, 4, Color::from_rgb(0xDD, 0xDD, 0xDD));
int fill_w = (ds->brightness_level * s.w) / 100;
if (fill_w > 0) fill_rounded_rect(fb, s.x, s.y, fill_w, s.h, 4, accent);
int knob_cx = s.x + fill_w;
int knob_cy = s.y + s.h / 2;
fill_circle(fb, knob_cx, knob_cy, BRIGHT_KNOB_R, accent);
fill_circle(fb, knob_cx, knob_cy, BRIGHT_KNOB_R - 3, colors::WHITE);
draw_button(ds, brightness_minus_rect(ds), "-");
draw_button(ds, brightness_plus_rect(ds), "+");
Rect settings = brightness_settings_rect(ds);
if (settings.contains(ds->mouse.x, ds->mouse.y))
fill_rounded_rect(fb, settings.x, settings.y, settings.w, settings.h, 5,
Color::from_rgb(0xF0, 0xF3, 0xF6));
draw_text(fb, settings.x + 6, settings.y + (settings.h - system_font_height()) / 2,
"Display Settings...", colors::TEXT_COLOR);
}
// Returns true when the event was consumed.
bool desktop_brightness_handle_mouse(DesktopState* ds, int mx, int my, bool left_pressed,
bool left_held, bool left_released, int scroll) {
if (!ds->brightness_present) return false;
if (ds->brightness_dragging) {
if (left_held) {
brightness_request(ds, brightness_from_x(ds, mx));
return true;
}
if (left_released) {
ds->brightness_dragging = false;
brightness_apply(ds, ds->brightness_level);
return true;
}
}
// The wheel works over the panel icon too, without opening anything.
bool over_popup = ds->brightness_popup_open && brightness_popup_rect(ds).contains(mx, my);
if (scroll != 0 && (over_popup || ds->brightness_icon_rect.contains(mx, my))) {
brightness_request(ds, ds->brightness_level + scroll * BRIGHT_STEP);
return true;
}
if (!ds->brightness_popup_open || !left_pressed) return false;
if (!over_popup) {
if (!ds->brightness_icon_rect.contains(mx, my)) ds->brightness_popup_open = false;
return false;
}
Rect s = brightness_slider_rect(ds);
if (my >= s.y - 10 && my <= s.y + s.h + 10
&& mx >= s.x - BRIGHT_KNOB_R && mx <= s.x + s.w + BRIGHT_KNOB_R) {
ds->brightness_dragging = true;
brightness_request(ds, brightness_from_x(ds, mx));
return true;
}
if (brightness_minus_rect(ds).contains(mx, my)) {
brightness_request(ds, ds->brightness_level - BRIGHT_STEP);
return true;
}
if (brightness_plus_rect(ds).contains(mx, my)) {
brightness_request(ds, ds->brightness_level + BRIGHT_STEP);
return true;
}
if (brightness_settings_rect(ds).contains(mx, my)) {
ds->brightness_popup_open = false;
desktop_spawn_app(ds, DISPLAY_APP_PATH, nullptr);
return true;
}
return true; // inside the popup, on nothing in particular
}
+4
View File
@@ -273,6 +273,10 @@ void gui::desktop_compose(DesktopState* ds) {
desktop_draw_battery_popup(ds); desktop_draw_battery_popup(ds);
} }
if (ds->brightness_popup_open) {
desktop_draw_brightness_popup(ds);
}
// Draw volume popup if open // Draw volume popup if open
if (ds->vol_popup_open) { if (ds->vol_popup_open) {
desktop_draw_vol_popup(ds); desktop_draw_vol_popup(ds);
+1
View File
@@ -68,6 +68,7 @@ void desktop_lock_screen(DesktopState* ds) {
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
// Cache display name for lock screen rendering // Cache display name for lock screen rendering
montauk::strncpy(ds->lock_display_name, ds->current_user, sizeof(ds->lock_display_name)); montauk::strncpy(ds->lock_display_name, ds->current_user, sizeof(ds->lock_display_name));
@@ -205,6 +205,15 @@ bool desktop_battery_poll(gui::DesktopState* ds, uint64_t now);
void desktop_draw_battery_popup(gui::DesktopState* ds); void desktop_draw_battery_popup(gui::DesktopState* ds);
bool desktop_battery_handle_mouse(gui::DesktopState* ds, int mx, int my); bool desktop_battery_handle_mouse(gui::DesktopState* ds, int mx, int my);
// brightness.cpp
void desktop_brightness_init(gui::DesktopState* ds);
bool desktop_brightness_poll(gui::DesktopState* ds, uint64_t now);
bool desktop_brightness_dragging(const gui::DesktopState* ds);
void desktop_draw_brightness_popup(gui::DesktopState* ds);
bool desktop_brightness_handle_mouse(gui::DesktopState* ds, int mx, int my,
bool left_pressed, bool left_held,
bool left_released, int scroll);
// compose.cpp // compose.cpp
void desktop_draw_lock_screen(gui::DesktopState* ds); void desktop_draw_lock_screen(gui::DesktopState* ds);
void desktop_mark_background_dirty(gui::DesktopState* ds); void desktop_mark_background_dirty(gui::DesktopState* ds);
+28
View File
@@ -187,6 +187,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false; ds->app_menu_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false; ds->battery_popup_open = false;
@@ -518,11 +519,17 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
return; // click inside popup but not on any control return; // click inside popup but not on any control
} else if (!ds->vol_icon_rect.contains(mx, my)) { } else if (!ds->vol_icon_rect.contains(mx, my)) {
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->vol_dragging = false; ds->vol_dragging = false;
} }
} }
} }
// Brightness popup, a drag on its slider, or the wheel over its icon.
if (desktop_brightness_handle_mouse(ds, mx, my, left_pressed, left_held,
left_released, ev.scroll))
return;
// Wi-Fi popup. The passphrase dialog is an ordinary window and needs // Wi-Fi popup. The passphrase dialog is an ordinary window and needs
// nothing here. // nothing here.
if (ds->wifi_popup_open) { if (ds->wifi_popup_open) {
@@ -566,6 +573,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false; ds->battery_popup_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
return; return;
} }
@@ -583,6 +591,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false; ds->battery_popup_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
return; return;
} }
@@ -591,6 +600,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (ds->vol_icon_rect.w > 0 && ds->vol_icon_rect.contains(mx, my)) { if (ds->vol_icon_rect.w > 0 && ds->vol_icon_rect.contains(mx, my)) {
ds->vol_popup_open = !ds->vol_popup_open; ds->vol_popup_open = !ds->vol_popup_open;
ds->vol_dragging = false; ds->vol_dragging = false;
ds->brightness_popup_open = false;
ds->app_menu_open = false; ds->app_menu_open = false;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
@@ -599,11 +609,25 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
return; return;
} }
// Brightness icon
if (ds->brightness_present && ds->brightness_icon_rect.w > 0
&& ds->brightness_icon_rect.contains(mx, my)) {
ds->brightness_popup_open = !ds->brightness_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->battery_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu);
return;
}
// Network icon // Network icon
if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) { if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) {
ds->net_popup_open = !ds->net_popup_open; ds->net_popup_open = !ds->net_popup_open;
ds->app_menu_open = false; ds->app_menu_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false; ds->battery_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
@@ -615,6 +639,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->wifi_popup_open = !ds->wifi_popup_open; ds->wifi_popup_open = !ds->wifi_popup_open;
ds->app_menu_open = false; ds->app_menu_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->battery_popup_open = false; ds->battery_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
@@ -626,6 +651,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->battery_popup_open = !ds->battery_popup_open; ds->battery_popup_open = !ds->battery_popup_open;
ds->app_menu_open = false; ds->app_menu_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
@@ -783,6 +809,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false; ds->app_menu_open = false;
gui::mtk::context_menu_close(ds->ctx_menu); gui::mtk::context_menu_close(ds->ctx_menu);
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
} }
// Forward continuous mouse events to focused window (hover, drag, release). // Forward continuous mouse events to focused window (hover, drag, release).
@@ -861,6 +888,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->battery_popup_open = false; ds->battery_popup_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
} }
} }
} }
+1
View File
@@ -408,6 +408,7 @@ void desktop_open_launcher(DesktopState* ds) {
ds->net_popup_open = false; ds->net_popup_open = false;
ds->wifi_popup_open = false; ds->wifi_popup_open = false;
ds->vol_popup_open = false; ds->vol_popup_open = false;
ds->brightness_popup_open = false;
ds->vol_dragging = false; ds->vol_dragging = false;
desktop_update_launcher_results(ds); desktop_update_launcher_results(ds);
} }
+3
View File
@@ -393,6 +393,7 @@ void gui::desktop_init(DesktopState* ds) {
desktop_refresh_netifs(ds); desktop_refresh_netifs(ds);
desktop_wifi_init(ds); desktop_wifi_init(ds);
desktop_battery_init(ds); desktop_battery_init(ds);
desktop_brightness_init(ds);
desktop_consent_init(ds); desktop_consent_init(ds);
ds->vol_popup_open = false; ds->vol_popup_open = false;
@@ -445,6 +446,7 @@ static bool desktop_has_visible_dirty_window(const DesktopState* ds) {
static bool desktop_has_active_interaction(const DesktopState* ds) { static bool desktop_has_active_interaction(const DesktopState* ds) {
if (ds->vol_dragging) return true; if (ds->vol_dragging) return true;
if (desktop_brightness_dragging(ds)) return true;
for (int i = 0; i < ds->window_count; i++) { for (int i = 0; i < ds->window_count; i++) {
const Window& win = ds->windows[i]; const Window& win = ds->windows[i];
@@ -521,6 +523,7 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
if (desktop_wifi_poll(ds, now)) changed = true; if (desktop_wifi_poll(ds, now)) changed = true;
if (desktop_battery_poll(ds, now)) changed = true; if (desktop_battery_poll(ds, now)) changed = true;
if (desktop_brightness_poll(ds, now)) changed = true;
// Event-driven sync: the kernel mixer bumps a serial on every state // Event-driven sync: the kernel mixer bumps a serial on every state
// change. Reading it is one cheap syscall; refresh only when the serial // change. Reading it is one cheap syscall; refresh only when the serial
+11
View File
@@ -173,6 +173,17 @@ void gui::desktop_draw_panel(DesktopState* ds) {
int icon_y = (PANEL_HEIGHT - 16) / 2; int icon_y = (PANEL_HEIGHT - 16) / 2;
int next_icon_x = vol_icon_x - 16 - 10; int next_icon_x = vol_icon_x - 16 - 10;
// Brightness sits next to the speaker: the two slider controls together.
if (ds->brightness_present) {
ds->brightness_icon_rect = {next_icon_x, icon_y, 16, 16};
next_icon_x -= 16 + 10;
if (ds->icon_brightness.pixels)
fb.blit_alpha(ds->brightness_icon_rect.x, icon_y, ds->icon_brightness.width,
ds->icon_brightness.height, ds->icon_brightness.pixels);
} else {
ds->brightness_icon_rect = {0, 0, 0, 0};
}
if (ds->eth_present) { if (ds->eth_present) {
ds->net_icon_rect = {next_icon_x, icon_y, 16, 16}; ds->net_icon_rect = {next_icon_x, icon_y, 16, 16};
next_icon_x -= 16 + 10; next_icon_x -= 16 + 10;
+1
View File
@@ -12,6 +12,7 @@
#include <montauk/heap.h> #include <montauk/heap.h>
#include <montauk/user.h> #include <montauk/user.h>
#include <montauk/config.h> #include <montauk/config.h>
#include <montauk/capabilities.h>
#include <gui/gui.hpp> #include <gui/gui.hpp>
#include <gui/framebuffer.hpp> #include <gui/framebuffer.hpp>
#include <gui/draw.hpp> #include <gui/draw.hpp>
+4
View File
@@ -52,6 +52,10 @@ void launch_session(LoginState* ls) {
caps.effective = montauk::user::is_admin(ls->username) caps.effective = montauk::user::is_admin(ls->username)
? montauk::abi::CAP_ADMIN_SESSION ? montauk::abi::CAP_ADMIN_SESSION
: montauk::abi::CAP_STANDARD_SESSION; : montauk::abi::CAP_STANDARD_SESSION;
// Clamp to what this login may delegate: a grant table from before a
// capability existed would otherwise make the kernel refuse the spawn
// outright, leaving no session at all rather than one without it.
caps.effective &= montauk::caps::self_delegable();
caps.delegable = caps.effective; caps.delegable = caps.effective;
int pid; int pid;
+1
View File
@@ -30,6 +30,7 @@ static void maybe_run_setup_session() {
caps.effective = admin caps.effective = admin
? montauk::abi::CAP_ADMIN_SESSION ? montauk::abi::CAP_ADMIN_SESSION
: montauk::abi::CAP_STANDARD_SESSION; : montauk::abi::CAP_STANDARD_SESSION;
caps.effective &= montauk::caps::self_delegable();
// Matches launch_session(): a session delegates from what it holds, and // Matches launch_session(): a session delegates from what it holds, and
// the grant table decides which program receives which part of it. // the grant table decides which program receives which part of it.
caps.delegable = caps.effective; caps.delegable = caps.effective;
+1
View File
@@ -153,6 +153,7 @@ ICONS=(
"apps/scalable/bluetooth.svg" "apps/scalable/bluetooth.svg"
"panel/volume-level-high.svg" "panel/volume-level-high.svg"
"status/symbolic/audio-volume-high-symbolic.svg" "status/symbolic/audio-volume-high-symbolic.svg"
"status/symbolic/display-brightness-symbolic.svg"
"apps/scalable/gnome-logout.svg" "apps/scalable/gnome-logout.svg"
"apps/scalable/lock.svg" "apps/scalable/lock.svg"
# Consent prompt, one per capability (see montauk/consent.h). The clock, # Consent prompt, one per capability (see montauk/consent.h). The clock,
+9 -3
View File
@@ -298,8 +298,14 @@ namespace montauk::abi {
never part of a session mask, never part of "all", and never never part of a session mask, never part of "all", and never
delegated by the compositor to what it launches. */ delegated by the compositor to what it launches. */
static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13; static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13;
static constexpr uint64_t CAP_ALL = (1ULL << 14) - 1; /* Set the backlight of the active display (SYS_DISPLAYBRIGHTNESS).
static constexpr uint64_t CAP_STANDARD_SESSION = CAP_POWER_REQUEST | CAP_SUSPEND; A seat preference rather than display administration, so every
session holds it; CAP_DISPLAY_ADMIN keeps mode setting. Reading the
level needs nothing. */
static constexpr uint64_t CAP_BRIGHTNESS = 1ULL << 14;
static constexpr uint64_t CAP_ALL = (1ULL << 15) - 1;
static constexpr uint64_t CAP_STANDARD_SESSION =
CAP_POWER_REQUEST | CAP_SUSPEND | CAP_BRIGHTNESS;
static constexpr uint64_t CAP_ADMIN_SESSION = static constexpr uint64_t CAP_ADMIN_SESSION =
CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN | CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN |
CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN | CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN |
@@ -351,7 +357,7 @@ namespace montauk::abi {
deferred delegation, not amplification. */ deferred delegation, not amplification. */
static constexpr uint64_t CAP_CONSENT_ELIGIBLE = static constexpr uint64_t CAP_CONSENT_ELIGIBLE =
CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME |
CAP_DISPLAY_ADMIN | CAP_LOG_READ; CAP_DISPLAY_ADMIN | CAP_LOG_READ | CAP_BRIGHTNESS;
static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0, static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0,
"consent can only hand out authority an admin session holds"); "consent can only hand out authority an admin session holds");
@@ -38,6 +38,7 @@ namespace caps {
{"log_read", montauk::abi::CAP_LOG_READ}, {"log_read", montauk::abi::CAP_LOG_READ},
{"system_image", montauk::abi::CAP_SYSTEM_IMAGE}, {"system_image", montauk::abi::CAP_SYSTEM_IMAGE},
{"compositor", montauk::abi::CAP_COMPOSITOR}, {"compositor", montauk::abi::CAP_COMPOSITOR},
{"brightness", montauk::abi::CAP_BRIGHTNESS},
}; };
// Never implied by "all": each must be named explicitly in a grant. // Never implied by "all": each must be named explicitly in a grant.
+3 -1
View File
@@ -31,10 +31,12 @@ namespace consent {
"0:/os/icons/network-wired.svg"}, "0:/os/icons/network-wired.svg"},
{montauk::abi::CAP_SET_TIME, "change the system clock and time zone", {montauk::abi::CAP_SET_TIME, "change the system clock and time zone",
"0:/os/icons/preferences-system-time.svg"}, "0:/os/icons/preferences-system-time.svg"},
{montauk::abi::CAP_DISPLAY_ADMIN, "change the screen resolution and brightness", {montauk::abi::CAP_DISPLAY_ADMIN, "change the screen resolution",
"0:/os/icons/preferences-desktop-display.svg"}, "0:/os/icons/preferences-desktop-display.svg"},
{montauk::abi::CAP_LOG_READ, "read the system log, which can reveal what other apps and devices are doing", {montauk::abi::CAP_LOG_READ, "read the system log, which can reveal what other apps and devices are doing",
"0:/os/icons/utilities-log-viewer.svg"}, "0:/os/icons/utilities-log-viewer.svg"},
{montauk::abi::CAP_BRIGHTNESS, "change the screen brightness",
"0:/os/icons/preferences-desktop-display.svg"},
}; };
// Fallback for a capability without its own icon. // Fallback for a capability without its own icon.