From f9e21e0f13b5a35c5d7bb75117a30d098d1a82db Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Fri, 2 Oct 2026 14:54:00 +0200 Subject: [PATCH] fix: add backlight modulation support for hybrid graphics laptops --- kernel/src/ACPI/AcpiWmi.cpp | 127 +++++++++++++++++++++++ kernel/src/ACPI/AcpiWmi.hpp | 44 ++++++++ kernel/src/ACPI/WmiEcBacklight.cpp | 160 +++++++++++++++++++++++++++++ kernel/src/ACPI/WmiEcBacklight.hpp | 28 +++++ kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Api/Graphics.hpp | 32 ++++-- 6 files changed, 381 insertions(+), 12 deletions(-) create mode 100644 kernel/src/ACPI/AcpiWmi.cpp create mode 100644 kernel/src/ACPI/AcpiWmi.hpp create mode 100644 kernel/src/ACPI/WmiEcBacklight.cpp create mode 100644 kernel/src/ACPI/WmiEcBacklight.hpp diff --git a/kernel/src/ACPI/AcpiWmi.cpp b/kernel/src/ACPI/AcpiWmi.cpp new file mode 100644 index 0000000..255547f --- /dev/null +++ b/kernel/src/ACPI/AcpiWmi.cpp @@ -0,0 +1,127 @@ +/* + * AcpiWmi.cpp + * ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call + * its WMxx method + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "AcpiWmi.hpp" +#include "AcpiDevices.hpp" +#include +#include +#include +#include + +using namespace Kt; + +namespace Hal { + namespace AcpiWmi { + + static constexpr const char* HidWmi = "PNP0C14"; + static constexpr uint32_t WdgEntrySize = 20; // GUID, id, count, flags + + static int HexDigit(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + } + + bool ParseGuid(const char* text, uint8_t out[16]) { + // Bytes in text order, then the first three fields byte-swapped. + uint8_t bytes[16]; + int count = 0; + for (int i = 0; text[i]; i++) { + if (text[i] == '-') continue; + int hi = HexDigit(text[i]); + int lo = text[i + 1] ? HexDigit(text[i + 1]) : -1; + if (hi < 0 || lo < 0 || count == 16) return false; + bytes[count++] = (uint8_t)(hi << 4 | lo); + i++; + } + if (count != 16) return false; + static constexpr int order[16] = {3, 2, 1, 0, 5, 4, 7, 6, + 8, 9, 10, 11, 12, 13, 14, 15}; + for (int i = 0; i < 16; i++) out[i] = bytes[order[i]]; + return true; + } + + static bool SearchDevice(int32_t device, const uint8_t guid[16], Block& out) { + auto& interp = AML::GetInterpreter(); + AML::Object wdg{}; + if (!interp.EvaluateChild(device, "_WDG", nullptr, 0, wdg) + || wdg.Type != AML::ObjectType::Buffer) + return false; + + // The interpreter caps buffers; a _WDG that fills the cap may have + // lost entries past it. + if (wdg.Buffer.Length >= (uint32_t)AML::MaxBufferLen) { + char path[128]; + interp.GetNamespace().GetNodePath(device, path, sizeof(path)); + KernelLogStream(WARNING, "WMI") << path << "._WDG may be truncated (" + << base::dec << (uint64_t)wdg.Buffer.Length << " bytes read)"; + } + + for (uint32_t off = 0; off + WdgEntrySize <= wdg.Buffer.Length; off += WdgEntrySize) { + const uint8_t* entry = wdg.Buffer.Data + off; + if (memcmp(entry, guid, 16) != 0) continue; + out.Device = device; + out.ObjectId[0] = (char)entry[16]; + out.ObjectId[1] = (char)entry[17]; + out.InstanceCount = entry[18]; + out.Flags = entry[19]; + return true; + } + return false; + } + + bool Find(const uint8_t guid[16], Block& out) { + auto& interp = AML::GetInterpreter(); + if (!interp.IsInitialized()) return false; + auto& ns = interp.GetNamespace(); + + bool found = false; + ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device, + [&](int32_t idx, const AML::NamespaceNode*) { + if (found) return; + char hid[16]; + if (!AcpiDevices::EvaluateHid(idx, hid, sizeof(hid))) return; + if (memcmp(hid, HidWmi, sizeof("PNP0C14")) != 0) return; + if (!(AcpiDevices::EvaluateSta(idx) & AcpiDevices::STA_PRESENT)) return; + found = SearchDevice(idx, guid, out); + }); + return found; + } + + bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId, + const uint8_t* input, uint32_t inputLength, + AML::Object& result) { + if (!(block.Flags & FlagMethod) || instance >= block.InstanceCount) + return false; + + AML::Object args[3]; + args[0].Type = AML::ObjectType::Integer; + args[0].Integer = instance; + args[1].Type = AML::ObjectType::Integer; + args[1].Integer = methodId; + if (block.Flags & FlagString) { + uint32_t n = inputLength; + if (n >= (uint32_t)AML::MaxStringLen) n = AML::MaxStringLen - 1; + args[2].Type = AML::ObjectType::String; + if (n) memcpy(args[2].String.Data, input, n); + args[2].String.Data[n] = '\0'; + args[2].String.Length = (uint16_t)n; + } else { + uint32_t n = inputLength; + if (n > (uint32_t)AML::MaxBufferLen) n = AML::MaxBufferLen; + args[2].Type = AML::ObjectType::Buffer; + if (n) memcpy(args[2].Buffer.Data, input, n); + args[2].Buffer.Length = n; + } + + char method[5] = {'W', 'M', block.ObjectId[0], block.ObjectId[1], '\0'}; + return AML::GetInterpreter().EvaluateChild(block.Device, method, args, 3, result); + } + + }; +}; diff --git a/kernel/src/ACPI/AcpiWmi.hpp b/kernel/src/ACPI/AcpiWmi.hpp new file mode 100644 index 0000000..08bf52c --- /dev/null +++ b/kernel/src/ACPI/AcpiWmi.hpp @@ -0,0 +1,44 @@ +/* + * AcpiWmi.hpp + * ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call + * its WMxx method + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Hal { + namespace AcpiWmi { + + // _WDG flags (Microsoft "Windows Instrumentation: WMI and ACPI"). + static constexpr uint8_t FlagExpensive = 0x1; + static constexpr uint8_t FlagMethod = 0x2; + static constexpr uint8_t FlagString = 0x4; + static constexpr uint8_t FlagEvent = 0x8; + + // One _WDG entry, bound to the PNP0C14 device that declared it. + struct Block { + int32_t Device; // namespace node of the PNP0C14 device + char ObjectId[2]; // names the WQxx / WSxx / WMxx methods + uint8_t InstanceCount; + uint8_t Flags; + }; + + // Parse "603E9613-EF25-4338-A3D0-C46177516DB7" into the byte order + // _WDG stores (first three fields little-endian). False if malformed. + bool ParseGuid(const char* text, uint8_t out[16]); + + // Search every present PNP0C14 device's _WDG for `guid`. The first + // declaration wins, as on Linux. + bool Find(const uint8_t guid[16], Block& out); + + // Evaluate WMxx(instance, methodId, input) on a method block. The input + // is passed as a String instead of a Buffer when the block says so. + bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId, + const uint8_t* input, uint32_t inputLength, + AML::Object& result); + + }; +}; diff --git a/kernel/src/ACPI/WmiEcBacklight.cpp b/kernel/src/ACPI/WmiEcBacklight.cpp new file mode 100644 index 0000000..e69d529 --- /dev/null +++ b/kernel/src/ACPI/WmiEcBacklight.cpp @@ -0,0 +1,160 @@ +/* + * WmiEcBacklight.cpp + * Firmware (EC) panel backlight behind the WMI interface NVIDIA specified + * for display-mux laptops + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "WmiEcBacklight.hpp" +#include "AcpiWmi.hpp" +#include "AcpiPower.hpp" +#include +#include +#include +#include + +using namespace Kt; + +namespace Hal { + namespace WmiEcBacklight { + + // ============================================================================ + + // Interface + + // ============================================================================ + static constexpr const char* Guid = "603E9613-EF25-4338-A3D0-C46177516DB7"; + + // WMxx method IDs. + static constexpr uint32_t MethodLevel = 1; + static constexpr uint32_t MethodSource = 2; + + // `mode` values in the argument block. + static constexpr uint32_t ModeGet = 0; + static constexpr uint32_t ModeSet = 1; + static constexpr uint32_t ModeGetMaxLevel = 2; + + static constexpr uint64_t SourceEc = 2; // 1 = GPU, 3 = eDP AUX + + // Lowest level driven, so the panel never goes fully dark. + static constexpr int MinPercent = 1; + + // ============================================================================ + + // State + + // ============================================================================ + static kcp::Mutex g_lock; + static bool g_probed = false; + static bool g_available = false; + static AcpiWmi::Block g_block{}; + static uint32_t g_maxLevel = 0; + + // Argument block: { u32 mode; u32 val; u32 ret; u32 reserved[3]; }. + // The method answers with an Integer. Linux reads it back as the + // `ret` field of the same block, which is where ACPICA's copy-out of + // an Integer puts the value; a firmware returning the block itself as + // a Buffer is read at that same offset. + static bool Call(uint32_t method, uint32_t mode, uint32_t value, uint32_t& out) { + uint8_t args[24] = {}; + args[0] = (uint8_t)mode; + args[1] = (uint8_t)(mode >> 8); + args[2] = (uint8_t)(mode >> 16); + args[3] = (uint8_t)(mode >> 24); + args[4] = (uint8_t)value; + args[5] = (uint8_t)(value >> 8); + args[6] = (uint8_t)(value >> 16); + args[7] = (uint8_t)(value >> 24); + + AML::Object result{}; + if (!AcpiWmi::EvaluateMethod(g_block, 0, method, args, sizeof(args), result)) + return false; + if (result.Type == AML::ObjectType::Integer) { + out = (uint32_t)result.Integer; + return true; + } + if (result.Type == AML::ObjectType::Buffer && result.Buffer.Length >= 12) { + const uint8_t* r = result.Buffer.Data + 8; + out = r[0] | (uint32_t)r[1] << 8 | (uint32_t)r[2] << 16 | (uint32_t)r[3] << 24; + return true; + } + // A set may legitimately return nothing. + out = 0; + return mode == ModeSet; + } + + static void Probe() { + g_probed = true; + uint8_t guid[16]; + if (!AcpiWmi::ParseGuid(Guid, guid) || !AcpiWmi::Find(guid, g_block)) return; + if (!(g_block.Flags & AcpiWmi::FlagMethod) || g_block.InstanceCount == 0) return; + + char path[128]; + AML::GetInterpreter().GetNamespace().GetNodePath(g_block.Device, path, sizeof(path)); + + // The interface also exists on machines where the GPU drives the + // backlight; there it must be left alone. + uint32_t source = 0; + if (!Call(MethodSource, ModeGet, 0, source)) { + KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path + << ": source query failed"; + return; + } + if (source != SourceEc) { + KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path + << " reports source " << base::dec << (uint64_t)source << ", not EC"; + return; + } + + uint32_t maximum = 0; + if (!Call(MethodLevel, ModeGetMaxLevel, 0, maximum) || maximum == 0) { + KernelLogStream(WARNING, "Backlight") << "WMI EC backlight at " << path + << ": no maximum level"; + return; + } + g_maxLevel = maximum; + g_available = true; + char method[5] = {'W', 'M', g_block.ObjectId[0], g_block.ObjectId[1], '\0'}; + KernelLogStream(OK, "Backlight") << "Panel brightness via EC (WMI " + << path << "." << method << ", " << base::dec << (uint64_t)maximum + << " levels)"; + } + + static int ToPercent(uint32_t level) { + if (level > g_maxLevel) level = g_maxLevel; + return (int)(((uint64_t)level * 100 + g_maxLevel / 2) / g_maxLevel); + } + + // ============================================================================ + + // Public API + + // ============================================================================ + int Brightness(int percent) { + g_lock.Acquire(); + if (!g_probed) Probe(); + int result = -1; + if (g_available) { + uint32_t level = 0; + if (percent >= 0) { + if (percent > 100) percent = 100; + if (percent < MinPercent) percent = MinPercent; + uint32_t target = (uint32_t)(((uint64_t)g_maxLevel * (uint32_t)percent + 50) / 100); + if (target == 0) target = 1; + uint32_t ignored = 0; + if (!Call(MethodLevel, ModeSet, target, ignored)) { + result = -2; + } else { + result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level) + : ToPercent(target); + } + } else { + result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level) : -2; + } + } + g_lock.Release(); + return result; + } + + }; +}; diff --git a/kernel/src/ACPI/WmiEcBacklight.hpp b/kernel/src/ACPI/WmiEcBacklight.hpp new file mode 100644 index 0000000..8c912fa --- /dev/null +++ b/kernel/src/ACPI/WmiEcBacklight.hpp @@ -0,0 +1,28 @@ +/* + * WmiEcBacklight.hpp + * Firmware (EC) panel backlight behind the WMI interface NVIDIA specified + * for display-mux laptops + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Hal { + namespace WmiEcBacklight { + + // On a laptop whose panel can be switched between GPUs, firmware may + // wire the backlight to the embedded controller instead of either + // GPU and expose it through WMI GUID 603E9613-EF25-4338-A3D0- + // C46177516DB7. Despite the name the interface is pure firmware: it + // works with the discrete GPU powered off. It is used only when the + // firmware itself reports the EC as the brightness source, which is + // also when GPU PWM writes stop reaching the panel. + // + // percent < 0 queries. Returns 0..100, -1 when this interface is not + // the panel's brightness source, -2 when the firmware call 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 69223bc..49dde4f 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 287 +#define MONTAUK_BUILD_NUMBER 288 diff --git a/kernel/src/Api/Graphics.hpp b/kernel/src/Api/Graphics.hpp index 11eabcf..7731807 100644 --- a/kernel/src/Api/Graphics.hpp +++ b/kernel/src/Api/Graphics.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "Syscall.hpp" @@ -129,29 +130,38 @@ 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() { + // Firmware brightness interfaces (EC via WMI, ACPI _BCM) drive the + // built-in panel, so they apply only while that panel is what is on + // screen (or no native driver knows what is): never for an external + // monitor, where they would silently dim a closed lid instead. + static bool PanelIsActiveOutput() { if (!Drivers::Graphics::IntelGPU::IsInitialized()) return true; uint8_t type = Drivers::Graphics::IntelGPU::ActiveConnectorType(); return type == DISPLAY_CONNECTOR_EDP || type == DISPLAY_CONNECTOR_LVDS; } + // Order follows who actually owns the backlight. A firmware that names + // the EC as the source has taken it away from the GPU, whose PWM then + // still accepts writes that reach nothing; so the EC interface goes + // first, then the GPU's own controls, then ACPI _BCM as a last resort. static int DisplayBrightness(int percent) { + bool panel = PanelIsActiveOutput(); + if (panel) { + int rc = Hal::WmiEcBacklight::Brightness(percent); + if (rc != -1) return rc; + } int rc = Drivers::Graphics::IntelGPU::Brightness(percent); - if (rc == -1 && AcpiBrightnessApplies()) - rc = Hal::AcpiVideo::Brightness(percent); + if (rc == -1 && panel) rc = Hal::AcpiVideo::Brightness(percent); return rc; } static int64_t Sys_DisplayInfo(DisplayInfo* out) { 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; - } + // The driver only knows its own backends; report what a set would use. + int level = DisplayBrightness(-1); + out->brightness = level >= 0 ? level : -1; + if (level >= 0) out->capabilities |= DISPLAY_CAP_BRIGHTNESS; + else out->capabilities &= ~DISPLAY_CAP_BRIGHTNESS; return Success; }