diff --git a/kernel/src/ACPI/AcpiVideo.cpp b/kernel/src/ACPI/AcpiVideo.cpp new file mode 100644 index 0000000..0e62663 --- /dev/null +++ b/kernel/src/ACPI/AcpiVideo.cpp @@ -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 +#include +#include +#include +#include + +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; + } + + }; +}; diff --git a/kernel/src/ACPI/AcpiVideo.hpp b/kernel/src/ACPI/AcpiVideo.hpp new file mode 100644 index 0000000..b394867 --- /dev/null +++ b/kernel/src/ACPI/AcpiVideo.hpp @@ -0,0 +1,23 @@ +/* + * AcpiVideo.hpp + * ACPI video extensions (_BCL/_BCM/_BQC) panel brightness fallback + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +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); + + }; +}; diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index ffdf6c3..69223bc 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 283 +#define MONTAUK_BUILD_NUMBER 287 diff --git a/kernel/src/Api/Graphics.hpp b/kernel/src/Api/Graphics.hpp index fb3304b..11eabcf 100644 --- a/kernel/src/Api/Graphics.hpp +++ b/kernel/src/Api/Graphics.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "Syscall.hpp" @@ -128,8 +129,30 @@ namespace montauk::abi { 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) { - 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) { @@ -145,8 +168,12 @@ namespace montauk::abi { 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) { - 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() { diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 314bb42..c5433af 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -207,7 +207,7 @@ namespace montauk::abi { return Sys_DisplaySetMode((int)frame->arg1); case SYS_DISPLAYBRIGHTNESS: 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); case SYS_GETEXECPATH: if (!UserMemory::Range(frame->arg2 ? frame->arg1 : frame->arg1, frame->arg2, true)) return BadPointer; diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 001d2e2..3306415 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -385,8 +385,14 @@ namespace montauk::abi { never part of a session mask, never part of "all", and never delegated by the compositor to what it launches. */ static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13; - static constexpr uint64_t CAP_ALL = (1ULL << 14) - 1; - static constexpr uint64_t CAP_STANDARD_SESSION = CAP_POWER_REQUEST | CAP_SUSPEND; + /* Set the backlight of the active display (SYS_DISPLAYBRIGHTNESS). + 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 = CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN | CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN | @@ -438,7 +444,7 @@ namespace montauk::abi { deferred delegation, not amplification. */ static constexpr uint64_t CAP_CONSENT_ELIGIBLE = 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, "consent can only hand out authority an admin session holds"); diff --git a/kernel/src/Drivers/Graphics/IntelGPU.cpp b/kernel/src/Drivers/Graphics/IntelGPU.cpp index e5a8e7b..436a048 100644 --- a/kernel/src/Drivers/Graphics/IntelGPU.cpp +++ b/kernel/src/Drivers/Graphics/IntelGPU.cpp @@ -82,8 +82,7 @@ namespace Drivers::Graphics::IntelGPU { static uint8_t g_connectorType = montauk::abi::DISPLAY_CONNECTOR_UNKNOWN; static bool g_connectorConnected = false; static bool g_edidValid = false; - static bool g_pwmSupported = false; - static uint32_t g_pwmCtlReg = 0; + static int g_ddiPort = -1; // DDI port driving the output, -1 unknown static char g_connectorName[32] = "Unknown"; static char g_monitorName[64] = "Unknown display"; static uint8_t g_auxEdid[256] = {}; @@ -556,10 +555,11 @@ namespace Drivers::Graphics::IntelGPU { } 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++) { uint8_t reply = 0xFF; - int result = AuxTransfer(request, 0x50, writeData, writeSize, + int result = AuxTransfer(request, i2cAddress, writeData, writeSize, readData, readSize, &reply); uint8_t nativeReply = reply & 0x3; uint8_t i2cReply = reply & 0xC; @@ -600,7 +600,8 @@ namespace Drivers::Graphics::IntelGPU { static bool ReadDpEdid() { 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) return false; @@ -789,9 +790,16 @@ namespace Drivers::Graphics::IntelGPU { g_connectorConnected = (ReadReg(trans.conf) & PIPECONF_ENABLE) != 0; uint32_t ddi = ReadReg(trans.ddi); g_auxControlReg = 0; + g_ddiPort = -1; 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; CopyText(g_connectorName, sizeof(g_connectorName), "LVDS-1"); } else if (ReadReg(ADPA) & ADPA_DAC_ENABLE) { @@ -807,14 +815,30 @@ namespace Drivers::Graphics::IntelGPU { } else if (trans.edp) { g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP; CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1"); + g_ddiPort = 0; + g_auxControlReg = DP_AUX_A_CTL; } else { uint32_t mode = (ddi & TRANS_DDI_MODE_MASK) >> 24; - uint32_t port = (ddi & TRANS_DDI_PORT_MASK) >> 27; - const char* suffix = port == 0 ? "A" : port == 1 ? "B" - : port == 2 ? "C" : port == 3 ? "D" - : port == 8 ? "TC1" : port == 9 ? "TC2" - : port == 10 ? "TC3" : port == 11 ? "TC4" - : "unknown"; + // Gen12 widened the port select to bits 30:27 and stores port + 1 + // (0 = none); earlier DDI parts store the port in bits 30:28. + // Gen12 numbering puts the Type-C ports from D (index 3) onward. + int port; + if (g_gpuGen >= 12) { + 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) { g_connectorType = montauk::abi::DISPLAY_CONNECTOR_HDMI; CopyText(g_connectorName, sizeof(g_connectorName), "HDMI-"); @@ -824,11 +848,9 @@ namespace Drivers::Graphics::IntelGPU { } else { g_connectorType = montauk::abi::DISPLAY_CONNECTOR_DP; CopyText(g_connectorName, sizeof(g_connectorName), "DP-"); - static constexpr uint32_t auxControls[] = { - DP_AUX_A_CTL, DP_AUX_B_CTL, DP_AUX_C_CTL, DP_AUX_D_CTL, - }; - if (port < sizeof(auxControls) / sizeof(auxControls[0])) - g_auxControlReg = auxControls[port]; + // AUX channels are laid out per port, Type-C ones included. + if (port >= 0 && port <= 8) + g_auxControlReg = DP_AUX_A_CTL + (uint32_t)port * 0x100; } uint64_t len = 0; while (len + 1 < sizeof(g_connectorName) && g_connectorName[len]) len++; @@ -913,25 +935,544 @@ namespace Drivers::Graphics::IntelGPU { } } - static void DiscoverPwm() { - g_pwmSupported = false; - if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP - && g_connectorType != montauk::abi::DISPLAY_CONNECTOR_LVDS) - return; + // ========================================================================= + // Backlight + // ========================================================================= - uint32_t pch = ReadReg(BLC_PWM_PCH_CTL2); - if ((pch >> 16) != 0) { - g_pwmCtlReg = BLC_PWM_PCH_CTL2; - g_pwmSupported = true; + // South-display family. The PWM register layout and the GMBUS pin map + // follow the PCH, not the GPU: a Coffee Lake on Cannon Point and a Kaby + // Lake on Sunrise Point are both Gen 9 but have incompatible PWM blocks. + 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 { - uint32_t cpu = ReadReg(BLC_PWM_CPU_CTL); - if ((cpu >> 16) != 0) { - g_pwmCtlReg = BLC_PWM_CPU_CTL; - g_pwmSupported = true; + uint32_t value = ReadReg(g_blDutyReg); + WriteReg(g_blDutyReg, (value & 0xFFFF0000u) | (duty & 0xFFFF)); + } + (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 " - << (g_pwmSupported ? "available" : "not exposed"); + if (ok) ok = GmbusWait(GMBUS_HW_WAIT_PHASE); + + 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() { @@ -942,7 +1483,7 @@ namespace Drivers::Graphics::IntelGPU { DiscoverActiveTranscoder(); ParseEdid(); SelectCurrentMode(); - DiscoverPwm(); + DiscoverBacklight(); } // ========================================================================= @@ -1660,7 +2201,6 @@ namespace Drivers::Graphics::IntelGPU { out->capabilities = montauk::abi::DISPLAY_CAP_MODESET; 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; for (int i = 0; i < g_modeCount; i++) { if (i != g_currentMode @@ -1672,6 +2212,7 @@ namespace Drivers::Graphics::IntelGPU { out->modeCount = (uint32_t)g_modeCount; out->currentMode = g_currentMode; out->brightness = Brightness(-1); + if (out->brightness >= 0) out->capabilities |= montauk::abi::DISPLAY_CAP_BRIGHTNESS; out->deviceId = g_gpuInfo.deviceId; out->generation = g_gpuInfo.gen; out->connectorType = g_connectorType; @@ -1737,20 +2278,28 @@ namespace Drivers::Graphics::IntelGPU { } int Brightness(int percent) { - if (!g_initialized || !g_pwmSupported) return -1; - uint32_t value = ReadReg(g_pwmCtlReg); - uint32_t maximum = value >> 16; - if (maximum == 0) return -1; - if (percent < 0) { - uint32_t duty = value & 0xFFFF; - if (duty > maximum) duty = maximum; - return (int)(((uint64_t)duty * 100 + maximum / 2) / maximum); + if (!g_initialized) return -1; + if (g_blMethod == BacklightMethod::None && !g_ddcCandidate) return -1; + g_blLock.Acquire(); + int result = BrightnessLocked(percent); + g_blLock.Release(); + return result; + } + + 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; - 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); + return nullptr; } // ========================================================================= @@ -1993,6 +2542,12 @@ namespace Drivers::Graphics::IntelGPU { << 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"; } diff --git a/kernel/src/Drivers/Graphics/IntelGPU.hpp b/kernel/src/Drivers/Graphics/IntelGPU.hpp index e7765fd..c9dcef8 100644 --- a/kernel/src/Drivers/Graphics/IntelGPU.hpp +++ b/kernel/src/Drivers/Graphics/IntelGPU.hpp @@ -237,6 +237,7 @@ namespace Drivers::Graphics::IntelGPU { static constexpr uint32_t DVOB = 0x61140; // DVO-B static constexpr uint32_t DVOC = 0x61160; // DVO-C 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_C = 0x64200; // DisplayPort C 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; // 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_CTL = 0x48254; static constexpr uint32_t BLC_PWM_PCH_CTL1 = 0xC8250; static constexpr uint32_t BLC_PWM_PCH_CTL2 = 0xC8254; 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 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_PIPE_B_SELECT = (1u << 30); - // --- GMBUS (I2C for EDID) --- - static constexpr uint32_t GMBUS0 = 0x5100; // Clock/port select - static constexpr uint32_t GMBUS1 = 0x5104; // Command/status - static constexpr uint32_t GMBUS2 = 0x5108; // Status - static constexpr uint32_t GMBUS3 = 0x510C; // Data buffer - static constexpr uint32_t GMBUS4 = 0x5110; // Interrupt mask - static constexpr uint32_t GMBUS5 = 0x5120; // 2-byte index + // --- GMBUS (display I2C: DDC for HDMI/DVI) --- + // Every supported generation is PCH-split, which moves GMBUS from the + // legacy 0x5100 block into the PCH display range. + static constexpr uint32_t GMBUS0 = 0xC5100; // Clock/pin select + static constexpr uint32_t GMBUS1 = 0xC5104; // Command/status + static constexpr uint32_t GMBUS2 = 0xC5108; // Status + 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 --- static constexpr uint32_t HWS_PGA = 0x02080; @@ -496,10 +532,19 @@ namespace Drivers::Graphics::IntelGPU { int SetDisplayMode(int modeIndex); // percent < 0 queries the current brightness. Otherwise sets 0..100 and - // returns the resulting percentage. Returns -1 when no PWM backlight is - // exposed by the active output. + // returns the resulting percentage. Returns -1 when the active output has + // 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); + // 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) // ========================================================================= diff --git a/kernel/src/Sched/Consent.cpp b/kernel/src/Sched/Consent.cpp index 3cc3669..d6823c7 100644 --- a/kernel/src/Sched/Consent.cpp +++ b/kernel/src/Sched/Consent.cpp @@ -32,6 +32,7 @@ namespace Sched::Consent { {"set_time", CAP_SET_TIME}, {"display_admin", CAP_DISPLAY_ADMIN}, {"log_read", CAP_LOG_READ}, + {"brightness", CAP_BRIGHTNESS}, }; constexpr uint64_t EligibleMask() { diff --git a/programs/data/config/capabilities.toml b/programs/data/config/capabilities.toml index 8d6ad75..09f01c5 100644 --- a/programs/data/config/capabilities.toml +++ b/programs/data/config/capabilities.toml @@ -3,7 +3,8 @@ # # Capability names: process_admin, power_request, power_control, suspend, # 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) ==== @@ -26,6 +27,7 @@ delegable = [ "power_request", "suspend", "process_admin", "storage_admin", "raw_storage", "network_admin", "set_time", "user_admin", "display_admin", "device_admin", "log_read", "compositor", + "brightness", ] [grant.sshd] @@ -38,11 +40,12 @@ effective = [ "power_request", "suspend", "process_admin", "storage_admin", "raw_storage", "network_admin", "set_time", "user_admin", "display_admin", "device_admin", "log_read", "compositor", + "brightness", ] delegable = [ "power_request", "suspend", "process_admin", "storage_admin", "raw_storage", "network_admin", "set_time", "user_admin", - "display_admin", "device_admin", "log_read", + "display_admin", "device_admin", "log_read", "brightness", ] [grant.terminal] @@ -68,7 +71,7 @@ delegable = ["network_admin"] [grant.display] path = "0:/apps/display/display.elf" -effective = ["display_admin"] +effective = ["display_admin", "brightness"] [grant.disks] path = "0:/apps/disks/disks.elf" diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 5807e67..f513f59 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -298,8 +298,14 @@ namespace montauk::abi { never part of a session mask, never part of "all", and never delegated by the compositor to what it launches. */ static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13; - static constexpr uint64_t CAP_ALL = (1ULL << 14) - 1; - static constexpr uint64_t CAP_STANDARD_SESSION = CAP_POWER_REQUEST | CAP_SUSPEND; + /* Set the backlight of the active display (SYS_DISPLAYBRIGHTNESS). + 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 = CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN | CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN | @@ -351,7 +357,7 @@ namespace montauk::abi { deferred delegation, not amplification. */ static constexpr uint64_t CAP_CONSENT_ELIGIBLE = 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, "consent can only hand out authority an admin session holds"); diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index 54b948c..8f76ee5 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -227,6 +227,18 @@ struct DesktopState { montauk::abi::BatteryInfo battery_info; 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; Rect vol_icon_rect; int vol_level; // 0-100 diff --git a/programs/include/montauk/capabilities.h b/programs/include/montauk/capabilities.h index bbf0595..fb2dfba 100644 --- a/programs/include/montauk/capabilities.h +++ b/programs/include/montauk/capabilities.h @@ -38,6 +38,7 @@ namespace caps { {"log_read", montauk::abi::CAP_LOG_READ}, {"system_image", montauk::abi::CAP_SYSTEM_IMAGE}, {"compositor", montauk::abi::CAP_COMPOSITOR}, + {"brightness", montauk::abi::CAP_BRIGHTNESS}, }; // Never implied by "all": each must be named explicitly in a grant. diff --git a/programs/include/montauk/consent.h b/programs/include/montauk/consent.h index 93abc45..093ea73 100644 --- a/programs/include/montauk/consent.h +++ b/programs/include/montauk/consent.h @@ -31,10 +31,12 @@ namespace consent { "0:/os/icons/network-wired.svg"}, {montauk::abi::CAP_SET_TIME, "change the system clock and time zone", "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"}, {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"}, + {montauk::abi::CAP_BRIGHTNESS, "change the screen brightness", + "0:/os/icons/preferences-desktop-display.svg"}, }; // Fallback for a capability without its own icon. diff --git a/programs/lib/libjpeg/libjpeg.a b/programs/lib/libjpeg/libjpeg.a index 1b4f728..df67214 100644 Binary files a/programs/lib/libjpeg/libjpeg.a and b/programs/lib/libjpeg/libjpeg.a differ diff --git a/programs/lib/libjpegwrite/libjpegwrite.a b/programs/lib/libjpegwrite/libjpegwrite.a index a4ca89b..5cba234 100644 Binary files a/programs/lib/libjpegwrite/libjpegwrite.a and b/programs/lib/libjpegwrite/libjpegwrite.a differ diff --git a/programs/lib/tls/libtls.a b/programs/lib/tls/libtls.a index b4e9ae8..adbb377 100644 Binary files a/programs/lib/tls/libtls.a and b/programs/lib/tls/libtls.a differ diff --git a/programs/src/capabilities/main.cpp b/programs/src/capabilities/main.cpp index a35fd31..722877b 100644 --- a/programs/src/capabilities/main.cpp +++ b/programs/src/capabilities/main.cpp @@ -20,7 +20,7 @@ extern "C" { using namespace gui; 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 FOOTER_H = 44; static constexpr int PAD = 18; @@ -60,11 +60,12 @@ static const char* const kCapDescriptions[CAP_COUNT] = { "Configure network interfaces, DHCP and Wi-Fi", "Set the system clock", "Manage user accounts and session identities", - "Change display modes and panel brightness", + "Change display modes", "Manage hardware devices (Bluetooth, radio)", "Read the kernel log", "Write to 0:/os and 0:/apps (never part of \"all\")", "Read and send input to other windows (never part of \"all\")", + "Set the display backlight", }; // ==== Grant table model ==== diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 4760605..3233223 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -56,7 +56,7 @@ LDFLAGS := \ # ---- 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)) SRCS := $(CORE_SRCS) $(APP_SRCS) OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) diff --git a/programs/src/desktop/brightness.cpp b/programs/src/desktop/brightness.cpp new file mode 100644 index 0000000..817e867 --- /dev/null +++ b/programs/src/desktop/brightness.cpp @@ -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 + +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 +} diff --git a/programs/src/desktop/compose.cpp b/programs/src/desktop/compose.cpp index 90b1637..3f47102 100644 --- a/programs/src/desktop/compose.cpp +++ b/programs/src/desktop/compose.cpp @@ -273,6 +273,10 @@ void gui::desktop_compose(DesktopState* ds) { desktop_draw_battery_popup(ds); } + if (ds->brightness_popup_open) { + desktop_draw_brightness_popup(ds); + } + // Draw volume popup if open if (ds->vol_popup_open) { desktop_draw_vol_popup(ds); diff --git a/programs/src/desktop/desktop_builtin.cpp b/programs/src/desktop/desktop_builtin.cpp index efb885a..70e0ff1 100644 --- a/programs/src/desktop/desktop_builtin.cpp +++ b/programs/src/desktop/desktop_builtin.cpp @@ -68,6 +68,7 @@ void desktop_lock_screen(DesktopState* ds) { ds->net_popup_open = false; ds->wifi_popup_open = false; ds->vol_popup_open = false; + ds->brightness_popup_open = false; // Cache display name for lock screen rendering montauk::strncpy(ds->lock_display_name, ds->current_user, sizeof(ds->lock_display_name)); diff --git a/programs/src/desktop/desktop_internal.hpp b/programs/src/desktop/desktop_internal.hpp index 68658fa..f97ce6b 100644 --- a/programs/src/desktop/desktop_internal.hpp +++ b/programs/src/desktop/desktop_internal.hpp @@ -205,6 +205,15 @@ bool desktop_battery_poll(gui::DesktopState* ds, uint64_t now); void desktop_draw_battery_popup(gui::DesktopState* ds); 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 void desktop_draw_lock_screen(gui::DesktopState* ds); void desktop_mark_background_dirty(gui::DesktopState* ds); diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index e29258a..cdc1dce 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -187,6 +187,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->app_menu_open = false; gui::mtk::context_menu_close(ds->ctx_menu); ds->vol_popup_open = false; + ds->brightness_popup_open = false; ds->net_popup_open = false; ds->wifi_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 } else if (!ds->vol_icon_rect.contains(mx, my)) { ds->vol_popup_open = false; + ds->brightness_popup_open = 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 // nothing here. if (ds->wifi_popup_open) { @@ -566,6 +573,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->wifi_popup_open = false; ds->battery_popup_open = false; ds->vol_popup_open = false; + ds->brightness_popup_open = false; gui::mtk::context_menu_close(ds->ctx_menu); return; } @@ -583,6 +591,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->wifi_popup_open = false; ds->battery_popup_open = false; ds->vol_popup_open = false; + ds->brightness_popup_open = false; gui::mtk::context_menu_close(ds->ctx_menu); 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)) { ds->vol_popup_open = !ds->vol_popup_open; ds->vol_dragging = false; + ds->brightness_popup_open = false; ds->app_menu_open = false; ds->net_popup_open = false; ds->wifi_popup_open = false; @@ -599,11 +609,25 @@ void gui::desktop_handle_mouse(DesktopState* ds) { 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 if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) { ds->net_popup_open = !ds->net_popup_open; ds->app_menu_open = false; ds->vol_popup_open = false; + ds->brightness_popup_open = false; ds->wifi_popup_open = false; ds->battery_popup_open = false; 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->app_menu_open = false; ds->vol_popup_open = false; + ds->brightness_popup_open = false; ds->net_popup_open = false; ds->battery_popup_open = false; 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->app_menu_open = false; ds->vol_popup_open = false; + ds->brightness_popup_open = false; ds->net_popup_open = false; ds->wifi_popup_open = false; gui::mtk::context_menu_close(ds->ctx_menu); @@ -783,6 +809,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->app_menu_open = false; gui::mtk::context_menu_close(ds->ctx_menu); ds->vol_popup_open = false; + ds->brightness_popup_open = false; } // 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->battery_popup_open = false; ds->vol_popup_open = false; + ds->brightness_popup_open = false; } } } diff --git a/programs/src/desktop/launcher.cpp b/programs/src/desktop/launcher.cpp index 5ecc75e..69ef62a 100644 --- a/programs/src/desktop/launcher.cpp +++ b/programs/src/desktop/launcher.cpp @@ -408,6 +408,7 @@ void desktop_open_launcher(DesktopState* ds) { ds->net_popup_open = false; ds->wifi_popup_open = false; ds->vol_popup_open = false; + ds->brightness_popup_open = false; ds->vol_dragging = false; desktop_update_launcher_results(ds); } diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 5655343..1652431 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -393,6 +393,7 @@ void gui::desktop_init(DesktopState* ds) { desktop_refresh_netifs(ds); desktop_wifi_init(ds); desktop_battery_init(ds); + desktop_brightness_init(ds); desktop_consent_init(ds); 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) { if (ds->vol_dragging) return true; + if (desktop_brightness_dragging(ds)) return true; for (int i = 0; i < ds->window_count; 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_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 // change. Reading it is one cheap syscall; refresh only when the serial diff --git a/programs/src/desktop/panel.cpp b/programs/src/desktop/panel.cpp index 2da3b0e..911548c 100644 --- a/programs/src/desktop/panel.cpp +++ b/programs/src/desktop/panel.cpp @@ -173,6 +173,17 @@ void gui::desktop_draw_panel(DesktopState* ds) { int icon_y = (PANEL_HEIGHT - 16) / 2; 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) { ds->net_icon_rect = {next_icon_x, icon_y, 16, 16}; next_icon_x -= 16 + 10; diff --git a/programs/src/login/login.hpp b/programs/src/login/login.hpp index 283da32..6802a04 100644 --- a/programs/src/login/login.hpp +++ b/programs/src/login/login.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include diff --git a/programs/src/login/login_input.cpp b/programs/src/login/login_input.cpp index 8af32fe..4a493fc 100644 --- a/programs/src/login/login_input.cpp +++ b/programs/src/login/login_input.cpp @@ -52,6 +52,10 @@ void launch_session(LoginState* ls) { caps.effective = montauk::user::is_admin(ls->username) ? montauk::abi::CAP_ADMIN_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; int pid; diff --git a/programs/src/login/main.cpp b/programs/src/login/main.cpp index 3f593d1..cc0849b 100644 --- a/programs/src/login/main.cpp +++ b/programs/src/login/main.cpp @@ -30,6 +30,7 @@ static void maybe_run_setup_session() { caps.effective = admin ? montauk::abi::CAP_ADMIN_SESSION : montauk::abi::CAP_STANDARD_SESSION; + caps.effective &= montauk::caps::self_delegable(); // Matches launch_session(): a session delegates from what it holds, and // the grant table decides which program receives which part of it. caps.delegable = caps.effective; diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index b47ab9e..6e9ce8d 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -153,6 +153,7 @@ ICONS=( "apps/scalable/bluetooth.svg" "panel/volume-level-high.svg" "status/symbolic/audio-volume-high-symbolic.svg" + "status/symbolic/display-brightness-symbolic.svg" "apps/scalable/gnome-logout.svg" "apps/scalable/lock.svg" # Consent prompt, one per capability (see montauk/consent.h). The clock, diff --git a/template/sysroot/include/Api/Syscall.hpp b/template/sysroot/include/Api/Syscall.hpp index 5807e67..f513f59 100644 --- a/template/sysroot/include/Api/Syscall.hpp +++ b/template/sysroot/include/Api/Syscall.hpp @@ -298,8 +298,14 @@ namespace montauk::abi { never part of a session mask, never part of "all", and never delegated by the compositor to what it launches. */ static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13; - static constexpr uint64_t CAP_ALL = (1ULL << 14) - 1; - static constexpr uint64_t CAP_STANDARD_SESSION = CAP_POWER_REQUEST | CAP_SUSPEND; + /* Set the backlight of the active display (SYS_DISPLAYBRIGHTNESS). + 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 = CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN | CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN | @@ -351,7 +357,7 @@ namespace montauk::abi { deferred delegation, not amplification. */ static constexpr uint64_t CAP_CONSENT_ELIGIBLE = 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, "consent can only hand out authority an admin session holds"); diff --git a/template/sysroot/include/montauk/capabilities.h b/template/sysroot/include/montauk/capabilities.h index bbf0595..fb2dfba 100644 --- a/template/sysroot/include/montauk/capabilities.h +++ b/template/sysroot/include/montauk/capabilities.h @@ -38,6 +38,7 @@ namespace caps { {"log_read", montauk::abi::CAP_LOG_READ}, {"system_image", montauk::abi::CAP_SYSTEM_IMAGE}, {"compositor", montauk::abi::CAP_COMPOSITOR}, + {"brightness", montauk::abi::CAP_BRIGHTNESS}, }; // Never implied by "all": each must be named explicitly in a grant. diff --git a/template/sysroot/include/montauk/consent.h b/template/sysroot/include/montauk/consent.h index 93abc45..093ea73 100644 --- a/template/sysroot/include/montauk/consent.h +++ b/template/sysroot/include/montauk/consent.h @@ -31,10 +31,12 @@ namespace consent { "0:/os/icons/network-wired.svg"}, {montauk::abi::CAP_SET_TIME, "change the system clock and time zone", "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"}, {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"}, + {montauk::abi::CAP_BRIGHTNESS, "change the screen brightness", + "0:/os/icons/preferences-desktop-display.svg"}, }; // Fallback for a capability without its own icon.