diff --git a/docs/power.md b/docs/power.md index cd3c17c..9a296f3 100644 --- a/docs/power.md +++ b/docs/power.md @@ -82,6 +82,40 @@ IA32_PM_ENABLE, HWP requests, and MSR_POWER_CTL do not survive suspend. BSP setup, and bumps the policy epoch so every AP re-applies its HWP request (and re-checks C1E) on its next tick. +### Battery (`ACPI/Battery.cpp`, `ACPI/EmbeddedController.cpp`) + +Battery state comes from the ACPI Control Method Battery +(`_HID PNP0C0A`) and, for the AC adapter, `ACPI0003`: + +- `_STA` bit 4 says whether a pack is installed. A laptop with the pack + removed still has the device, which is why "has a battery device" and + "has a battery" are separate answers. +- `_BIX` (or `_BIF` on older firmware) gives the power unit (mWh vs mAh), + the design capacity, and the last full charge capacity. +- `_BST` gives the charge/discharge state, the present rate, the remaining + capacity and the present voltage. +- `_PSR` on the adapter says whether it is plugged in. + +Percentage is remaining / last-full, clamped to 100 and falling back to the +design capacity when firmware reports last-full as zero. Results are cached +for two seconds so that a panel repaint does not turn into EC traffic. + +Almost all of this reaches the CPU through the **embedded controller**. +`Hal::EmbeddedController` finds the EC's command/data port pair from the +ECDT table when one exists, and otherwise from `_CRS` on the `PNP0C09` +device, then drives the polled read/write protocol (`0x80`/`0x81` with +OBF/IBF handshaking). It is wired into the AML interpreter as the +`EmbeddedControl` region space, so any `OperationRegion` in the DSDT that +targets the EC now works -- battery methods being the first user. + +Supporting `_BST` also needed real package element storage in the AML +interpreter. Firmware overwhelmingly builds its result package with +`Store(x, Index(BST0, n))`, which requires `Index()` to name a writable +element rather than yield a value. `AML::PackageStore` provides that: a +persistent region for packages declared with `Name()`, and a transient ring +rewound at each top-level evaluation for packages built inside a method, so +polling `_BST` cannot exhaust it. + ## Diagnostics `SYS_POWERINFO` (149) fills a `PowerInfo` struct: package temperature, @@ -90,7 +124,15 @@ CPUID base/turbo frequency, the measured average active frequency (APERF/MPERF over the 500 ms governor window), and the AP idle MWAIT hint. Returns -1 when the CPU exposes no power features (e.g. under QEMU). -The `power` program (`programs/src/power`) prints it: +`SYS_BATTERYINFO` (188) fills a `BatteryInfo` struct: percentage, charge +state, AC presence, capacities, present rate, voltage and an estimated time +remaining. It returns -1 on a machine with no battery device at all, which +is how the desktop panel decides not to draw an indicator. Like +`SYS_POWERINFO` it is ungated read-only telemetry: the panel indicator has +to work in an unprivileged session, which holds no device capability to +spend on it. + +The `power` program (`programs/src/power`) prints both: ``` power one-shot snapshot @@ -104,8 +146,17 @@ Boot log lines to look for (Kernel Log app): [CpuPower] HWP enabled: perf range 4-54, EPP=0x80 [CpuPower] Thermal governor armed: TjMax=100C, passive=85C, hot=94C, ... [CpuIdle] AP deep idle: MWAIT hint 0x20 +[EC] Embedded Controller at cmd=66 data=62 +[Battery] ACPI battery at \_SB.PCI0.LPCB.EC0.BAT0 (AC adapter present) ``` +The desktop panel shows a battery icon left of the Wi-Fi and network icons, +using the icon pack's `battery-level-N[-charging]-symbolic` glyphs (one per +10% step, reloaded only when the step changes). Clicking it opens a popout +with the percentage in numbers, the charge state, time remaining and raw +capacity. The icon is absent entirely when `SYS_BATTERYINFO` fails, so a +desktop machine shows nothing there. + ## Tuning The governor constants live at the top of `Hal/CpuPower.cpp` diff --git a/kernel/src/ACPI/AML/AmlInterpreter.cpp b/kernel/src/ACPI/AML/AmlInterpreter.cpp index aeb904f..4641c6d 100644 --- a/kernel/src/ACPI/AML/AmlInterpreter.cpp +++ b/kernel/src/ACPI/AML/AmlInterpreter.cpp @@ -6,10 +6,13 @@ #include "AmlInterpreter.hpp" #include +#include #include #include #include #include +#include +#include #include using namespace Kt; @@ -41,13 +44,78 @@ namespace Hal { return IsLeadNameChar(c) || (c >= '0' && c <= '9'); } + // The interpreter runs on whatever stack its caller has -- the boot + // stack early on, a thread's 16 KiB kernel stack once userspace is up. + // m_stackFloor is anchored at the top-level entry; a frame below it has + // spent the budget and must unwind rather than keep recursing. + bool Interpreter::HaveStackHeadroom(const void* frame) const { + if (m_stackFloor == 0) return true; // no evaluation in progress + if ((uint64_t)frame > m_stackFloor) return true; + + // Say so once. A method that quietly gives up here returns a + // default value that looks like a hardware answer -- a battery + // reporting itself absent, say -- and nothing upstream can tell + // the difference. + static bool warned = false; + if (!warned) { + warned = true; + KernelLogStream(WARNING, "AML") + << "Evaluation stopped: spent the " << base::dec + << (uint64_t)(StackBudget / 1024) << " KiB stack allowance"; + } + return false; + } + + void Interpreter::ReportDepthLimit() { + static bool warned = false; + if (!warned) { + warned = true; + KernelLogStream(WARNING, "AML") << "Evaluation stopped: nesting past " + << base::dec << (uint64_t)MaxExprDepth << " levels"; + } + } + + // True when [address, address+bytes) is inside the contiguous HHDM + // mapping and so safe to touch at phys+hhdmBase. + static bool HhdmReadable(uint64_t address, uint32_t bytes) { + uint64_t limit = Memory::VMM::Paging::HhdmLimit(); + if (limit == 0) return false; + if (address >= limit) return false; + return (limit - address) >= bytes; + } + // ============================================================================ // Constructor // ============================================================================ Interpreter::Interpreter() - : m_dsdt(nullptr), m_dsdtLength(0), m_initialized(false) {} + : m_dsdt(nullptr), m_dsdtLength(0), m_initialized(false), + m_ctxPool(nullptr), m_ctxInUse(0), m_evalNesting(0), m_stackFloor(0), + m_traceBudget(0) {} + + // ============================================================================ + + // Execution context pool + + // ============================================================================ + Interpreter::ExecContext* Interpreter::AcquireContext() { + if (!m_ctxPool) { + m_ctxPool = (ExecContext*)Memory::g_heap->Request( + sizeof(ExecContext) * (MaxCallDepth + 1)); + if (!m_ctxPool) return nullptr; + } + + if (m_ctxInUse > MaxCallDepth) return nullptr; + + ExecContext* ctx = &m_ctxPool[m_ctxInUse++]; + *ctx = ExecContext{}; + return ctx; + } + + void Interpreter::ReleaseContext() { + if (m_ctxInUse > 0) m_ctxInUse--; + } // ============================================================================ @@ -148,7 +216,9 @@ namespace Hal { // Reads a NameString (which may include root prefix, parent prefixes, // dual/multi name prefix) and produces an absolute path. int Interpreter::ReadNameString(const uint8_t* aml, uint32_t* pos, int32_t scopeNode, - char* outPath, int maxLen) { + char* outPath, int maxLen, + char* outRelative, int relLen, + bool* outSearchable) { int pathPos = 0; bool isAbsolute = false; int parentPrefixCount = 0; @@ -220,6 +290,22 @@ namespace Hal { } outPath[pathPos] = '\0'; + + // Hand back the name as written, for callers that must apply the + // ACPI search rule when the rebased path does not exist. + if (outSearchable) + *outSearchable = !isAbsolute && parentPrefixCount == 0 && segCount > 0; + + if (outRelative && relLen > 0) { + int rp = 0; + for (int i = 0; i < segCount; i++) { + if (i > 0 && rp < relLen - 1) outRelative[rp++] = '.'; + for (int j = 0; j < 4 && rp < relLen - 1; j++) + outRelative[rp++] = segments[i][j]; + } + outRelative[rp] = '\0'; + } + return pathPos; } @@ -582,13 +668,17 @@ namespace Hal { uint32_t fieldEnd = pkgStart + pkgLen; if (fieldEnd > endOffset) fieldEnd = endOffset; - // Region name + // Region name. A Field almost always names a region + // declared in an enclosing scope, so the search rule is not + // optional here. char regionPath[256]; - ReadNameString(aml, pos, scopeNode, regionPath, 256); + char regionRel[64]; + bool regionSearchable = false; + ReadNameString(aml, pos, scopeNode, regionPath, 256, + regionRel, sizeof(regionRel), ®ionSearchable); int32_t regionNode = m_ns.FindNode(regionPath); - if (regionNode < 0) { - regionNode = m_ns.ResolveName(regionPath, scopeNode); - } + if (regionNode < 0 && regionSearchable) + regionNode = m_ns.SearchUpward(regionRel, scopeNode); // Field flags uint8_t fieldFlags = aml[*pos]; @@ -833,34 +923,74 @@ namespace Hal { auto* nsNode = m_ns.GetNode(node); if (!nsNode || nsNode->Obj.Type != ObjectType::Method) return false; - ExecContext ctx{}; - ctx.Aml = m_dsdt; - ctx.AmlBase = nsNode->Obj.Method.AmlOffset; - ctx.AmlLength = nsNode->Obj.Method.AmlLength; - ctx.ScopeNode = node; - ctx.Returned = false; - ctx.Broken = false; - ctx.Depth = 0; + // Packages built inside the method come out of the transient ring. + // Rewinding it here bounds the store to one evaluation's worth, + // which is what makes repeated polling (_BST every few seconds) + // safe. The caller must read the returned package before the next + // evaluation, which every caller in the kernel does. + // Evaluation is serialized: the execution contexts come from a + // shared pool, and the battery indicator now polls from a user + // thread while other subsystems may evaluate AML of their own. + bool topLevel = false; + m_evalLock.Acquire(); + if (m_evalNesting == 0) { + topLevel = true; + PackageStore::ResetTransient(); + + // Anchor the stack budget to this frame. Everything below + // measures against it, so the same limit works whether we were + // called on the large boot stack or a thread's 16 KiB one. + char stackMarker = 0; + m_stackFloor = (uint64_t)&stackMarker - StackBudget; + } + m_evalNesting++; + + ExecContext* ctx = AcquireContext(); + if (!ctx) { + m_evalNesting--; + m_evalLock.Release(); + return false; + } + + ctx->Aml = m_dsdt; + ctx->AmlBase = nsNode->Obj.Method.AmlOffset; + ctx->AmlLength = nsNode->Obj.Method.AmlLength; + ctx->ScopeNode = node; + ctx->Returned = false; + ctx->Broken = false; + ctx->Depth = 0; + ctx->ExprDepth = 0; // Set up arguments for (int i = 0; i < argCount && i < MaxMethodArgs; i++) { - ctx.Args[i] = args[i]; + ctx->Args[i] = args[i]; } // Initialize locals to zero for (int i = 0; i < MaxMethodLocals; i++) { - ctx.Locals[i].Type = ObjectType::Integer; - ctx.Locals[i].Integer = 0; + ctx->Locals[i].Type = ObjectType::Integer; + ctx->Locals[i].Integer = 0; } - bool ok = ExecuteBlock(ctx, ctx.AmlBase, ctx.AmlBase + ctx.AmlLength); + bool ok = ExecuteBlock(*ctx, ctx->AmlBase, ctx->AmlBase + ctx->AmlLength); - result = ctx.ReturnValue; + result = ctx->ReturnValue; if (result.Type == ObjectType::None) { result.Type = ObjectType::Integer; result.Integer = 0; } + ReleaseContext(); + m_evalNesting--; + + // The floor refers to a frame on this call's stack. Leaving it set + // would have the next evaluation -- possibly on a different thread's + // stack entirely -- compare against an address that no longer means + // anything, and bail out for no reason. + if (topLevel) m_stackFloor = 0; + + m_evalLock.Release(); + return ok; } @@ -870,6 +1000,12 @@ namespace Hal { // ============================================================================ bool Interpreter::ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset) { + char stackMarker = 0; + if (ctx.ExprDepth >= MaxExprDepth) ReportDepthLimit(); + if (ctx.ExprDepth >= MaxExprDepth || !HaveStackHeadroom(&stackMarker)) + return false; + DepthGuard depthGuard(ctx.ExprDepth); + uint32_t pos = offset; while (pos < endOffset && !ctx.Returned && !ctx.Broken) { @@ -890,6 +1026,19 @@ namespace Hal { uint8_t op = ctx.Aml[*pos]; + if (m_traceBudget > 0) { + m_traceBudget--; + // Offsets are absolute into the DSDT, so they line up with a + // disassembly of the same table. + if (op == ExtOpPrefix && *pos + 1 < endOffset) { + KernelLogStream(INFO, "AML") << " @" << base::hex << (uint64_t)*pos + << " ext op " << (uint64_t)ctx.Aml[*pos + 1]; + } else { + KernelLogStream(INFO, "AML") << " @" << base::hex << (uint64_t)*pos + << " op " << (uint64_t)op; + } + } + // Return if (op == ReturnOp) { (*pos)++; @@ -988,8 +1137,11 @@ namespace Hal { int32_t targetNode = -1; bool isLocal = false, isArg = false; int localIdx = 0, argIdx = 0; - EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx); - StoreToTarget(ctx, value, targetNode, isLocal, localIdx, isArg, argIdx); + int32_t pkgStore = -1; int pkgElement = 0; + EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx, + pkgStore, pkgElement); + StoreToTarget(ctx, value, targetNode, isLocal, localIdx, isArg, argIdx, + pkgStore, pkgElement); return true; } @@ -999,7 +1151,9 @@ namespace Hal { int32_t targetNode = -1; bool isLocal = false, isArg = false; int localIdx = 0, argIdx = 0; - EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx); + int32_t pkgStore = -1; int pkgElement = 0; + EvalTarget(ctx, pos, targetNode, isLocal, localIdx, isArg, argIdx, + pkgStore, pkgElement); Object current{}; if (isLocal) current = ctx.Locals[localIdx]; @@ -1012,7 +1166,8 @@ namespace Hal { if (current.Type == ObjectType::Integer) { current.Integer += (op == IncrementOp) ? 1 : -1; } - StoreToTarget(ctx, current, targetNode, isLocal, localIdx, isArg, argIdx); + StoreToTarget(ctx, current, targetNode, isLocal, localIdx, isArg, argIdx, + pkgStore, pkgElement); return true; } @@ -1083,6 +1238,16 @@ namespace Hal { // Evaluate an AML term that produces a value. bool Interpreter::EvalTerm(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result) { + char stackMarker = 0; + if (ctx.ExprDepth >= MaxExprDepth) ReportDepthLimit(); + if (ctx.ExprDepth >= MaxExprDepth || !HaveStackHeadroom(&stackMarker)) { + result = Object{}; + result.Type = ObjectType::Integer; + result.Integer = 0; + return false; + } + DepthGuard depthGuard(ctx.ExprDepth); + if (*pos >= endOffset) return false; uint8_t op = ctx.Aml[*pos]; @@ -1136,6 +1301,14 @@ namespace Hal { return true; } + // DerefOf. Index() already yields the element's value here, so a + // dereference of one is just the value again; anything else passes + // through unchanged. + if (op == DerefOfOp) { + (*pos)++; + return EvalTerm(ctx, pos, endOffset, result); + } + // Package if (op == PackageOp) { (*pos)++; @@ -1144,13 +1317,38 @@ namespace Hal { uint32_t pkgEnd = pkgStart + pkgLen; if (pkgEnd > endOffset) pkgEnd = endOffset; - // Store raw package as buffer for now + // Retain the raw bytes (constant elements are read straight + // out of them) and, in parallel, evaluate each element into the + // package store so computed ones -- Package(){Local0, Local1}, + // the shape _BST and friends are built with -- survive. result.Type = ObjectType::Package; uint32_t dataLen = pkgEnd - *pos; if (dataLen > MaxBufferLen) dataLen = MaxBufferLen; result.Buffer.Length = dataLen; memcpy(result.Buffer.Data, &ctx.Aml[*pos], dataLen); + uint32_t elemPos = *pos; + int count = (elemPos < pkgEnd) ? (int)ctx.Aml[elemPos] : 0; + elemPos++; + if (count > MaxPackageElements) count = MaxPackageElements; + + if (count > 0) { + int32_t store = PackageStore::AllocTransient(count); + if (store > 0) { + for (int i = 0; i < count && elemPos < pkgEnd; i++) { + Object elem{}; + uint32_t before = elemPos; + if (!EvalTerm(ctx, &elemPos, pkgEnd, elem)) break; + // An element that consumed nothing would spin here + // forever on a stream we decoded wrongly. + if (elemPos <= before) break; + Object* slot = PackageStore::Element(store, i); + if (slot) *slot = elem; + } + result.PkgIndex = store; + } + } + *pos = pkgEnd; return true; } @@ -1259,14 +1457,16 @@ namespace Hal { int32_t tn = -1; bool il = false, ia = false; int li = 0, ai = 0; - EvalTarget(ctx, pos, tn, il, li, ia, ai); - StoreToTarget(ctx, result, tn, il, li, ia, ai); + int32_t ps = -1; int pe = 0; + EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe); + StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe); } else if (IsLeadNameChar(next) || next == '\\' || next == '^') { int32_t tn = -1; bool il = false, ia = false; int li = 0, ai = 0; - EvalTarget(ctx, pos, tn, il, li, ia, ai); - StoreToTarget(ctx, result, tn, il, li, ia, ai); + int32_t ps = -1; int pe = 0; + EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe); + StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe); } // ZeroOp as "no target" — skip else if (next == ZeroOp) { @@ -1289,8 +1489,9 @@ namespace Hal { int32_t tn = -1; bool il = false, ia = false; int li = 0, ai = 0; - EvalTarget(ctx, pos, tn, il, li, ia, ai); - StoreToTarget(ctx, result, tn, il, li, ia, ai); + int32_t ps = -1; int pe = 0; + EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe); + StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe); } else if (*pos < endOffset && ctx.Aml[*pos] == ZeroOp) { (*pos)++; } @@ -1321,16 +1522,18 @@ namespace Hal { int32_t tn = -1; bool il = false, ia = false; int li = 0, ai = 0; - EvalTarget(ctx, pos, tn, il, li, ia, ai); - StoreToTarget(ctx, remainder, tn, il, li, ia, ai); + int32_t ps = -1; int pe = 0; + EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe); + StoreToTarget(ctx, remainder, tn, il, li, ia, ai, ps, pe); } // Result target if (*pos < endOffset) { int32_t tn = -1; bool il = false, ia = false; int li = 0, ai = 0; - EvalTarget(ctx, pos, tn, il, li, ia, ai); - StoreToTarget(ctx, result, tn, il, li, ia, ai); + int32_t ps = -1; int pe = 0; + EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe); + StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe); } return true; } @@ -1345,6 +1548,8 @@ namespace Hal { result.Integer = obj.String.Length; else if (obj.Type == ObjectType::Buffer) result.Integer = obj.Buffer.Length; + else if (obj.Type == ObjectType::Package) + result.Integer = (uint64_t)GetPackageCount(obj); else result.Integer = 0; return true; @@ -1357,12 +1562,23 @@ namespace Hal { EvalTerm(ctx, pos, endOffset, source); EvalTerm(ctx, pos, endOffset, index); - result.Type = ObjectType::Integer; uint64_t idx = index.Integer; - if (source.Type == ObjectType::Buffer && idx < source.Buffer.Length) { + if (source.Type == ObjectType::Package) { + // ACPI defines Index() as producing a reference; we hand + // back the element's value instead, which is what every + // operand context here goes on to use. Writes through + // Index() are handled separately, in EvalTarget. + if (!GetPackageElement(source, (int)idx, result)) { + result = Object{}; + result.Type = ObjectType::Integer; + result.Integer = 0; + } + } else if (source.Type == ObjectType::Buffer && idx < source.Buffer.Length) { + result.Type = ObjectType::Integer; result.Integer = source.Buffer.Data[idx]; } else { + result.Type = ObjectType::Integer; result.Integer = 0; } @@ -1371,8 +1587,9 @@ namespace Hal { int32_t tn = -1; bool il = false, ia = false; int li = 0, ai = 0; - EvalTarget(ctx, pos, tn, il, li, ia, ai); - StoreToTarget(ctx, result, tn, il, li, ia, ai); + int32_t ps = -1; int pe = 0; + EvalTarget(ctx, pos, tn, il, li, ia, ai, ps, pe); + StoreToTarget(ctx, result, tn, il, li, ia, ai, ps, pe); } else if (*pos < endOffset && ctx.Aml[*pos] == ZeroOp) { (*pos)++; } @@ -1405,12 +1622,14 @@ namespace Hal { if (IsLeadNameChar(op) || op == '\\' || op == '^' || op == DualNamePrefix || op == MultiNamePrefix) { char path[256]; - ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256); + char relative[64]; + bool searchable = false; + ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256, + relative, sizeof(relative), &searchable); int32_t node = m_ns.FindNode(path); - if (node < 0) { - node = m_ns.ResolveName(path, ctx.ScopeNode); - } + if (node < 0 && searchable) + node = m_ns.SearchUpward(relative, ctx.ScopeNode); if (node >= 0) { auto* nsNode = m_ns.GetNode(node); @@ -1430,7 +1649,13 @@ namespace Hal { return true; } - ExecContext subCtx{}; + ExecContext* sub = AcquireContext(); + if (!sub) { + result.Type = ObjectType::Integer; + result.Integer = 0; + return true; + } + ExecContext& subCtx = *sub; subCtx.Aml = ctx.Aml; subCtx.AmlBase = nsNode->Obj.Method.AmlOffset; subCtx.AmlLength = nsNode->Obj.Method.AmlLength; @@ -1438,6 +1663,7 @@ namespace Hal { subCtx.Returned = false; subCtx.Broken = false; subCtx.Depth = ctx.Depth + 1; + subCtx.ExprDepth = ctx.ExprDepth; for (int i = 0; i < argc; i++) subCtx.Args[i] = args[i]; for (int i = 0; i < MaxMethodLocals; i++) { @@ -1451,6 +1677,7 @@ namespace Hal { result.Type = ObjectType::Integer; result.Integer = 0; } + ReleaseContext(); } else if (nsNode->Obj.Type == ObjectType::Field) { uint64_t val = 0; ReadField(node, val); @@ -1483,12 +1710,25 @@ namespace Hal { // ============================================================================ bool Interpreter::EvalTarget(ExecContext& ctx, uint32_t* pos, int32_t& nodeIndex, bool& isLocal, int& localIdx, - bool& isArg, int& argIdx) { + bool& isArg, int& argIdx, + int32_t& pkgStore, int& pkgElement) { nodeIndex = -1; + char stackMarker = 0; + if (ctx.ExprDepth >= MaxExprDepth) ReportDepthLimit(); + if (ctx.ExprDepth >= MaxExprDepth || !HaveStackHeadroom(&stackMarker)) { + isLocal = false; isArg = false; + localIdx = 0; argIdx = 0; + pkgStore = -1; pkgElement = 0; + return false; + } + DepthGuard depthGuard(ctx.ExprDepth); + isLocal = false; isArg = false; localIdx = 0; argIdx = 0; + pkgStore = -1; + pkgElement = 0; if (*pos >= ctx.AmlBase + ctx.AmlLength) return false; @@ -1520,10 +1760,73 @@ namespace Hal { if (IsLeadNameChar(op) || op == '\\' || op == '^' || op == DualNamePrefix || op == MultiNamePrefix) { char path[256]; - ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256); + char relative[64]; + bool searchable = false; + ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256, + relative, sizeof(relative), &searchable); nodeIndex = m_ns.FindNode(path); - if (nodeIndex < 0) - nodeIndex = m_ns.ResolveName(path, ctx.ScopeNode); + if (nodeIndex < 0 && searchable) + nodeIndex = m_ns.SearchUpward(relative, ctx.ScopeNode); + return true; + } + + // Index(source, n) names a package element. This is how firmware + // fills a result package -- Store(x, Index(BST0, 1)) -- so the + // element has to be addressable, not just readable. + if (op == IndexOp) { + (*pos)++; + + // Resolve the source by name where we can, so the write lands + // in the namespace object rather than in a copy of it. + int32_t srcNode = -1; + uint8_t srcOp = (*pos < ctx.AmlBase + ctx.AmlLength) ? ctx.Aml[*pos] : 0; + if (IsLeadNameChar(srcOp) || srcOp == '\\' || srcOp == '^' || + srcOp == DualNamePrefix || srcOp == MultiNamePrefix) { + char path[256]; + char relative[64]; + bool searchable = false; + ReadNameString(ctx.Aml, pos, ctx.ScopeNode, path, 256, + relative, sizeof(relative), &searchable); + srcNode = m_ns.FindNode(path); + if (srcNode < 0 && searchable) + srcNode = m_ns.SearchUpward(relative, ctx.ScopeNode); + } else { + // Locals and arguments can hold a package too. + Object src{}; + EvalTerm(ctx, pos, ctx.AmlBase + ctx.AmlLength, src); + if (src.Type == ObjectType::Package && src.PkgIndex > 0) + pkgStore = src.PkgIndex; + } + + Object idx{}; + EvalTerm(ctx, pos, ctx.AmlBase + ctx.AmlLength, idx); + pkgElement = (int)idx.Integer; + + if (srcNode >= 0) { + auto* srcObj = m_ns.GetNode(srcNode); + if (srcObj && srcObj->Obj.Type == ObjectType::Package) + pkgStore = MaterializeNodePackage(srcNode); + } + + // Index() always carries a target operand of its own -- ZeroOp + // when there is none. It has to be consumed either way, or the + // rest of the method decodes off by however long it was. + if (*pos < ctx.AmlBase + ctx.AmlLength) { + int32_t innerNode = -1; + bool innerLocal = false, innerArg = false; + int innerLocalIdx = 0, innerArgIdx = 0; + int32_t innerStore = -1; int innerElement = 0; + EvalTarget(ctx, pos, innerNode, innerLocal, innerLocalIdx, + innerArg, innerArgIdx, innerStore, innerElement); + } + + if (pkgStore <= 0) { + // Not a package we can write through (a buffer, or an + // unresolvable source): discard the store rather than + // corrupting an unrelated object. + pkgStore = -1; + nodeIndex = -1; + } return true; } @@ -1539,7 +1842,14 @@ namespace Hal { // ============================================================================ void Interpreter::StoreToTarget(ExecContext& ctx, const Object& value, int32_t nodeIndex, bool isLocal, int localIdx, - bool isArg, int argIdx) { + bool isArg, int argIdx, + int32_t pkgStore, int pkgElement) { + if (pkgStore > 0) { + Object* slot = PackageStore::Element(pkgStore, pkgElement); + if (slot) *slot = value; + return; + } + if (isLocal && localIdx >= 0 && localIdx < MaxMethodLocals) { ctx.Locals[localIdx] = value; } else if (isArg && argIdx >= 0 && argIdx < MaxMethodArgs) { @@ -1558,6 +1868,171 @@ namespace Hal { // ============================================================================ + // Package elements + + // ============================================================================ + bool Interpreter::DecodeConstElement(const uint8_t* data, uint32_t length, + uint32_t* pos, Object& out) { + out = Object{}; + if (*pos >= length) return false; + + uint8_t op = data[(*pos)++]; + + switch (op) { + case ZeroOp: + out.Type = ObjectType::Integer; + out.Integer = 0; + return true; + case OneOp: + out.Type = ObjectType::Integer; + out.Integer = 1; + return true; + case OnesOp: + out.Type = ObjectType::Integer; + out.Integer = ~0ULL; + return true; + default: + break; + } + + // Fixed-width integer prefixes + int width = 0; + if (op == BytePrefix) width = 1; + else if (op == WordPrefix) width = 2; + else if (op == DWordPrefix) width = 4; + else if (op == QWordPrefix) width = 8; + + if (width > 0) { + if (*pos + (uint32_t)width > length) return false; + uint64_t value = 0; + for (int i = 0; i < width; i++) + value |= (uint64_t)data[*pos + i] << (i * 8); + *pos += (uint32_t)width; + out.Type = ObjectType::Integer; + out.Integer = value; + return true; + } + + if (op == StringPrefix) { + out.Type = ObjectType::String; + uint16_t n = 0; + while (*pos < length && data[*pos] != 0) { + if (n < MaxStringLen - 1) + out.String.Data[n++] = (char)data[*pos]; + (*pos)++; + } + if (*pos < length) (*pos)++; // trailing NUL + out.String.Data[n] = '\0'; + out.String.Length = n; + return true; + } + + if (op == BufferOp || op == PackageOp || op == VarPackageOp) { + uint32_t start = *pos; + uint32_t pkgLen = DecodePkgLength(data, pos); + uint32_t end = start + pkgLen; + if (end > length) end = length; + if (end < *pos) return false; + + uint32_t dataLen = end - *pos; + if (dataLen > MaxBufferLen) dataLen = MaxBufferLen; + + out.Type = (op == BufferOp) ? ObjectType::Buffer : ObjectType::Package; + out.Buffer.Length = dataLen; + memcpy(out.Buffer.Data, &data[*pos], dataLen); + *pos = end; + return true; + } + + // A name reference, local, or anything else we cannot resolve from + // the bytes alone. Skip it and hand back zero. + if (IsLeadNameChar(op) || op == '\\' || op == '^' || + op == DualNamePrefix || op == MultiNamePrefix) { + (*pos)--; + uint32_t p = *pos; + // Walk the NameString by hand: prefixes, then 4-char segments. + while (p < length && (data[p] == '\\' || data[p] == '^')) p++; + if (p < length && data[p] == DualNamePrefix) { + p += 1 + 8; + } else if (p < length && data[p] == MultiNamePrefix) { + uint8_t segs = (p + 1 < length) ? data[p + 1] : 0; + p += 2 + (uint32_t)segs * 4; + } else if (p < length && data[p] == 0x00) { + p += 1; // NullName + } else { + p += 4; + } + *pos = (p > length) ? length : p; + } + + out.Type = ObjectType::Integer; + out.Integer = 0; + return true; + } + + int Interpreter::GetPackageCount(const Object& package) { + if (package.Type != ObjectType::Package) return 0; + if (package.PkgIndex > 0) + return PackageStore::Count(package.PkgIndex); + // Raw form: the first byte of the retained AML is NumElements. + if (package.Buffer.Length < 1) return 0; + return (int)package.Buffer.Data[0]; + } + + bool Interpreter::GetPackageElement(const Object& package, int index, Object& result) { + result = Object{}; + if (package.Type != ObjectType::Package || index < 0) return false; + + if (package.PkgIndex > 0) { + Object* elem = PackageStore::Element(package.PkgIndex, index); + if (!elem) return false; + result = *elem; + return true; + } + + uint32_t length = package.Buffer.Length; + if (length < 1) return false; + if (index >= (int)package.Buffer.Data[0]) return false; + + uint32_t pos = 1; // past NumElements + for (int i = 0; i <= index; i++) { + if (!DecodeConstElement(package.Buffer.Data, length, &pos, result)) + return false; + } + return true; + } + + int32_t Interpreter::MaterializeNodePackage(int32_t nodeIndex) { + auto* node = m_ns.GetNode(nodeIndex); + if (!node || node->Obj.Type != ObjectType::Package) return -1; + if (node->Obj.PkgIndex > 0) return node->Obj.PkgIndex; + + int count = GetPackageCount(node->Obj); + if (count <= 0) return -1; + if (count > MaxPackageElements) count = MaxPackageElements; + + int32_t store = PackageStore::AllocPersistent(count); + if (store < 0) return -1; + + // Seed the elements from the constant bytes the package was + // declared with, so a package that is only partly written through + // Index() keeps its declared values everywhere else. + uint32_t pos = 1; + for (int i = 0; i < count; i++) { + Object elem{}; + if (!DecodeConstElement(node->Obj.Buffer.Data, node->Obj.Buffer.Length, + &pos, elem)) + break; + Object* slot = PackageStore::Element(store, i); + if (slot) *slot = elem; + } + + node->Obj.PkgIndex = store; + return store; + } + + // ============================================================================ + // ReadField // ============================================================================ @@ -1578,7 +2053,19 @@ namespace Hal { uint64_t byteAddr = regBase + (bitOff / 8); uint32_t bitShift = bitOff % 8; - return ReadRegion(regionNode->Obj.Region.Space, byteAddr, bitLen + bitShift, value); + uint64_t raw = 0; + if (!ReadRegion(regionNode->Obj.Region.Space, byteAddr, bitLen + bitShift, raw)) + return false; + + // ReadRegion hands back whole access units starting at byteAddr; + // narrow that to the field's own bits so sub-byte flags (common in + // EC and PM register layouts) don't carry their neighbours along. + raw >>= bitShift; + if (bitLen < 64) + raw &= (1ULL << bitLen) - 1; + + value = raw; + return true; } // ============================================================================ @@ -1639,6 +2126,13 @@ namespace Hal { } if (space == RegionSpace::SystemMemory) { + // The offset comes from firmware AML and may be nonsense: a + // region we mis-decoded, or one whose base is computed by code + // we do not fully execute. Dereferencing that through the HHDM + // is how a bad table takes the kernel down with a #GP, so the + // address has to be inside what Init() actually mapped. + if (!HhdmReadable(address, accessBytes)) return false; + volatile uint8_t* mmio = (volatile uint8_t*)Memory::HHDM(address); if (accessBytes <= 1) value = *mmio; @@ -1651,6 +2145,19 @@ namespace Hal { return true; } + if (space == RegionSpace::EmbeddedControl) { + // The EC address space is a flat 256-byte array reached one + // byte at a time over the command/data port pair, so a wider + // field is assembled little-endian from consecutive reads. + for (uint32_t i = 0; i < accessBytes; i++) { + uint8_t byte = 0; + if (!EmbeddedController::Read((uint8_t)(address + i), byte)) + return false; + value |= (uint64_t)byte << (i * 8); + } + return true; + } + if (space == RegionSpace::PciConfig) { // PCI config space access -- not yet implemented return false; @@ -1681,6 +2188,8 @@ namespace Hal { } if (space == RegionSpace::SystemMemory) { + if (!HhdmReadable(address, accessBytes)) return false; + volatile uint8_t* mmio = (volatile uint8_t*)Memory::HHDM(address); if (accessBytes <= 1) *mmio = (uint8_t)value; @@ -1693,6 +2202,15 @@ namespace Hal { return true; } + if (space == RegionSpace::EmbeddedControl) { + for (uint32_t i = 0; i < accessBytes; i++) { + if (!EmbeddedController::Write((uint8_t)(address + i), + (uint8_t)(value >> (i * 8)))) + return false; + } + return true; + } + return false; } diff --git a/kernel/src/ACPI/AML/AmlInterpreter.hpp b/kernel/src/ACPI/AML/AmlInterpreter.hpp index b52966f..960fce9 100644 --- a/kernel/src/ACPI/AML/AmlInterpreter.hpp +++ b/kernel/src/ACPI/AML/AmlInterpreter.hpp @@ -7,6 +7,7 @@ #pragma once #include "AmlNamespace.hpp" +#include #include namespace Hal { @@ -105,6 +106,24 @@ namespace Hal { static constexpr int MaxCallDepth = 16; static constexpr int MaxLoopIterations = 1024; + // Backstop cap on evaluation nesting. This counter is shared by + // expression evaluation AND block execution, and block nesting is + // legitimately deep -- every If/Else/While body is another level, so a + // long firmware method reaches well past a handful. The real protection + // against running off the stack is HaveStackHeadroom(), which measures + // actual usage; this only catches a decode that recurses without + // consuming input. + static constexpr int MaxExprDepth = 48; + + // How much stack the interpreter may consume before it gives up. Note + // this is an ALLOWANCE, not a reserve: frames here are expensive (a + // name-resolving EvalTerm frame carries a 256-byte path buffer plus + // 272-byte Object temporaries, so ~800 bytes each), and a real + // firmware _BIF nests far enough to need tens of them. Sized against + // Sched::StackPages -- keep well under it, since the syscall path below + // has already spent some of that stack. + static constexpr uint64_t StackBudget = 32768; + // ============================================================================ // Interpreter @@ -138,6 +157,31 @@ namespace Hal { // Check if the interpreter has been initialized bool IsInitialized() const { return m_initialized; } + // Log every opcode executed, with its offset, for the next + // `budget` opcodes. The only way to see where a method that runs to + // completion without returning a value actually goes. + void SetTrace(int budget) { m_traceBudget = budget; } + + // ============================================================================ + + // Packages + + // ============================================================================ + // Read one element of a Package object. Works for both a package + // whose elements were computed at run time (materialized in the + // package store) and one that is still just the constant AML bytes + // it was declared with. Returns false when the object is not a + // package or the index is out of range. + bool GetPackageElement(const Object& package, int index, Object& result); + + // Number of elements a Package declares. + int GetPackageCount(const Object& package); + + // Give a package stored in a namespace node real per-element + // storage so Index() can be written through. Returns the store + // index, or -1 if the node does not hold a package. + int32_t MaterializeNodePackage(int32_t nodeIndex); + private: // ============================================================================ // Parsing (table load) @@ -158,8 +202,17 @@ namespace Hal { // ============================================================================ // Read a NameString from AML and produce an absolute path. // Advances *pos past the name. + // Produces the absolute path (rebased onto scopeNode for a + // relative name). outRelative, when given, receives the name as it + // actually appeared in the AML, and *outSearchable says whether the + // ACPI search rule applies to it -- true only when the name carried + // no root or parent prefix. Resolution must fall back to + // Namespace::SearchUpward() in that case: the absolute path is a + // guess at one scope, and the object usually lives in a parent. int ReadNameString(const uint8_t* aml, uint32_t* pos, int32_t scopeNode, - char* outPath, int maxLen); + char* outPath, int maxLen, + char* outRelative = nullptr, int relLen = 0, + bool* outSearchable = nullptr); // Read a single 4-char NameSeg from AML. Advances *pos. void ReadNameSeg(const uint8_t* aml, uint32_t* pos, char* outSeg); @@ -170,6 +223,13 @@ namespace Hal { // ============================================================================ uint32_t DecodePkgLength(const uint8_t* aml, uint32_t* pos); + + // Decode one constant package element out of raw AML bytes and + // advance *pos past it. Non-constant elements (name references, + // locals) decode to integer 0, which is what they were worth + // before the package store existed. + bool DecodeConstElement(const uint8_t* data, uint32_t length, + uint32_t* pos, Object& out); uint64_t DecodeInteger(const uint8_t* aml, uint32_t* pos); // ============================================================================ @@ -187,7 +247,16 @@ namespace Hal { Object ReturnValue; bool Returned; bool Broken; - int Depth; + int Depth; // method-call nesting + int ExprDepth; // expression/block nesting + }; + + // Increments a depth counter for the duration of a scope; pairs + // with the MaxExprDepth check at each recursive entry point. + struct DepthGuard { + int& Value; + explicit DepthGuard(int& v) : Value(v) { Value++; } + ~DepthGuard() { Value--; } }; bool ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset); @@ -199,14 +268,19 @@ namespace Hal { // Evaluate a "SuperName" target for Store operations. // Returns the node index for named targets, or handles locals/args. // If isLocal/isArg is set, localIdx/argIdx contains the index. + // A target may also be Index(package, n), which names a package + // element rather than a node, local or arg. pkgStore/pkgElement + // carry that case (pkgStore > 0 when it applies). bool EvalTarget(ExecContext& ctx, uint32_t* pos, int32_t& nodeIndex, bool& isLocal, int& localIdx, - bool& isArg, int& argIdx); + bool& isArg, int& argIdx, + int32_t& pkgStore, int& pkgElement); // Store a value to a target (node, local, or arg). void StoreToTarget(ExecContext& ctx, const Object& value, int32_t nodeIndex, bool isLocal, int localIdx, - bool isArg, int argIdx); + bool isArg, int argIdx, + int32_t pkgStore = -1, int pkgElement = 0); // ============================================================================ @@ -221,10 +295,32 @@ namespace Hal { // State // ============================================================================ + // ExecContext is ~4.4 KB (sixteen Objects, each carrying a + // 256-byte buffer inline). One per nested method call on a 16 KiB + // kernel stack overflows after three levels, so they come from a + // pool instead. The pool is only safe because evaluation is + // serialized -- see m_evalLock. + // Stack-exhaustion check; `frame` is the address of a local in + // the calling frame. + bool HaveStackHeadroom(const void* frame) const; + + // One-shot warning when the nesting backstop trips. + static void ReportDepthLimit(); + + ExecContext* AcquireContext(); + void ReleaseContext(); + Namespace m_ns; const uint8_t* m_dsdt; uint32_t m_dsdtLength; bool m_initialized; + + ExecContext* m_ctxPool; // MaxCallDepth + 1 entries, heap + int m_ctxInUse; + int m_evalNesting; // top-level evaluations in progress + uint64_t m_stackFloor; // stack address the interpreter stops at + int m_traceBudget; // opcodes left to log, 0 = off + kcp::Mutex m_evalLock; }; // ============================================================================ diff --git a/kernel/src/ACPI/AML/AmlNamespace.cpp b/kernel/src/ACPI/AML/AmlNamespace.cpp index 1e2ae6c..9c03207 100644 --- a/kernel/src/ACPI/AML/AmlNamespace.cpp +++ b/kernel/src/ACPI/AML/AmlNamespace.cpp @@ -7,10 +7,114 @@ #include "AmlNamespace.hpp" #include #include +#include +#include + +using namespace Kt; namespace Hal { namespace AML { + // ============================================================================ + + // Package element store + + // ============================================================================ + namespace PackageStore { + + struct Entry { + Object* Elements; + int Count; + }; + + static Entry s_persistent[MaxPersistent]; + static Entry s_transient[MaxTransient]; + static int s_persistentUsed = 0; + static int s_transientNext = 0; + + // Indices handed out are 1-based so that a zeroed Object (nodes are + // memset before Clear() runs, and callers zero-init freely) never + // looks like a reference to entry 0. + static constexpr int32_t TransientBase = MaxPersistent; + + static Object* AllocElements(int count) { + auto* mem = (Object*)Memory::g_heap->Request(sizeof(Object) * count); + if (!mem) return nullptr; + for (int i = 0; i < count; i++) + mem[i] = Object{}; + return mem; + } + + int32_t AllocPersistent(int count) { + if (count <= 0 || count > MaxPackageElements) return -1; + if (s_persistentUsed >= MaxPersistent) return -1; + + Entry& e = s_persistent[s_persistentUsed]; + e.Elements = AllocElements(count); + if (!e.Elements) return -1; + e.Count = count; + return (int32_t)(s_persistentUsed++) + 1; + } + + int32_t AllocTransient(int count) { + if (count <= 0 || count > MaxPackageElements) return -1; + + int slot = s_transientNext; + if (slot >= MaxTransient) { + // The ring is only rewound between top-level evaluations, so + // a method that builds more packages than this reuses the + // oldest slots rather than failing outright. + slot = 0; + s_transientNext = 0; + } + s_transientNext++; + + Entry& e = s_transient[slot]; + // Element arrays are sized on first use and reused afterwards; + // a larger request reallocates. + if (e.Elements == nullptr || e.Count < count) { + Object* mem = AllocElements(count); + if (!mem) return -1; + if (e.Elements) Memory::g_heap->Free(e.Elements); + e.Elements = mem; + } else { + for (int i = 0; i < count; i++) + e.Elements[i] = Object{}; + } + e.Count = count; + return TransientBase + slot + 1; + } + + void ResetTransient() { + s_transientNext = 0; + } + + static Entry* Lookup(int32_t index) { + if (index <= 0) return nullptr; + int32_t i = index - 1; + + if (i < MaxPersistent) { + if (i >= s_persistentUsed) return nullptr; + return s_persistent[i].Elements ? &s_persistent[i] : nullptr; + } + + i -= TransientBase; + if (i < 0 || i >= MaxTransient) return nullptr; + return s_transient[i].Elements ? &s_transient[i] : nullptr; + } + + Object* Element(int32_t index, int element) { + Entry* e = Lookup(index); + if (!e || element < 0 || element >= e->Count) return nullptr; + return &e->Elements[element]; + } + + int Count(int32_t index) { + Entry* e = Lookup(index); + return e ? e->Count : 0; + } + }; + Namespace::Namespace() : m_chunkCount(0), m_nodeCount(0) { for (int i = 0; i < MaxChunks; i++) m_chunks[i] = nullptr; @@ -30,7 +134,17 @@ namespace Hal { } bool Namespace::AllocChunk() { - if (m_chunkCount >= MaxChunks) return false; + if (m_chunkCount >= MaxChunks) { + static bool warned = false; + if (!warned) { + warned = true; + KernelLogStream(WARNING, "AML") + << "Namespace node pool full at " << base::dec + << (uint64_t)(MaxChunks * NodesPerChunk) + << " nodes; part of the namespace will be missing"; + } + return false; + } auto* chunk = (NamespaceNode*)Memory::g_heap->Request( sizeof(NamespaceNode) * NodesPerChunk); @@ -45,6 +159,41 @@ namespace Hal { return true; } + bool Namespace::AppendChild(NamespaceNode* parent, int32_t child) { + if (parent->ChildCount >= parent->ChildCapacity) { + int32_t newCap = parent->ChildCapacity ? parent->ChildCapacity * 2 + : InitialChildren; + + auto* grown = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * newCap); + if (!grown) { + // Losing a child silently costs whole devices, so say it + // once rather than leaving a hole nobody can see. + static bool warned = false; + if (!warned) { + warned = true; + KernelLogStream(WARNING, "AML") + << "Out of heap growing a namespace node's child list;" + << " part of the namespace will be missing"; + } + return false; + } + + for (int32_t i = 0; i < parent->ChildCount; i++) + grown[i] = parent->ChildIndices[i]; + for (int32_t i = parent->ChildCount; i < newCap; i++) + grown[i] = -1; + + if (parent->ChildIndices) + Memory::g_heap->Free(parent->ChildIndices); + + parent->ChildIndices = grown; + parent->ChildCapacity = newCap; + } + + parent->ChildIndices[parent->ChildCount++] = child; + return true; + } + int32_t Namespace::AllocNode() { // Check if we need a new chunk int32_t capacity = m_chunkCount * NodesPerChunk; @@ -165,11 +314,7 @@ namespace Hal { // Add to parent's children auto* parent = GetNode(current); if (!parent) return -1; - if (parent->ChildCount < MaxChildren) { - parent->ChildIndices[parent->ChildCount++] = child; - } else { - return -1; // too many children - } + if (!AppendChild(parent, child)) return -1; } current = child; } @@ -188,6 +333,51 @@ namespace Hal { return current; } + int32_t Namespace::SearchUpward(const char* relative, int32_t scopeNodeIndex) const { + char segments[MaxPathDepth][MaxNameSegLen + 1]; + int segCount = 0; + + // Split on '.', padding each segment to four characters the way + // node names are stored. + int i = 0; + while (relative[i] && segCount < MaxPathDepth) { + char seg[MaxNameSegLen + 1]; + int n = 0; + while (relative[i] && relative[i] != '.' && n < MaxNameSegLen) + seg[n++] = relative[i++]; + seg[n] = '\0'; + while (relative[i] && relative[i] != '.') i++; // overlong segment + if (relative[i] == '.') i++; + + PadSegment(seg, segments[segCount]); + segCount++; + } + + if (segCount == 0) return -1; + + // Walk up from the current scope looking for the first segment. + int32_t scope = scopeNodeIndex; + while (scope >= 0) { + int32_t found = FindChildByName(scope, segments[0]); + if (found >= 0) { + // Descend the rest. A scope that matches the first segment + // but not the remainder does not win -- keep walking up. + int32_t cur = found; + bool ok = true; + for (int k = 1; k < segCount; k++) { + cur = FindChildByName(cur, segments[k]); + if (cur < 0) { ok = false; break; } + } + if (ok) return cur; + } + + auto* node = GetNode(scope); + scope = node ? node->ParentIndex : -1; + } + + return -1; + } + int32_t Namespace::ResolveName(const char* name, int32_t scopeNodeIndex) const { // If it starts with '\', it's absolute if (name[0] == '\\') diff --git a/kernel/src/ACPI/AML/AmlNamespace.hpp b/kernel/src/ACPI/AML/AmlNamespace.hpp index ced9ac2..218f006 100644 --- a/kernel/src/ACPI/AML/AmlNamespace.hpp +++ b/kernel/src/ACPI/AML/AmlNamespace.hpp @@ -55,10 +55,15 @@ namespace Hal { // ============================================================================ static constexpr int MaxNameSegLen = 4; static constexpr int MaxPathDepth = 16; - static constexpr int MaxChildren = 64; + // Children are held in a heap array that grows on demand, starting at + // this many entries. A fixed ceiling here is not survivable: an EC or + // GPE scope in a modern laptop DSDT carries well over a hundred named + // objects, and dropping the overflow loses whole devices (the battery + // among them) with no way to tell from the outside. + static constexpr int InitialChildren = 8; static constexpr int MaxStringLen = 64; static constexpr int MaxBufferLen = 256; - static constexpr int MaxPackageElements = 16; + static constexpr int MaxPackageElements = 32; static constexpr int MaxMethodArgs = 7; static constexpr int MaxMethodLocals = 8; @@ -76,6 +81,14 @@ namespace Hal { struct Object { ObjectType Type = ObjectType::None; + // Packages keep their raw AML bytes in Buffer (constant elements can + // be decoded straight from there), but a package whose elements are + // computed or written through Index() needs real per-element + // storage. PkgIndex names that storage once it is materialized; + // -1 means "not materialized yet". It lives outside the union so a + // package can carry both representations at once. + int32_t PkgIndex = -1; + union { uint64_t Integer; @@ -116,7 +129,36 @@ namespace Hal { } Processor; }; - Object() : Type(ObjectType::None), Integer(0) {} + Object() : Type(ObjectType::None), PkgIndex(-1), Integer(0) {} + }; + + // ============================================================================ + + // Package element store + + // ============================================================================ + // Element arrays for materialized packages, kept outside Object so the + // tagged union stays small. Entries are split in two: a persistent + // region for packages declared with Name() (materialized once and kept + // for the life of the namespace) and a transient ring for packages + // built inside a method, which is rewound at each top-level evaluation + // so repeated calls -- polling _BST, say -- cannot exhaust it. + namespace PackageStore { + static constexpr int MaxPersistent = 32; + static constexpr int MaxTransient = 32; + + // Allocate an element array of `count` objects. Returns -1 when the + // store is full or the count is out of range. + int32_t AllocPersistent(int count); + int32_t AllocTransient(int count); + + // Rewind the transient ring. Called once per top-level evaluation. + void ResetTransient(); + + // Element access. Returns nullptr for an unknown entry or an + // out-of-range element. + Object* Element(int32_t index, int element); + int Count(int32_t index); }; // ============================================================================ @@ -129,16 +171,19 @@ namespace Hal { char Name[MaxNameSegLen + 1]; // null-terminated 4-char segment Object Obj; int32_t ParentIndex; // -1 for root - int32_t ChildIndices[MaxChildren]; + int32_t* ChildIndices; // heap array, ChildCapacity long int32_t ChildCount; + int32_t ChildCapacity; + // Only ever called on a freshly allocated (zeroed) node, so there is + // no child array to release here. void Clear() { Name[0] = 0; Obj = Object{}; ParentIndex = -1; + ChildIndices = nullptr; ChildCount = 0; - for (int i = 0; i < MaxChildren; i++) - ChildIndices[i] = -1; + ChildCapacity = 0; } }; @@ -166,6 +211,14 @@ namespace Hal { // 3. Root scope int32_t ResolveName(const char* name, int32_t scopeNodeIndex) const; + // The ACPI name search rule: for a name written WITHOUT a root or + // parent prefix, search the current scope for its first segment, + // then each parent scope up to the root, and descend the remaining + // segments from wherever it matched. `relative` is the name as it + // appeared in the AML ("PBIF", "BAT0.PBIF"), never a path already + // rebased onto a scope. Returns -1 if no scope contains it. + int32_t SearchUpward(const char* relative, int32_t scopeNodeIndex) const; + // Get a node by index. NamespaceNode* GetNode(int32_t index); const NamespaceNode* GetNode(int32_t index) const; @@ -217,6 +270,11 @@ namespace Hal { private: int32_t AllocNode(); bool AllocChunk(); + + // Append a child index to a parent, growing its array as needed. + // Returns false only when the heap is exhausted. + bool AppendChild(NamespaceNode* parent, int32_t child); + void EnsureRoot(); int32_t FindChildByName(int32_t parentIndex, const char* seg) const; diff --git a/kernel/src/ACPI/AcpiDevices.cpp b/kernel/src/ACPI/AcpiDevices.cpp index 82e8089..572845a 100644 --- a/kernel/src/ACPI/AcpiDevices.cpp +++ b/kernel/src/ACPI/AcpiDevices.cpp @@ -319,6 +319,58 @@ namespace Hal { // EnumerateAll + // ============================================================================ + // Fill one DeviceInfo from a namespace node. + static void DescribeDevice(AML::Namespace& ns, int32_t idx, DeviceInfo& dev) { + dev.NodeIndex = idx; + ns.GetNodePath(idx, dev.Path, 128); + + dev.Status = EvaluateSta(idx); + dev.IsPresent = (dev.Status & STA_PRESENT) && (dev.Status & STA_FUNCTIONAL); + + if (!EvaluateHid(idx, dev.HardwareId, 16)) + dev.HardwareId[0] = '\0'; + if (!EvaluateUid(idx, dev.UniqueId, 16)) + dev.UniqueId[0] = '\0'; + + dev.Address = EvaluateAdr(idx); + } + + // ============================================================================ + + // FindDeviceByHid + + // ============================================================================ + bool FindDeviceByHid(const char* hid, DeviceInfo& out) { + auto& interp = AML::GetInterpreter(); + if (!interp.IsInitialized()) return false; + + auto& ns = interp.GetNamespace(); + int32_t found = -1; + + // Only _HID is evaluated per candidate; the full description is + // built once, for the match. WalkDescendants has no early exit, so + // the first match wins and later ones are ignored. + ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device, + [&](int32_t idx, [[maybe_unused]] const AML::NamespaceNode* node) { + if (found >= 0) return; + + char candidate[16]; + if (!EvaluateHid(idx, candidate, 16)) return; + if (StrEqual(candidate, hid)) found = idx; + }); + + if (found < 0) return false; + + out = DeviceInfo{}; + DescribeDevice(ns, found, out); + return true; + } + + // ============================================================================ + + // EnumerateAll + // ============================================================================ void EnumerateAll(DeviceList& result) { auto& interp = AML::GetInterpreter(); @@ -326,39 +378,32 @@ namespace Hal { auto& ns = interp.GetNamespace(); result.Count = 0; + result.Truncated = false; // Walk all namespace nodes looking for Device objects ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device, [&](int32_t idx, [[maybe_unused]] const AML::NamespaceNode* node) { - if (result.Count >= MaxDevices) return; - - auto& dev = result.Devices[result.Count]; - dev.NodeIndex = idx; - - // Get the path - ns.GetNodePath(idx, dev.Path, 128); - - // Evaluate _STA - dev.Status = EvaluateSta(idx); - dev.IsPresent = (dev.Status & STA_PRESENT) && (dev.Status & STA_FUNCTIONAL); - - // Evaluate _HID - if (!EvaluateHid(idx, dev.HardwareId, 16)) - dev.HardwareId[0] = '\0'; - - // Evaluate _UID - if (!EvaluateUid(idx, dev.UniqueId, 16)) - dev.UniqueId[0] = '\0'; - - // Evaluate _ADR - dev.Address = EvaluateAdr(idx); + if (result.Count >= MaxDevices) { + result.Truncated = true; + return; + } + DescribeDevice(ns, idx, result.Devices[result.Count]); result.Count++; }); KernelLogStream(OK, "ACPI") << "Enumerated " << base::dec << (uint64_t)result.Count << " ACPI devices"; + // The list is a fixed-size stack object, so a large namespace runs + // past it. Say so: a device missing from the log below may simply + // be one that did not fit, and looking it up by HID still works. + if (result.Truncated) { + KernelLogStream(WARNING, "ACPI") << "Device list truncated at " + << base::dec << (uint64_t)MaxDevices + << "; use FindDeviceByHid to reach the rest"; + } + // Log discovered devices for (int i = 0; i < result.Count; i++) { auto& dev = result.Devices[i]; diff --git a/kernel/src/ACPI/AcpiDevices.hpp b/kernel/src/ACPI/AcpiDevices.hpp index 184fe53..39945e8 100644 --- a/kernel/src/ACPI/AcpiDevices.hpp +++ b/kernel/src/ACPI/AcpiDevices.hpp @@ -45,8 +45,9 @@ namespace Hal { struct DeviceList { DeviceInfo Devices[MaxDevices]; int Count; + bool Truncated; // the namespace held more than MaxDevices - DeviceList() : Count(0) {} + DeviceList() : Count(0), Truncated(false) {} // Find a device by HID. Returns nullptr if not found. const DeviceInfo* FindByHid(const char* hid) const; @@ -64,6 +65,12 @@ namespace Hal { // The interpreter must be initialized first (LoadTable called). void EnumerateAll(DeviceList& result); + // Find the first device with the given _HID, walking the namespace + // directly. Unlike EnumerateAll this has no device-count ceiling and + // needs no multi-kilobyte list on the caller's stack, so it is the way + // to reach a specific device on a machine whose namespace is large. + bool FindDeviceByHid(const char* hid, DeviceInfo& out); + // Evaluate _STA for a device node. Returns STA_DEFAULT if no _STA exists. uint32_t EvaluateSta(int32_t deviceNodeIndex); diff --git a/kernel/src/ACPI/Battery.cpp b/kernel/src/ACPI/Battery.cpp new file mode 100644 index 0000000..16b5468 --- /dev/null +++ b/kernel/src/ACPI/Battery.cpp @@ -0,0 +1,422 @@ +/* + * Battery.cpp + * ACPI Control Method Battery: _STA / _BIF / _BIX / _BST, plus _PSR on the + * AC adapter + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Battery.hpp" +#include "AcpiDevices.hpp" +#include +#include +#include +#include +#include + +using namespace Kt; + +namespace Hal { + namespace Battery { + + // ============================================================================ + + // State + + // ============================================================================ + // ACPI reports an unreadable field as all-ones rather than zero. + static constexpr uint32_t Unknown32 = 0xFFFFFFFFu; + + // The EC transactions behind _BST take a few milliseconds, and the + // value only moves on a scale of minutes, so a short cache keeps a + // one-second panel repaint from becoming a bus workload. + static constexpr uint64_t CacheMs = 2000; + + static bool g_initialized = false; + static bool g_haveBattery = false; + static bool g_haveAdapter = false; + static char g_batteryPath[128]; + static char g_adapterPath[128]; + static int32_t g_batteryNode = -1; + + static kcp::Mutex g_lock; + + // Set for the boot-path probe only, so the per-method tracing does not + // repeat on every poll once the system is up. + static bool g_tracing = false; + + static bool Refresh(Status& out); + static void Probe(); + static Status g_cached{}; + static bool g_cacheValid = false; + static uint64_t g_cacheStamp = 0; + + // ============================================================================ + + // Path helpers + + // ============================================================================ + static void AppendMethod(const char* devicePath, const char* method, char* out, int maxLen) { + int i = 0; + while (devicePath[i] && i < maxLen - 6) { + out[i] = devicePath[i]; + i++; + } + // The root scope is written "\" with no separator before the first + // segment; every other path takes a dot. + if (!(i == 1 && out[0] == '\\')) + out[i++] = '.'; + for (int j = 0; j < 4 && i < maxLen - 1; j++) + out[i++] = method[j]; + out[i] = '\0'; + } + + static void CopyPath(char* dst, const char* src, int maxLen) { + int i = 0; + while (src[i] && i < maxLen - 1) { + dst[i] = src[i]; + i++; + } + dst[i] = '\0'; + } + + // ============================================================================ + + // Initialize + + // ============================================================================ + void Initialize() { + if (g_initialized) return; + g_initialized = true; + + auto& interp = AML::GetInterpreter(); + if (!interp.IsInitialized()) return; + + // Searched by HID rather than off an enumerated list: a laptop + // namespace runs well past the list's fixed size, and the battery + // sits deep under the EC where it would be cut off. + AcpiDevices::DeviceInfo bat{}; + if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_BATTERY, bat)) { + CopyPath(g_batteryPath, bat.Path, sizeof(g_batteryPath)); + g_batteryNode = bat.NodeIndex; + g_haveBattery = true; + } + + AcpiDevices::DeviceInfo ac{}; + if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_AC_ADAPTER, ac)) { + CopyPath(g_adapterPath, ac.Path, sizeof(g_adapterPath)); + g_haveAdapter = true; + } + + if (!g_haveBattery) { + // Distinguish "this machine has no battery" from "the namespace + // walk found nothing at all", which would point at the walk + // rather than the hardware. + auto& ns = interp.GetNamespace(); + int deviceCount = 0; + ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device, + [&](int32_t, const AML::NamespaceNode*) { deviceCount++; }); + + KernelLogStream(INFO, "Battery") << "No ACPI battery device (PNP0C0A) among " + << base::dec << (uint64_t)deviceCount << " ACPI devices"; + return; + } + + KernelLogStream(OK, "Battery") << "ACPI battery at " << g_batteryPath + << (g_haveAdapter ? " (AC adapter present)" : " (no AC adapter device)"); + + Probe(); + } + + // Evaluate the battery methods once, here on the boot path, announcing + // each before it runs. These are the first firmware methods this kernel + // executes that reach the embedded controller, and if one of them takes + // the machine down the last line printed says which -- far better than + // discovering it when the desktop makes its first query. + static void Probe() { + KernelLogStream(INFO, "Battery") << "Probing battery methods"; + + g_tracing = true; + Status s{}; + bool ok = Refresh(s); + g_tracing = false; + + if (!ok) { + KernelLogStream(WARNING, "Battery") << "Probe failed; battery status unavailable"; + g_haveBattery = false; + return; + } + + g_cached = s; + g_cacheStamp = 0; // let the first real query refresh + g_cacheValid = true; + + if (!s.Present) { + KernelLogStream(INFO, "Battery") << "Battery slot empty" + << (s.AcOnline ? " (on AC)" : ""); + return; + } + + KernelLogStream(OK, "Battery") << "Battery at " << base::dec + << (uint64_t)s.Percent << "% (" + << (s.Charging ? "charging" : (s.Discharging ? "discharging" : "idle")) + << ", " << (uint64_t)s.RemainingCapacity << " of " + << (uint64_t)s.FullCapacity << (s.PowerUnitMilliAmps ? " mAh)" : " mWh)"); + } + + bool IsPresent() { + return g_haveBattery; + } + + // ============================================================================ + + // Method evaluation + + // ============================================================================ + static const char* TypeName(AML::ObjectType t) { + switch (t) { + case AML::ObjectType::None: return "none"; + case AML::ObjectType::Integer: return "integer"; + case AML::ObjectType::String: return "string"; + case AML::ObjectType::Buffer: return "buffer"; + case AML::ObjectType::Package: return "package"; + case AML::ObjectType::Device: return "device"; + case AML::ObjectType::Method: return "method"; + case AML::ObjectType::OperationRegion: return "opregion"; + case AML::ObjectType::Field: return "field"; + default: return "other"; + } + } + + static bool EvalPackage(const char* devicePath, const char* method, AML::Object& out) { + char path[160]; + AppendMethod(devicePath, method, path, sizeof(path)); + + auto& interp = AML::GetInterpreter(); + + // Separate "the method is not there" from "it ran and gave us + // something that is not a package": those need opposite fixes, and + // the failure looks identical from the caller. + int32_t node = interp.GetNamespace().FindNode(path); + if (node < 0) { + if (g_tracing) + KernelLogStream(INFO, "Battery") << " " << path << ": absent"; + return false; + } + + // Trace the opcodes of the one method whose behaviour we cannot + // otherwise explain: it runs to completion, trips no limit, and + // still yields no value. + bool traceThis = g_tracing && method[1] == 'B'; + if (traceThis) interp.SetTrace(120); + + bool ok = interp.EvaluateObject(path, out); + + if (traceThis) interp.SetTrace(0); + + if (g_tracing) { + KernelLogStream(INFO, "Battery") << " " << path << ": " + << (ok ? "ok" : "FAILED") << ", returned " << TypeName(out.Type) + << " with " << base::dec + << (uint64_t)interp.GetPackageCount(out) << " element(s)"; + } + + return ok && out.Type == AML::ObjectType::Package; + } + + static uint32_t ElementInteger(const AML::Object& pkg, int index) { + AML::Object elem{}; + if (!AML::GetInterpreter().GetPackageElement(pkg, index, elem)) return Unknown32; + if (elem.Type != AML::ObjectType::Integer) return Unknown32; + return (uint32_t)elem.Integer; + } + + // _BIX supersedes _BIF and prepends a Revision field, shifting every + // shared field by one. Both are read into the same three numbers. + struct BatteryInfo { + bool Valid; + bool PowerUnitMilliAmps; + uint32_t DesignCapacity; + uint32_t FullCapacity; + }; + + static bool ReadInfo(BatteryInfo& info) { + info = BatteryInfo{}; + + AML::Object pkg{}; + int base = 0; + + if (EvalPackage(g_batteryPath, "_BIX", pkg)) { + // _BIX revision 0 starts with Revision; later revisions keep it + // in the same place, so the offset is fixed at one. + base = 1; + } else if (EvalPackage(g_batteryPath, "_BIF", pkg)) { + base = 0; + } else { + return false; + } + + uint32_t unit = ElementInteger(pkg, base + 0); + uint32_t design = ElementInteger(pkg, base + 1); + uint32_t full = ElementInteger(pkg, base + 2); + + info.PowerUnitMilliAmps = (unit != Unknown32) && (unit & 1); + info.DesignCapacity = (design == Unknown32) ? 0 : design; + info.FullCapacity = (full == Unknown32) ? 0 : full; + info.Valid = true; + return true; + } + + static bool AcOnline() { + if (!g_haveAdapter) return false; + + char path[160]; + AppendMethod(g_adapterPath, "_PSR", path, sizeof(path)); + + if (g_tracing) + KernelLogStream(INFO, "Battery") << " evaluating " << path; + + AML::Object result{}; + if (!AML::GetInterpreter().EvaluateObject(path, result)) return false; + return result.Type == AML::ObjectType::Integer && result.Integer != 0; + } + + // ============================================================================ + + // GetStatus + + // ============================================================================ + static bool Refresh(Status& out) { + out = Status{}; + out.MinutesRemaining = -1; + out.HasAcAdapter = g_haveAdapter; + + // _STA bit 4 says whether a battery is actually in the slot. A + // laptop with the pack removed still has the device. + uint32_t sta = (g_batteryNode >= 0) + ? AcpiDevices::EvaluateSta(g_batteryNode) + : AcpiDevices::STA_DEFAULT; + + // Bit 4 is the one that decides everything below, and firmware + // clears it for reasons that have nothing to do with the pack + // (an EC that has not been told its region handler is live, most + // often), so log the raw value rather than just the verdict. + if (g_tracing) { + KernelLogStream(INFO, "Battery") << " _STA = " << base::hex + << (uint64_t)sta << (((sta & AcpiDevices::STA_BATTERY) != 0) + ? " (battery present)" : " (battery bit clear)"); + } + + out.AcOnline = AcOnline(); + + // _STA bit 4 is the spec's answer for "is a pack installed", but + // it is only as good as the firmware path behind it. Read the + // battery methods either way and let the capacity data have the + // final say -- see the staSaysAbsent handling below. + bool staSaysAbsent = (sta & AcpiDevices::STA_BATTERY) == 0; + + BatteryInfo info{}; + if (!ReadInfo(info)) { + if (g_tracing) + KernelLogStream(INFO, "Battery") << " no _BIX/_BIF package"; + return true; + } + + AML::Object bst{}; + if (!EvalPackage(g_batteryPath, "_BST", bst)) { + if (g_tracing) + KernelLogStream(INFO, "Battery") << " no _BST package"; + return true; + } + + uint32_t state = ElementInteger(bst, 0); + uint32_t rate = ElementInteger(bst, 1); + uint32_t remaining = ElementInteger(bst, 2); + uint32_t voltage = ElementInteger(bst, 3); + + if (g_tracing) { + KernelLogStream(INFO, "Battery") << " _BIF/_BIX: design=" + << base::dec << (uint64_t)info.DesignCapacity + << " lastFull=" << (uint64_t)info.FullCapacity + << (info.PowerUnitMilliAmps ? " (mAh)" : " (mWh)"); + KernelLogStream(INFO, "Battery") << " _BST: state=" << base::hex + << (uint64_t)state << " rate=" << base::dec << (uint64_t)rate + << " remaining=" << (uint64_t)remaining + << " voltage=" << (uint64_t)voltage << "mV"; + } + + // Trust real capacity data over a status bit that says otherwise: + // firmware that reports a pack's charge is describing a pack that + // is there. + if (staSaysAbsent) { + bool looksReal = info.FullCapacity > 0 && info.FullCapacity != Unknown32 + && remaining != Unknown32; + if (!looksReal) { + if (g_tracing) + KernelLogStream(INFO, "Battery") + << " _STA says absent and the methods agree"; + return true; + } + if (g_tracing) + KernelLogStream(WARNING, "Battery") + << " _STA says absent but _BST reports charge; trusting _BST"; + } + + if (state == Unknown32) state = 0; + + out.Present = true; + out.Charging = (state & StateCharging) != 0; + out.Discharging = (state & StateDischarging) != 0; + out.Critical = (state & StateCritical) != 0; + + out.PowerUnitMilliAmps = info.PowerUnitMilliAmps; + out.DesignCapacity = info.DesignCapacity; + out.FullCapacity = info.FullCapacity; + out.RemainingCapacity = (remaining == Unknown32) ? 0 : remaining; + out.PresentRate = (rate == Unknown32) ? 0 : rate; + out.VoltageMv = (voltage == Unknown32) ? 0 : voltage; + + // Some firmware leaves last-full at zero or below the current + // reading after a swap; fall back to the design capacity, and + // clamp so the panel can never show 143%. + uint32_t full = out.FullCapacity; + if (full == 0) full = out.DesignCapacity; + + if (full > 0 && out.RemainingCapacity > 0) { + uint64_t pct = ((uint64_t)out.RemainingCapacity * 100) / full; + out.Percent = (pct > 100) ? 100 : (uint8_t)pct; + } + + if (out.PresentRate > 0 && out.Discharging) { + out.MinutesRemaining = + (int32_t)(((uint64_t)out.RemainingCapacity * 60) / out.PresentRate); + } + + return true; + } + + bool GetStatus(Status& out) { + if (!g_haveBattery) return false; + + g_lock.Acquire(); + + uint64_t now = Timekeeping::GetMilliseconds(); + if (g_cacheValid && now - g_cacheStamp < CacheMs) { + out = g_cached; + g_lock.Release(); + return true; + } + + Status fresh{}; + bool ok = Refresh(fresh); + if (ok) { + g_cached = fresh; + g_cacheStamp = now; + g_cacheValid = true; + } + + out = ok ? fresh : g_cached; + g_lock.Release(); + return ok || g_cacheValid; + } + }; +}; diff --git a/kernel/src/ACPI/Battery.hpp b/kernel/src/ACPI/Battery.hpp new file mode 100644 index 0000000..2e4fe1a --- /dev/null +++ b/kernel/src/ACPI/Battery.hpp @@ -0,0 +1,62 @@ +/* + * Battery.hpp + * ACPI Control Method Battery (PNP0C0A) and AC adapter (ACPI0003) + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Hal { + namespace Battery { + + // ============================================================================ + + // Battery state + + // ============================================================================ + // _BST battery state bits. + static constexpr uint32_t StateDischarging = (1 << 0); + static constexpr uint32_t StateCharging = (1 << 1); + static constexpr uint32_t StateCritical = (1 << 2); + + struct Status { + bool Present; // a battery slot exists and reports a battery in it + bool Charging; + bool Discharging; + bool Critical; + bool AcOnline; // AC adapter connected (false when no adapter device) + bool HasAcAdapter; // an ACPI0003 device exists + + uint8_t Percent; // 0..100, clamped; 0 when capacity is unknown + uint32_t RemainingCapacity; // mWh or mAh, per PowerUnitMilliAmps + uint32_t FullCapacity; // last full charge capacity, same unit + uint32_t DesignCapacity; + uint32_t PresentRate; // mW or mA, 0 when unknown + uint32_t VoltageMv; // present voltage in mV, 0 when unknown + bool PowerUnitMilliAmps; // capacities are mAh/mA rather than mWh/mW + + // Minutes of runtime left at the present rate, or -1 when the rate + // is unknown/zero (which includes every charging battery). + int32_t MinutesRemaining; + }; + + // ============================================================================ + + // API + + // ============================================================================ + // Discover battery and AC adapter devices in the ACPI namespace. Safe to + // call more than once; only the first call does work. + void Initialize(); + + // True when at least one battery device was found, whether or not a + // battery is currently installed in it. + bool IsPresent(); + + // Read the current status. Results are cached briefly so that polling + // from a panel indicator does not hammer the embedded controller. + // Returns false when the machine has no battery device at all. + bool GetStatus(Status& out); + }; +}; diff --git a/kernel/src/ACPI/EmbeddedController.cpp b/kernel/src/ACPI/EmbeddedController.cpp new file mode 100644 index 0000000..2aa2fd7 --- /dev/null +++ b/kernel/src/ACPI/EmbeddedController.cpp @@ -0,0 +1,326 @@ +/* + * EmbeddedController.cpp + * ACPI Embedded Controller transport (polled, no burst mode) + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "EmbeddedController.hpp" +#include "ACPI.hpp" +#include "AcpiDevices.hpp" +#include +#include +#include +#include +#include +#include + +using namespace Kt; + +namespace Hal { + namespace EmbeddedController { + + // ============================================================================ + + // EC register protocol + + // ============================================================================ + // EC_SC (status, read from the command port) + static constexpr uint8_t SC_OBF = (1 << 0); // output buffer full: data is readable + static constexpr uint8_t SC_IBF = (1 << 1); // input buffer full: EC has not consumed our byte + + // EC_SC commands (written to the command port) + static constexpr uint8_t CMD_READ = 0x80; + static constexpr uint8_t CMD_WRITE = 0x81; + + // The EC is a slow microcontroller; a byte transaction is typically a + // few hundred microseconds but firmware is allowed to take much longer. + // One "tick" is a port-wait (~1us), so this caps a wait near 500ms. + static constexpr uint32_t PollTicks = 500000; + + // ============================================================================ + + // State + + // ============================================================================ + static uint16_t g_dataPort = 0; + static uint16_t g_cmdPort = 0; + static bool g_available = false; + static bool g_initialized = false; + // A Mutex, not a Spinlock: EC transactions run only from process and + // syscall context, and can spin for milliseconds waiting on the + // controller. A Spinlock would hold interrupts off for all of it. + static kcp::Mutex g_lock; + + // ============================================================================ + + // ECDT + + // ============================================================================ + // The ECDT gives the EC's ports without needing the AML namespace. It is + // optional, and some firmware ships one with zeroed ports, so treat a + // zero port as "not described here" and fall through to the namespace. + struct EcdtTable { + ACPI::CommonSDTHeader Header; + uint8_t ControlGas[12]; + uint8_t DataGas[12]; + uint32_t Uid; + uint8_t GpeBit; + // followed by a null-terminated namespace path + } __attribute__((packed)); + + static bool SignatureMatch(const char* sig, const char* target) { + for (int i = 0; i < 4; i++) { + if (sig[i] != target[i]) return false; + } + return true; + } + + // Decode a Generic Address Structure that must describe a 16-bit I/O port. + static uint16_t GasIoPort(const uint8_t gas[12]) { + if (gas[0] != 1) return 0; // AddressSpaceId 1 == SystemIO + + uint64_t address = 0; + for (int i = 0; i < 8; i++) + address |= (uint64_t)gas[4 + i] << (i * 8); + return address <= 0xFFFF ? (uint16_t)address : 0; + } + + static bool FromEcdt(ACPI::CommonSDTHeader* xsdt) { + if (xsdt == nullptr) return false; + + uint32_t entryCount = (xsdt->Length - sizeof(ACPI::CommonSDTHeader)) / 8; + uint64_t* entries = (uint64_t*)((uint64_t)xsdt + sizeof(ACPI::CommonSDTHeader)); + + for (uint32_t i = 0; i < entryCount; i++) { + auto* header = (ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]); + if (!SignatureMatch(header->Signature, "ECDT")) continue; + if (header->Length < sizeof(EcdtTable)) continue; + + auto* ecdt = (EcdtTable*)header; + uint16_t cmd = GasIoPort(ecdt->ControlGas); + uint16_t data = GasIoPort(ecdt->DataGas); + if (cmd == 0 || data == 0) continue; + + g_cmdPort = cmd; + g_dataPort = data; + return true; + } + + return false; + } + + // ============================================================================ + + // Namespace fallback + + // ============================================================================ + // _CRS on an EC device lists two I/O resources in a fixed order: data + // register first, then command/status. Anything else is not an EC we + // know how to drive. + static bool FromNamespace() { + if (!AML::GetInterpreter().IsInitialized()) return false; + + AcpiDevices::DeviceInfo ec{}; + if (!AcpiDevices::FindDeviceByHid(AcpiDevices::HID_EC, ec)) return false; + + AML::ResourceList crs{}; + if (!AcpiDevices::EvaluateCrs(ec.NodeIndex, crs)) return false; + + uint16_t ports[2] = {0, 0}; + int found = 0; + for (int i = 0; i < crs.Count && found < 2; i++) { + if (crs.Resources[i].Type == AML::ResourceType::IoPort) + ports[found++] = crs.Resources[i].IoPort.Base; + else if (crs.Resources[i].Type == AML::ResourceType::FixedIoPort) + ports[found++] = crs.Resources[i].FixedIoPort.Base; + } + + if (found < 2 || ports[0] == 0 || ports[1] == 0) return false; + + g_dataPort = ports[0]; + g_cmdPort = ports[1]; + return true; + } + + // ============================================================================ + + // Initialize + + // ============================================================================ + void Initialize(void* xsdt) { + if (g_initialized) return; + g_initialized = true; + + if (!FromEcdt((ACPI::CommonSDTHeader*)xsdt) && !FromNamespace()) { + KernelLogStream(INFO, "EC") << "No ACPI Embedded Controller present"; + return; + } + + g_available = true; + KernelLogStream(OK, "EC") << "Embedded Controller at cmd=" << base::hex + << (uint64_t)g_cmdPort << " data=" << (uint64_t)g_dataPort; + + RunRegMethods(); + + KernelLogStream(INFO, "EC") << "EC space dump:"; + DumpSpace(0x00, 32); + } + + // ============================================================================ + + // _REG + + // ============================================================================ + void RunRegMethods() { + auto& interp = AML::GetInterpreter(); + if (!interp.IsInitialized()) return; + + auto& ns = interp.GetNamespace(); + + // Collect first, evaluate second. Executing AML can create namespace + // nodes, which reallocates a parent's child array -- walking the + // tree while methods run would be iterating a freed array. + static constexpr int MaxRegMethods = 64; + int32_t nodes[MaxRegMethods]; + int count = 0; + + ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Method, + [&](int32_t idx, const AML::NamespaceNode* node) { + if (count >= MaxRegMethods) return; + if (node->Name[0] == '_' && node->Name[1] == 'R' && + node->Name[2] == 'E' && node->Name[3] == 'G') + nodes[count++] = idx; + }); + + // Arg0 = region space (3, EmbeddedControl), Arg1 = 1 (handler is + // connected). A _REG that handles some other space checks Arg0 and + // ignores the call, so this is safe to issue namespace-wide. + AML::Object args[2]{}; + args[0].Type = AML::ObjectType::Integer; + args[0].Integer = (uint64_t)AML::RegionSpace::EmbeddedControl; + args[1].Type = AML::ObjectType::Integer; + args[1].Integer = 1; + + int ran = 0; + for (int i = 0; i < count; i++) { + char path[256]; + ns.GetNodePath(nodes[i], path, sizeof(path)); + + AML::Object result{}; + bool ok = interp.EvaluateMethod(path, args, 2, result); + if (ok) ran++; + + KernelLogStream(INFO, "EC") << " _REG " << path + << (ok ? " ok" : " FAILED"); + } + + KernelLogStream(OK, "EC") << "Ran " << base::dec << (uint64_t)ran + << " of " << (uint64_t)count << " _REG method(s)"; + } + + // ============================================================================ + + // Diagnostics + + // ============================================================================ + void DumpSpace(uint8_t base_, int bytes) { + if (!g_available) return; + + // Whether the transport works at all is not otherwise observable: + // a failed read and a genuinely zero register look identical by the + // time firmware AML has turned them into a status value. All-00 or + // all-FF here means the handshake is wrong, not that the battery is + // missing. + for (int row = 0; row < bytes; row += 16) { + char line[64]; + int n = 0; + bool anyFailed = false; + + for (int i = 0; i < 16 && (row + i) < bytes; i++) { + uint8_t v = 0; + if (!Read((uint8_t)(base_ + row + i), v)) anyFailed = true; + + static const char hex[] = "0123456789ABCDEF"; + line[n++] = hex[(v >> 4) & 0xF]; + line[n++] = hex[v & 0xF]; + line[n++] = ' '; + } + line[n] = '\0'; + + KernelLogStream(INFO, "EC") << " " << base::hex + << (uint64_t)(base_ + row) << ": " << line + << (anyFailed ? " (read timeout)" : ""); + } + } + + bool IsAvailable() { + return g_available; + } + + // ============================================================================ + + // Transport + + // ============================================================================ + // Spin until the named status bit reaches the wanted state. + static bool WaitStatus(uint8_t mask, bool wantSet) { + for (uint32_t i = 0; i < PollTicks; i++) { + uint8_t sc = Io::In8(g_cmdPort); + if (((sc & mask) != 0) == wantSet) return true; + Io::IoPortWait(); + } + return false; + } + + // Drain a byte the EC left in the output buffer from an earlier aborted + // transaction. Without this the next read returns the stale value. + static void DrainOutput() { + if (Io::In8(g_cmdPort) & SC_OBF) + (void)Io::In8(g_dataPort); + } + + bool Read(uint8_t address, uint8_t& value) { + if (!g_available) return false; + + g_lock.Acquire(); + DrainOutput(); + + bool ok = false; + if (WaitStatus(SC_IBF, false)) { + Io::Out8(CMD_READ, g_cmdPort); + if (WaitStatus(SC_IBF, false)) { + Io::Out8(address, g_dataPort); + if (WaitStatus(SC_OBF, true)) { + value = Io::In8(g_dataPort); + ok = true; + } + } + } + + g_lock.Release(); + return ok; + } + + bool Write(uint8_t address, uint8_t value) { + if (!g_available) return false; + + g_lock.Acquire(); + DrainOutput(); + + bool ok = false; + if (WaitStatus(SC_IBF, false)) { + Io::Out8(CMD_WRITE, g_cmdPort); + if (WaitStatus(SC_IBF, false)) { + Io::Out8(address, g_dataPort); + if (WaitStatus(SC_IBF, false)) { + Io::Out8(value, g_dataPort); + ok = WaitStatus(SC_IBF, false); + } + } + } + + g_lock.Release(); + return ok; + } + }; +}; diff --git a/kernel/src/ACPI/EmbeddedController.hpp b/kernel/src/ACPI/EmbeddedController.hpp new file mode 100644 index 0000000..dee457b --- /dev/null +++ b/kernel/src/ACPI/EmbeddedController.hpp @@ -0,0 +1,50 @@ +/* + * EmbeddedController.hpp + * ACPI Embedded Controller (PNP0C09) transport + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Hal { + namespace EmbeddedController { + + // ============================================================================ + + // Bring-up + + // ============================================================================ + // Locate the EC in the ACPI namespace and latch its command/data ports. + // Prefers the ECDT table when present (it is valid before the namespace + // is loaded); otherwise walks the namespace for PNP0C09 and reads _CRS. + // Safe to call more than once; only the first call does work. + void Initialize(void* xsdt); + + // True once a usable command/data port pair has been found. + bool IsAvailable(); + + // Run the namespace's _REG(EmbeddedControl, 1) methods, telling firmware + // that EC region access is live. ACPI requires this once a handler for + // an address space is installed, and firmware leans on it hard: EC + // methods are typically guarded by a flag that _REG sets, and until it + // is set they return safe do-nothing answers (a battery reports itself + // absent, a thermal zone reports no temperature). Called by Initialize. + void RunRegMethods(); + + // Log `bytes` bytes of EC address space starting at `base_`. Purely a + // diagnostic: it is the only way to tell a broken handshake from a + // register that genuinely reads zero. + void DumpSpace(uint8_t base_, int bytes); + + // ============================================================================ + + // Transport + + // ============================================================================ + // Read/write one byte of the EC's 256-byte address space. Both return + // false on timeout or when no EC is present, leaving *value untouched. + bool Read(uint8_t address, uint8_t& value); + bool Write(uint8_t address, uint8_t value); + }; +}; diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 600ad57..5ca920d 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 185 +#define MONTAUK_BUILD_NUMBER 220 diff --git a/kernel/src/Api/Power.hpp b/kernel/src/Api/Power.hpp index 1b070d5..e347716 100644 --- a/kernel/src/Api/Power.hpp +++ b/kernel/src/Api/Power.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -114,4 +115,30 @@ namespace montauk::abi { out->apIdleHint = Hal::CpuIdle::GetApIdleHint(); return 0; } + + // SYS_BATTERYINFO. Fills a battery snapshot. Returns -1 on a machine with + // no ACPI battery device, which is how a desktop-class caller learns not to + // draw an indicator at all. + static int64_t Sys_BatteryInfo(BatteryInfo* out) { + Hal::Battery::Status s{}; + if (!Hal::Battery::GetStatus(s)) { + return -1; + } + + out->present = s.Present ? 1 : 0; + out->charging = s.Charging ? 1 : 0; + out->discharging = s.Discharging ? 1 : 0; + out->critical = s.Critical ? 1 : 0; + out->acOnline = s.AcOnline ? 1 : 0; + out->hasAcAdapter = s.HasAcAdapter ? 1 : 0; + out->percent = s.Percent; + out->powerUnitMilliAmps = s.PowerUnitMilliAmps ? 1 : 0; + out->remainingCapacity = s.RemainingCapacity; + out->fullCapacity = s.FullCapacity; + out->designCapacity = s.DesignCapacity; + out->presentRate = s.PresentRate; + out->voltageMv = s.VoltageMv; + out->minutesRemaining = s.MinutesRemaining; + return 0; + } }; diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index d935770..701fb0b 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -504,6 +504,12 @@ namespace montauk::abi { case SYS_POWERINFO: if (!UserMemory::Writable(frame->arg1)) return -1; return Sys_PowerInfo((PowerInfo*)frame->arg1); + case SYS_BATTERYINFO: + // Read-only power telemetry, ungated like SYS_POWERINFO: the + // panel indicator has to work in an unprivileged session, which + // holds no device capability to spend on it. + if (!UserMemory::Writable(frame->arg1)) return -1; + return Sys_BatteryInfo((BatteryInfo*)frame->arg1); case SYS_THREAD_SPAWN: return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3); case SYS_THREAD_EXIT: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index f05602c..1b72f48 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -343,6 +343,9 @@ namespace montauk::abi { touch in full (image decode, heap slabs). */ static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure + /* Battery.hpp -- ACPI battery status */ + static constexpr uint64_t SYS_BATTERYINFO = 188; // (BatteryInfo*) -> 0, -1 no battery device + /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; @@ -881,6 +884,26 @@ namespace montauk::abi { uint32_t apIdleHint; // MWAIT hint used for AP deep idle }; + // Battery status (returned by SYS_BATTERYINFO). A machine with no battery + // device fails the call outright; `present` distinguishes a laptop with the + // pack removed from one that is running on it. + struct BatteryInfo { + uint8_t present; // a battery is installed + uint8_t charging; + uint8_t discharging; + uint8_t critical; + uint8_t acOnline; // AC adapter connected + uint8_t hasAcAdapter; // an AC adapter device exists at all + uint8_t percent; // 0..100, 0 when capacity is unknown + uint8_t powerUnitMilliAmps; // capacities are mAh/mA, not mWh/mW + uint32_t remainingCapacity; + uint32_t fullCapacity; // last full charge capacity + uint32_t designCapacity; + uint32_t presentRate; // mW or mA, 0 when unknown + uint32_t voltageMv; + int32_t minutesRemaining; // -1 when not estimable (e.g. charging) + }; + // Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT) struct CrashReportInfo { int pid; diff --git a/kernel/src/Boot/Main.cpp b/kernel/src/Boot/Main.cpp index f83e438..a3d4f3f 100644 --- a/kernel/src/Boot/Main.cpp +++ b/kernel/src/Boot/Main.cpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include #include #include @@ -131,6 +133,12 @@ extern "C" void kmain() { #if defined (__x86_64__) if (g_acpi.GetXSDT() != nullptr) { Hal::AcpiShutdown::Initialize(g_acpi.GetXSDT()); + + // The EC is the transport behind battery (and most thermal) methods, so + // it has to be latched before anything evaluates them. + Hal::EmbeddedController::Initialize(g_acpi.GetXSDT()); + Hal::Battery::Initialize(); + Hal::CpuIdle::Initialize(g_acpi.GetXSDT()); Hal::ApicInitialize(g_acpi.GetXSDT()); diff --git a/kernel/src/Common/Panic.cpp b/kernel/src/Common/Panic.cpp index fd2e0d5..7b398e0 100644 --- a/kernel/src/Common/Panic.cpp +++ b/kernel/src/Common/Panic.cpp @@ -42,10 +42,21 @@ static void PrintGPFInfo(System::PanicFrame*& frame) { PrintBoxedDec(kerr, "Error Code", gpf->GeneralProtectionFaultError, BoxWidth); } +// Link-time base of the kernel image (see linker-x86_64.ld). +static constexpr uint64_t KernelBaseVirt = 0xFFFFFFFF80000000ULL; + static void PrintRegisters(System::PanicFrame* frame) { PrintBoxedSeparator(kerr, BoxWidth); PrintBoxedLine(kerr, "Registers:", BoxWidth, true); PrintBoxedHex(kerr, "Instruction Pointer", frame->IP, BoxWidth); + + // The absolute address means nothing without the link map. Its offset from + // the kernel's load base is what `nm bin-x86_64/kernel` can be searched + // for directly, so print that next to it. + if (frame->IP >= KernelBaseVirt) + PrintBoxedHex(kerr, " kernel+", frame->IP - KernelBaseVirt, BoxWidth); + + PrintBoxedHex(kerr, "Code Segment", frame->CS, BoxWidth); PrintBoxedHex(kerr, "Flags", frame->Flags, BoxWidth); PrintBoxedHex(kerr, "Stack Pointer", frame->SP, BoxWidth); diff --git a/kernel/src/Memory/Paging.cpp b/kernel/src/Memory/Paging.cpp index 980bfc6..3d75e68 100644 --- a/kernel/src/Memory/Paging.cpp +++ b/kernel/src/Memory/Paging.cpp @@ -5,6 +5,14 @@ #include namespace Memory::VMM { + + // Top of the contiguous HHDM mapping, published by Init() for callers that + // must validate a firmware-supplied physical address before dereferencing + // it through the HHDM. + static std::uint64_t g_hhdmLimit = 0; + + std::uint64_t Paging::HhdmLimit() { return g_hhdmLimit; } + Paging* g_paging = nullptr; static constexpr uint64_t LeafAddressMask = 0x000FFFFFFFFFF000ULL; @@ -88,6 +96,7 @@ namespace Memory::VMM { if (entryEnd > maxPhysAddr) maxPhysAddr = entryEnd; } maxPhysAddr = (maxPhysAddr + 0xFFF) & ~0xFFFULL; + g_hhdmLimit = maxPhysAddr; for (uint64_t pageAddr = 0; pageAddr < maxPhysAddr; pageAddr += 0x1000) { Map(pageAddr, HHDM(pageAddr)); diff --git a/kernel/src/Memory/Paging.hpp b/kernel/src/Memory/Paging.hpp index 766c9cb..8d82d52 100644 --- a/kernel/src/Memory/Paging.hpp +++ b/kernel/src/Memory/Paging.hpp @@ -90,6 +90,14 @@ public: Paging(); void Init(std::uint64_t kernelBaseVirt, std::uint64_t kernelSize, const montauk::boot::MemoryMap& memMap, const montauk::boot::Framebuffer& framebuffer); + // Top of the contiguous HHDM mapping built by Init(). A physical + // address at or above this is NOT reachable at phys+hhdmBase, and + // dereferencing it there faults (non-canonical addresses raise #GP + // rather than #PF). Anything that turns firmware-supplied numbers into + // HHDM pointers -- the AML interpreter's SystemMemory regions above all + // -- must check against this first. + static std::uint64_t HhdmLimit(); + void Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress); void MapMMIO(std::uint64_t physicalAddress, std::uint64_t virtualAddress); void MapWC(std::uint64_t physicalAddress, std::uint64_t virtualAddress); diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index 7baffaf..c1e6953 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -18,7 +18,12 @@ namespace Sched { // every slot uniformly -- the primary/sibling distinction matters only // for resource ownership on exit. static constexpr int MaxProcesses = 256; - static constexpr uint64_t StackPages = 4; // 16 KiB kernel stack per thread + // 64 KiB kernel stack per thread. The AML interpreter is the driver of + // this size: it runs on the calling thread's stack when a syscall asks for + // battery status, and firmware methods nest deeply through frames that + // carry 256-byte path buffers and 272-byte AML objects. See + // AML::StackBudget, which must stay comfortably under this. + static constexpr uint64_t StackPages = 16; static constexpr uint64_t StackSize = StackPages * 0x1000; static constexpr uint64_t UserStackPages = 8; // 32 KiB user stack (main thread, eagerly mapped) static constexpr uint64_t UserStackSize = UserStackPages * 0x1000; diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 94965c4..3cb63a4 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -256,6 +256,9 @@ namespace montauk::abi { // in one at a time. For buffers the caller is about to touch in full. static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure + /* Battery.hpp -- ACPI battery status */ + static constexpr uint64_t SYS_BATTERYINFO = 188; // (BatteryInfo*) -> 0, -1 no battery device + /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; @@ -808,6 +811,26 @@ namespace montauk::abi { uint64_t pageSize; }; + // Battery status (returned by SYS_BATTERYINFO). A machine with no battery + // device fails the call outright; `present` distinguishes a laptop with the + // pack removed from one that is running on it. + struct BatteryInfo { + uint8_t present; // a battery is installed + uint8_t charging; + uint8_t discharging; + uint8_t critical; + uint8_t acOnline; // AC adapter connected + uint8_t hasAcAdapter; // an AC adapter device exists at all + uint8_t percent; // 0..100, 0 when capacity is unknown + uint8_t powerUnitMilliAmps; // capacities are mAh/mA, not mWh/mW + uint32_t remainingCapacity; + uint32_t fullCapacity; // last full charge capacity + uint32_t designCapacity; + uint32_t presentRate; // mW or mA, 0 when unknown + uint32_t voltageMv; + int32_t minutesRemaining; // -1 when not estimable (e.g. charging) + }; + // Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT) struct CrashReportInfo { int pid; diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index 0783747..54b948c 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -217,6 +217,16 @@ struct DesktopState { uint64_t wifi_status_time; + // Battery indicator. Absent entirely on a machine with no ACPI battery + // device, which is how a desktop keeps a clean panel. + SvgIcon icon_battery; + char battery_icon_name[64]; // which glyph icon_battery holds + bool battery_present; // a battery device exists + bool battery_popup_open; + Rect battery_icon_rect; + montauk::abi::BatteryInfo battery_info; + uint64_t battery_last_poll; + bool vol_popup_open; Rect vol_icon_rect; int vol_level; // 0-100 diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index 2e33d42..49b3066 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -211,6 +211,7 @@ extern "C" { #define MTK_SYS_SPAWN_CAPS 185 #define MTK_SYS_SPAWN_REDIR_CAPS 186 #define MTK_SYS_ALLOC_EAGER 187 +#define MTK_SYS_BATTERYINFO 188 /* @SYSCALLS-END */ #define MTK_SOCK_TCP 1 diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 4e32286..5bdee4b 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -639,6 +639,13 @@ namespace montauk { return (int)syscall1(montauk::abi::SYS_BTINFO, (uint64_t)buf); } + // ACPI battery status. Returns 0 on success, or -1 on a machine with no + // battery device at all -- which is the signal not to draw an indicator. + // A laptop with the pack removed still returns 0, with present == 0. + inline int battery_info(montauk::abi::BatteryInfo* out) { + return (int)syscall1(montauk::abi::SYS_BATTERYINFO, (uint64_t)out); + } + // Wi-Fi. scan() runs a full channel sweep and blocks until it finishes or // timeoutMs elapses, then fills buf with the networks seen; it returns the // number of entries written, or -1 when no adapter is ready. diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index ad5cf88..c793e99 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 compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp font_data.cpp stb_truetype_impl.cpp +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 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/battery.cpp b/programs/src/desktop/battery.cpp new file mode 100644 index 0000000..179b28f --- /dev/null +++ b/programs/src/desktop/battery.cpp @@ -0,0 +1,238 @@ +/* + * battery.cpp + * Panel battery indicator: the icon that only appears on a machine with a + * battery, and the popout that puts a number on it. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "desktop_internal.hpp" +#include + +using namespace gui; + +// ============================================================================ +// Geometry +// ============================================================================ + +static constexpr int BATT_POPUP_W = 210; +static constexpr int BATT_POLL_MS = 5000; // the kernel caches for 2s anyway + +static int battery_popup_x(const DesktopState* ds) { + int x = ds->battery_icon_rect.x + ds->battery_icon_rect.w - BATT_POPUP_W; + return x < 4 ? 4 : x; +} + +// Rows below the headline: state, then whatever of capacity/time we can say. +static int battery_detail_rows(const DesktopState* ds) { + const montauk::abi::BatteryInfo& b = ds->battery_info; + if (!b.present) return 0; + + int rows = 2; // "Charge", then "State" + if (b.minutesRemaining >= 0) rows++; // "Time left" + if (b.fullCapacity > 0) rows++; // "Capacity" + return rows; +} + +static int battery_popup_h(const DesktopState* ds) { + int fh = system_font_height(); + int header = fh + 10; // title row + int rows = battery_detail_rows(ds) * (fh + 6); + return 10 + header + rows + 12; +} + +static Rect battery_popup_rect(const DesktopState* ds) { + return {battery_popup_x(ds), PANEL_HEIGHT + 2, BATT_POPUP_W, battery_popup_h(ds)}; +} + +// ============================================================================ +// Icon selection +// ============================================================================ + +// The icon pack ships one glyph per 10% step. Round to the nearest step so the +// drawn level matches the number in the popout rather than always sitting a +// bucket low. +static int battery_bucket(int percent) { + if (percent < 0) percent = 0; + if (percent > 100) percent = 100; + int bucket = ((percent + 5) / 10) * 10; + return bucket > 100 ? 100 : bucket; +} + +static void battery_icon_path(const DesktopState* ds, char* out, int cap) { + const montauk::abi::BatteryInfo& b = ds->battery_info; + + if (!b.present) { + montauk::strncpy(out, "0:/icons/battery-missing-symbolic.svg", cap); + return; + } + + int bucket = battery_bucket(b.percent); + + // A full battery on the charger gets the "charged" glyph; the pack has no + // 100% charging variant, and "charged" is what that state means anyway. + if (b.charging && bucket >= 100) { + montauk::strncpy(out, "0:/icons/battery-level-100-charged-symbolic.svg", cap); + return; + } + + snprintf(out, cap, "0:/icons/battery-level-%d%s-symbolic.svg", + bucket, b.charging ? "-charging" : ""); +} + +// Reload the glyph only when the one we want changes: at a 5 second poll a +// laptop crosses a bucket boundary every few minutes at most, and each load +// rasterises an SVG. +static bool battery_refresh_icon(DesktopState* ds) { + char path[64]; + battery_icon_path(ds, path, sizeof(path)); + + if (ds->icon_battery.pixels && montauk::streq(path, ds->battery_icon_name)) + return false; + + SvgIcon loaded = svg_load(path, 16, 16, colors::PANEL_TEXT); + if (!loaded.pixels) return false; + + svg_free(ds->icon_battery); + ds->icon_battery = loaded; + montauk::strncpy(ds->battery_icon_name, path, sizeof(ds->battery_icon_name)); + return true; +} + +// ============================================================================ +// Startup and polling +// ============================================================================ + +void desktop_battery_init(DesktopState* ds) { + ds->icon_battery = {nullptr, 0, 0}; + ds->battery_icon_name[0] = '\0'; + ds->battery_popup_open = false; + ds->battery_icon_rect = {0, 0, 0, 0}; + ds->battery_last_poll = 0; + montauk::memset(&ds->battery_info, 0, sizeof(ds->battery_info)); + + // A machine with no ACPI battery device fails the call outright, and the + // indicator never appears. This does not change while we are running, so + // unlike Wi-Fi there is nothing to keep looking for. + ds->battery_present = montauk::battery_info(&ds->battery_info) == 0; + if (ds->battery_present) battery_refresh_icon(ds); +} + +bool desktop_battery_poll(DesktopState* ds, uint64_t now) { + if (!ds->battery_present) return false; + if (ds->battery_last_poll != 0 && now - ds->battery_last_poll < BATT_POLL_MS) + return false; + ds->battery_last_poll = now; + + montauk::abi::BatteryInfo fresh; + if (montauk::battery_info(&fresh) != 0) return false; + + bool changed = fresh.percent != ds->battery_info.percent + || fresh.present != ds->battery_info.present + || fresh.charging != ds->battery_info.charging + || fresh.acOnline != ds->battery_info.acOnline + || fresh.minutesRemaining != ds->battery_info.minutesRemaining; + + ds->battery_info = fresh; + if (battery_refresh_icon(ds)) changed = true; + + // While the popout is up its numbers are on screen, so any change at all + // has to be repainted. + return changed || ds->battery_popup_open; +} + +// ============================================================================ +// Popout +// ============================================================================ + + +void desktop_draw_battery_popup(DesktopState* ds) { + Framebuffer& fb = ds->fb; + const montauk::abi::BatteryInfo& b = ds->battery_info; + Rect popup = battery_popup_rect(ds); + int fh = system_font_height(); + Color dim = Color::from_rgb(0x66, 0x66, 0x66); + + 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 lx = popup.x + 14; + int vx = popup.x + 92; + int ty = popup.y + 10; + + draw_text(fb, lx, ty, "Battery", colors::TEXT_COLOR); + + // A coloured dot mirrors the Wi-Fi popup: green on the charger, amber when + // the charge is getting short, red once the firmware calls it critical. + const char* status_str; + Color dot_color; + if (!b.present) { + status_str = "Not present"; + dot_color = Color::from_rgb(0x88, 0x88, 0x88); + } else if (b.charging) { + status_str = "Charging"; + dot_color = Color::from_rgb(0x4C, 0xAF, 0x50); + } else if (b.critical || b.percent <= 10) { + status_str = "Low"; + dot_color = Color::from_rgb(0xCC, 0x33, 0x33); + } else if (b.acOnline) { + status_str = "Plugged in"; + dot_color = Color::from_rgb(0x4C, 0xAF, 0x50); + } else { + status_str = "On battery"; + dot_color = (b.percent <= 25) ? Color::from_rgb(0xD0, 0x9A, 0x2E) + : Color::from_rgb(0x66, 0x66, 0x66); + } + + int sw = text_width(status_str); + int status_x = popup.x + popup.w - 14 - sw; + fill_circle(fb, status_x - 9, ty + fh / 2, 4, dot_color); + draw_text(fb, status_x, ty, status_str, dim); + + ty += fh + 10; + + if (!b.present) return; + + char value[40]; + + snprintf(value, sizeof(value), "%d%%", (int)b.percent); + draw_text(fb, lx, ty, "Charge", dim); + draw_text(fb, vx, ty, value, colors::TEXT_COLOR); + ty += fh + 6; + + snprintf(value, sizeof(value), "%s", + b.charging ? "Charging" : (b.acOnline ? "Not charging" : "Discharging")); + draw_text(fb, lx, ty, "State", dim); + draw_text(fb, vx, ty, value, colors::TEXT_COLOR); + ty += fh + 6; + + if (b.minutesRemaining >= 0) { + int mins = b.minutesRemaining; + if (mins >= 60) snprintf(value, sizeof(value), "%dh %02dm", mins / 60, mins % 60); + else snprintf(value, sizeof(value), "%dm", mins); + draw_text(fb, lx, ty, "Time left", dim); + draw_text(fb, vx, ty, value, colors::TEXT_COLOR); + ty += fh + 6; + } + + if (b.fullCapacity > 0) { + const char* unit = b.powerUnitMilliAmps ? "mAh" : "mWh"; + snprintf(value, sizeof(value), "%u/%u %s", + (unsigned)b.remainingCapacity, (unsigned)b.fullCapacity, unit); + draw_text(fb, lx, ty, "Capacity", dim); + draw_text(fb, vx, ty, value, colors::TEXT_COLOR); + } +} + +// A click anywhere outside the popout closes it; the popout itself has nothing +// to click, so it only has to swallow the event. +bool desktop_battery_handle_mouse(DesktopState* ds, int mx, int my) { + if (!ds->battery_popup_open) return false; + + if (battery_popup_rect(ds).contains(mx, my)) return true; + + if (!ds->battery_icon_rect.contains(mx, my)) + ds->battery_popup_open = false; + return false; +} diff --git a/programs/src/desktop/compose.cpp b/programs/src/desktop/compose.cpp index 6962e06..90b1637 100644 --- a/programs/src/desktop/compose.cpp +++ b/programs/src/desktop/compose.cpp @@ -268,6 +268,11 @@ void gui::desktop_compose(DesktopState* ds) { desktop_draw_wifi_popup(ds); } + // Draw battery popup if open + if (ds->battery_popup_open) { + desktop_draw_battery_popup(ds); + } + // Draw volume popup if open if (ds->vol_popup_open) { desktop_draw_vol_popup(ds); diff --git a/programs/src/desktop/desktop_internal.hpp b/programs/src/desktop/desktop_internal.hpp index a7dd5ce..d939009 100644 --- a/programs/src/desktop/desktop_internal.hpp +++ b/programs/src/desktop/desktop_internal.hpp @@ -195,6 +195,12 @@ void desktop_draw_wifi_popup(gui::DesktopState* ds); bool desktop_wifi_handle_mouse(gui::DesktopState* ds, int mx, int my, bool left_pressed, int scroll); +// battery.cpp +void desktop_battery_init(gui::DesktopState* ds); +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); + // 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 17dfe47..e29258a 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -189,6 +189,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->vol_popup_open = false; ds->net_popup_open = false; ds->wifi_popup_open = false; + ds->battery_popup_open = false; return; } @@ -529,6 +530,13 @@ void gui::desktop_handle_mouse(DesktopState* ds) { return; } + // Battery popout: it has no controls, so it only swallows clicks that land + // inside it and closes on anything else. + if (ds->battery_popup_open && left_pressed) { + if (desktop_battery_handle_mouse(ds, mx, my)) + return; + } + // Handle net popup clicks if (ds->net_popup_open && left_pressed) { int popup_w = 220; @@ -545,6 +553,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { } else if (!ds->net_icon_rect.contains(mx, my)) { ds->net_popup_open = false; ds->wifi_popup_open = false; + ds->battery_popup_open = false; } } @@ -555,6 +564,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->app_menu_open = !ds->app_menu_open; ds->net_popup_open = false; ds->wifi_popup_open = false; + ds->battery_popup_open = false; ds->vol_popup_open = false; gui::mtk::context_menu_close(ds->ctx_menu); return; @@ -571,6 +581,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->app_menu_open = false; ds->net_popup_open = false; ds->wifi_popup_open = false; + ds->battery_popup_open = false; ds->vol_popup_open = false; gui::mtk::context_menu_close(ds->ctx_menu); return; @@ -583,6 +594,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->app_menu_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; } @@ -593,6 +605,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->app_menu_open = false; ds->vol_popup_open = false; ds->wifi_popup_open = false; + ds->battery_popup_open = false; gui::mtk::context_menu_close(ds->ctx_menu); return; } @@ -603,9 +616,22 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->app_menu_open = false; ds->vol_popup_open = false; ds->net_popup_open = false; + ds->battery_popup_open = false; gui::mtk::context_menu_close(ds->ctx_menu); return; } + + // Battery icon + if (ds->battery_icon_rect.w > 0 && ds->battery_icon_rect.contains(mx, my)) { + ds->battery_popup_open = !ds->battery_popup_open; + ds->app_menu_open = false; + ds->vol_popup_open = false; + ds->net_popup_open = false; + ds->wifi_popup_open = false; + gui::mtk::context_menu_close(ds->ctx_menu); + return; + } + // Window indicator buttons int indicator_x = 40; for (int i = 0; i < ds->window_count; i++) { @@ -833,6 +859,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ds->app_menu_open = false; ds->net_popup_open = false; ds->wifi_popup_open = false; + ds->battery_popup_open = false; ds->vol_popup_open = false; } } diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 0afd20f..83fef69 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -392,6 +392,7 @@ void gui::desktop_init(DesktopState* ds) { desktop_refresh_netifs(ds); desktop_wifi_init(ds); + desktop_battery_init(ds); ds->vol_popup_open = false; ds->vol_icon_rect = {0, 0, 0, 0}; @@ -518,6 +519,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; // 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 8356c49..2da3b0e 100644 --- a/programs/src/desktop/panel.cpp +++ b/programs/src/desktop/panel.cpp @@ -210,6 +210,33 @@ void gui::desktop_draw_panel(DesktopState* ds) { } else { ds->wifi_icon_rect = {0, 0, 0, 0}; } + + // Battery, leftmost of the status icons. Only a machine that reported a + // battery device at startup gets one; a desktop shows nothing here. + if (ds->battery_present && ds->icon_battery.pixels) { + ds->battery_icon_rect = {next_icon_x, icon_y, 16, 16}; + next_icon_x -= 16 + 10; + + // The glyph already encodes level and charging state, and the charging + // variants carry their own green, so it is blitted as loaded. A + // critical battery is the one case worth shouting about. + if (ds->battery_info.present && ds->battery_info.critical + && !ds->battery_info.charging) { + uint32_t* src = ds->icon_battery.pixels; + uint32_t tinted[256]; + for (int p = 0; p < 16 * 16; p++) { + uint8_t a = (src[p] >> 24) & 0xFF; + tinted[p] = ((uint32_t)a << 24) | 0x00CC3333; + } + fb.blit_alpha(ds->battery_icon_rect.x, icon_y, 16, 16, tinted); + } else { + fb.blit_alpha(ds->battery_icon_rect.x, icon_y, + ds->icon_battery.width, ds->icon_battery.height, + ds->icon_battery.pixels); + } + } else { + ds->battery_icon_rect = {0, 0, 0, 0}; + } } // ============================================================================ diff --git a/programs/src/login/login_shutdown.cpp b/programs/src/login/login_shutdown.cpp index 5aa4dae..0897014 100644 --- a/programs/src/login/login_shutdown.cpp +++ b/programs/src/login/login_shutdown.cpp @@ -186,18 +186,26 @@ static bool path_is_file(const char* path) { #define LOG_READ_SIZE 65536 -// Write the system log to a file on disk +// Write the system log to a file on disk. +// +// 0:/state is the system's mutable runtime state: generated by the system, +// kept across boots, and never part of the system image. The log used to live +// in 0:/os/logs, which the protected-path table covers as CAP_SYSTEM_IMAGE +// (Fs/ProtectedPaths.cpp) -- nothing is ever granted that capability, so every +// create, write and delete below failed silently and the log froze at whatever +// an earlier build had written. void stage_save_log() { - // Edge case - 0:/os/logs exists and is a file - if (path_is_file("0:/os/logs")) + // Edge cases - 0:/state or 0:/state/logs exists and is a file + if (path_is_file("0:/state") || path_is_file("0:/state/logs")) return; - montauk::fmkdir("0:/os/logs"); + montauk::fmkdir("0:/state"); + montauk::fmkdir("0:/state/logs"); // Clear any existing montaukos log - montauk::fdelete("0:/os/logs/montaukos"); + montauk::fdelete("0:/state/logs/montaukos"); - int handle = montauk::fcreate("0:/os/logs/montaukos"); + int handle = montauk::fcreate("0:/state/logs/montaukos"); if (handle < 0) return; diff --git a/programs/src/power/main.cpp b/programs/src/power/main.cpp index dcb77d9..a3012c1 100644 --- a/programs/src/power/main.cpp +++ b/programs/src/power/main.cpp @@ -95,6 +95,63 @@ static void print_snapshot(const montauk::abi::PowerInfo& info) { print("\n"); } +// Battery is reported independently of the CPU power features: a laptop under +// emulation has neither, but a real one may well expose a battery and no HWP. +static void print_battery() { + montauk::abi::BatteryInfo b{}; + if (montauk::battery_info(&b) != 0) { + print("Battery: none (no ACPI battery device)\n"); + return; + } + + if (!b.present) { + print("Battery: slot present, no pack installed"); + if (b.hasAcAdapter) print(b.acOnline ? " (on AC)" : " (no AC)"); + print("\n"); + return; + } + + print("Battery: "); + put_u64(b.percent); + print("%"); + + if (b.charging) print(", charging"); + else if (b.acOnline) print(", on AC, not charging"); + else print(", discharging"); + if (b.critical) print(", CRITICAL"); + print("\n"); + + if (b.fullCapacity > 0) { + print(" Capacity: "); + put_u64(b.remainingCapacity); + print(" of "); + put_u64(b.fullCapacity); + print(b.powerUnitMilliAmps ? " mAh (design " : " mWh (design "); + put_u64(b.designCapacity); + print(")\n"); + } + + if (b.presentRate > 0) { + print(" Rate: "); + put_u64(b.presentRate); + print(b.powerUnitMilliAmps ? " mA\n" : " mW\n"); + } + + if (b.minutesRemaining >= 0) { + print(" Time left: "); + put_u64((uint64_t)(b.minutesRemaining / 60)); + print("h "); + put_u64((uint64_t)(b.minutesRemaining % 60)); + print("m\n"); + } + + if (b.voltageMv > 0) { + print(" Voltage: "); + put_u64(b.voltageMv); + print(" mV\n"); + } +} + static void print_watch_line(const montauk::abi::PowerInfo& info, uint64_t t) { put_u64(t); print("s "); @@ -124,13 +181,16 @@ extern "C" void _start() { montauk::abi::PowerInfo info{}; if (!read_info(info)) { print("power: no CPU power management available\n"); - print("(non-Intel CPU or running under emulation)\n"); + print("(non-Intel CPU or running under emulation)\n\n"); + print_battery(); montauk::exit(1); } bool watch = rest[0] == 'w'; if (!watch) { print_snapshot(info); + print("\n"); + print_battery(); montauk::exit(0); } diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index 29e061a..634611a 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -48,6 +48,31 @@ ICONS=( "devices/symbolic/computer-symbolic.svg" "devices/symbolic/network-wired-symbolic.svg" "devices/symbolic/network-wireless-symbolic.svg" + # Panel battery indicator: one icon per 10% bucket, plus the + # charging variants, the charged state and the empty-slot glyph. + "status/symbolic/battery-level-0-symbolic.svg" + "status/symbolic/battery-level-0-charging-symbolic.svg" + "status/symbolic/battery-level-10-symbolic.svg" + "status/symbolic/battery-level-10-charging-symbolic.svg" + "status/symbolic/battery-level-20-symbolic.svg" + "status/symbolic/battery-level-20-charging-symbolic.svg" + "status/symbolic/battery-level-30-symbolic.svg" + "status/symbolic/battery-level-30-charging-symbolic.svg" + "status/symbolic/battery-level-40-symbolic.svg" + "status/symbolic/battery-level-40-charging-symbolic.svg" + "status/symbolic/battery-level-50-symbolic.svg" + "status/symbolic/battery-level-50-charging-symbolic.svg" + "status/symbolic/battery-level-60-symbolic.svg" + "status/symbolic/battery-level-60-charging-symbolic.svg" + "status/symbolic/battery-level-70-symbolic.svg" + "status/symbolic/battery-level-70-charging-symbolic.svg" + "status/symbolic/battery-level-80-symbolic.svg" + "status/symbolic/battery-level-80-charging-symbolic.svg" + "status/symbolic/battery-level-90-symbolic.svg" + "status/symbolic/battery-level-90-charging-symbolic.svg" + "status/symbolic/battery-level-100-symbolic.svg" + "status/symbolic/battery-level-100-charged-symbolic.svg" + "status/symbolic/battery-missing-symbolic.svg" "apps/symbolic/web-browser-symbolic.svg" # Scalable (colorful) icons for app menu "apps/scalable/utilities-terminal.svg"