feat: power down dGPUs on hybrid graphics laptops

This commit is contained in:
daniel committed 2026-10-01 13:09:26 +02:00
1 parent 7f12e1a150
commit 24f88ec4c6
23 files changed
+4325 -1130

No files matched your search

File diff suppressed because it is too large. Load diff
+204 -30
View File
@@ -97,6 +97,35 @@ namespace Hal {
static constexpr uint8_t ConcatResOp = 0x84;
static constexpr uint8_t ToHexStringOp = 0x98;
static constexpr uint8_t ToDecimalStringOp = 0x97;
static constexpr uint8_t ExternalOp = 0x15;
static constexpr uint8_t RefOfOp = 0x71;
static constexpr uint8_t FindSetLeftBitOp = 0x81;
static constexpr uint8_t FindSetRightBitOp = 0x82;
static constexpr uint8_t NotifyOp = 0x86;
static constexpr uint8_t MatchOp = 0x89;
static constexpr uint8_t ObjectTypeOp = 0x8E;
static constexpr uint8_t CreateQWordFieldOp = 0x8F;
static constexpr uint8_t ToStringOp = 0x9C;
static constexpr uint8_t CopyObjectOp = 0x9D;
static constexpr uint8_t MidOp = 0x9E;
static constexpr uint8_t ContinueOp = 0x9F;
static constexpr uint8_t BreakPointOp = 0xCC;
// More extended opcodes (after ExtOpPrefix)
static constexpr uint8_t CondRefOfOp = 0x12;
static constexpr uint8_t CreateFieldOp = 0x13;
static constexpr uint8_t LoadTableOp = 0x1F;
static constexpr uint8_t LoadOp = 0x20;
static constexpr uint8_t SignalOp = 0x24;
static constexpr uint8_t WaitOp = 0x25;
static constexpr uint8_t ResetOp = 0x26;
static constexpr uint8_t FromBCDOp = 0x28;
static constexpr uint8_t ToBCDOp = 0x29;
static constexpr uint8_t UnloadOp = 0x2A;
static constexpr uint8_t DebugOp = 0x31;
static constexpr uint8_t FatalOp = 0x32;
static constexpr uint8_t TimerOp = 0x33;
static constexpr uint8_t DataRegionOp = 0x88;
// ============================================================================
@@ -104,7 +133,11 @@ namespace Hal {
// ============================================================================
static constexpr int MaxCallDepth = 16;
static constexpr int MaxLoopIterations = 1024;
// ACPICA's ceiling. Firmware polls hardware in While loops with a
// Sleep or Stall in the body (link training, power-good, L2/L3 ready)
// and expects more than a token number of passes.
static constexpr int MaxLoopIterations = 0xFFFF;
// Backstop cap on evaluation nesting. This counter is shared by
// expression evaluation AND block execution, and block nesting is
@@ -133,8 +166,10 @@ namespace Hal {
public:
Interpreter();
// Parse a DSDT or SSDT table into the namespace.
// tableData points to the CommonSDTHeader (HHDM-mapped).
// Parse a DSDT or SSDT table into the namespace. Tables may be
// loaded one after another; each method remembers which table
// its body lives in. tableData points to the CommonSDTHeader
// (HHDM-mapped) and must stay mapped.
bool LoadTable(void* tableData);
// Evaluate a named object, returning its value.
@@ -144,6 +179,28 @@ namespace Hal {
// Evaluate a method with arguments.
bool EvaluateMethod(const char* path, const Object* args, int argCount, Object& result);
// Evaluate a node: a method is run with `args`, a field is read,
// anything else hands back its value (non-data objects as a
// Reference to the node).
bool EvaluateNode(int32_t node, const Object* args, int argCount, Object& result);
// Find the child of `parent` named `seg` ("_PS3", "_ON"), -1 if
// there is none. Short names are padded with '_'.
int32_t FindChild(int32_t parent, const char* seg) const;
// EvaluateNode() on a named child. False when it does not exist.
bool EvaluateChild(int32_t parent, const char* seg, const Object* args,
int argCount, Object& result);
// Turn a Reference, or a NameRef/String naming an object relative
// to `scopeNode`, into a node index. -1 when it names nothing.
int32_t ResolveReference(const Object& obj, int32_t scopeNode) const;
// The PCI function a Device node describes: its _ADR on the bus
// behind its parent (a root bridge's _BBN, or a PCI-to-PCI
// bridge's secondary bus). False when it cannot be placed.
bool GetPciAddress(int32_t deviceNode, uint8_t& bus, uint8_t& dev, uint8_t& fn);
// Read a field value. Returns integer.
bool ReadField(int32_t nodeIndex, uint64_t& value);
@@ -157,6 +214,10 @@ namespace Hal {
// Check if the interpreter has been initialized
bool IsInitialized() const { return m_initialized; }
// Nodes [0, DsdtNodeCount()) came from the DSDT. Nodes past it were
// added by SSDTs (or by methods run since).
int32_t DsdtNodeCount() const { return m_dsdtNodeCount; }
// 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.
@@ -170,8 +231,10 @@ namespace Hal {
// 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.
// it was declared with. A name in a constant package comes back as
// a NameRef unless it resolves (from the root) to plain data, in
// which case its value is returned. 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.
@@ -183,17 +246,35 @@ namespace Hal {
int32_t MaterializeNodePackage(int32_t nodeIndex);
private:
struct ExecContext;
// ============================================================================
// Parsing (table load)
// ============================================================================
// ctx is the method being executed when a definition appears in
// a method body, nullptr at table load. Operands that are TermArgs
// (region offsets, buffer sizes, bank values) are evaluated in it.
bool ParseBlock(const uint8_t* aml, uint32_t offset, uint32_t endOffset,
int32_t scopeNode);
bool ParseNamedObject(const uint8_t* aml, uint32_t* pos, uint32_t endOffset,
int32_t scopeNode);
int32_t scopeNode, ExecContext* ctx = nullptr);
bool ParseExtendedOp(const uint8_t* aml, uint32_t* pos, uint32_t endOffset,
int32_t scopeNode);
int32_t scopeNode, ExecContext* ctx = nullptr);
// The FieldList shared by Field, IndexField and BankField.
void ParseFieldList(const uint8_t* aml, uint32_t* pos, uint32_t fieldEnd,
int32_t scopeNode, uint8_t fieldFlags, const Object& proto);
// Evaluate a TermArg operand of a definition: in the running
// method, or in the table-load context.
bool EvalOperand(ExecContext* ctx, const uint8_t* aml, uint32_t* pos,
uint32_t endOffset, int32_t scopeNode, Object& result);
// CreateBitField .. CreateQWordField and CreateField.
bool CreateBufferField(ExecContext& ctx, uint32_t* pos, uint32_t endOffset,
uint8_t op);
// ============================================================================
@@ -214,6 +295,10 @@ namespace Hal {
char* outRelative = nullptr, int relLen = 0,
bool* outSearchable = nullptr);
// Read a NameString and resolve it with the search rule. -1 when
// it names nothing.
int32_t ReadAndResolveName(const uint8_t* aml, uint32_t* pos, int32_t scopeNode);
// Read a single 4-char NameSeg from AML. Advances *pos.
void ReadNameSeg(const uint8_t* aml, uint32_t* pos, char* outSeg);
@@ -225,11 +310,10 @@ 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.
// advance *pos past it. Names come back as NameRef; locals and
// anything else that needs a running method decode to integer 0.
bool DecodeConstElement(const uint8_t* data, uint32_t length,
uint32_t* pos, Object& out);
uint32_t* pos, Object& out) const;
uint64_t DecodeInteger(const uint8_t* aml, uint32_t* pos);
// ============================================================================
@@ -247,6 +331,8 @@ namespace Hal {
Object ReturnValue;
bool Returned;
bool Broken;
bool Continued;
bool Aborted; // undecodable AML: unwind the method
int Depth; // method-call nesting
int ExprDepth; // expression/block nesting
};
@@ -259,36 +345,117 @@ namespace Hal {
~DepthGuard() { Value--; }
};
// Where a Store lands.
struct Target {
enum class Kind : uint8_t { None, Node, Local, Arg, PkgElement, BufByte };
Kind What = Kind::None;
int32_t Node = -1;
int Index = 0; // local/arg number, or element index
int32_t PkgStore = -1;
Object* Buf = nullptr; // BufByte: the buffer being indexed
};
bool ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset);
bool ExecuteOpcode(ExecContext& ctx, uint32_t* pos, uint32_t endOffset);
// Evaluate a term (expression that produces a value) within an execution context.
bool EvalTerm(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
// 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,
int32_t& pkgStore, int& pkgElement);
// Extended (0x5B-prefixed) opcodes in expression or statement position.
bool EvalExtOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
// 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,
int32_t pkgStore = -1, int pkgElement = 0);
// EvalTerm's opcode handlers. They live out of line so the
// recursive path only carries the locals of the one running.
bool EvalBufferOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalPackageOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalBinary(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalUnary(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalDivide(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalStoreOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalReference(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalIndexOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalMatchOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalMidOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalNameRef(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool CallMethod(ExecContext& ctx, int32_t node, uint32_t* pos, uint32_t endOffset,
Object& result);
bool IndexElement(const Object& source, uint64_t idx, Object& out);
void ReportUnresolved(ExecContext& ctx, uint32_t namePos);
// Target forms that need operand evaluation of their own.
bool EvalIndexTarget(ExecContext& ctx, uint32_t* pos, Target& target);
bool EvalDerefTarget(ExecContext& ctx, uint32_t* pos, Target& target);
// A package element: like EvalTerm, except that a name is a
// reference to the object rather than a call or a read.
bool EvalPackageElement(ExecContext& ctx, uint32_t* pos, uint32_t endOffset,
Object& result);
// Parse a SuperName/Target operand.
bool EvalTarget(ExecContext& ctx, uint32_t* pos, Target& target);
// Store a value to a target, with ACPI's implicit conversion for
// named data objects.
void StoreToTarget(ExecContext& ctx, const Object& value, const Target& target);
// Parse a Target operand and store `value` there.
void StoreResult(ExecContext& ctx, uint32_t* pos, const Object& value);
// The current value behind a target (for Increment and friends).
Object ReadTarget(ExecContext& ctx, const Target& target);
// Run a method node. Caller holds m_evalLock.
bool InvokeMethodNode(int32_t node, const Object* args, int argCount,
Object& result, int depth, int exprDepth);
// Run a method in a context already acquired and holding its
// arguments; releases the context.
bool RunMethodIn(ExecContext* ctx, int32_t node, int depth, int exprDepth,
Object& result);
// The value of a node as an operand: fields read, methods run
// without arguments, non-data objects become References.
bool NodeValue(int32_t node, Object& result, int depth, int exprDepth);
// Log an opcode we cannot decode and stop the method: carrying on
// past it would execute operand bytes as code.
bool Abort(ExecContext& ctx, uint32_t pos, const char* why);
// Native methods (\_OSI).
bool RunNativeMethod(uint8_t which, const Object* args, int argCount, Object& result);
// Install the OS-defined objects (\_OSI, \_REV, \_OS_, \_GL_).
void InstallPredefined();
// Device identification (for PCI_Config regions).
bool IntegerChild(int32_t device, const char* seg, uint64_t fallback,
uint64_t& value, int depth);
bool IsPciRootBridge(int32_t device, int depth);
bool ResolvePciAddress(int32_t node, uint8_t& bus, uint8_t& dev,
uint8_t& fn, int depth);
// ============================================================================
// Field I/O
// ============================================================================
bool ReadRegion(RegionSpace space, uint64_t address, uint32_t bitWidth, uint64_t& value);
bool WriteRegion(RegionSpace space, uint64_t address, uint32_t bitWidth, uint64_t value);
// Access `bits` (little-endian, bitLength long) of a field unit.
bool AccessField(int32_t fieldNode, bool write, uint8_t* bits);
bool ReadFieldObject(int32_t fieldNode, Object& value);
bool WriteFieldObject(int32_t fieldNode, const Object& value);
// One aligned access unit of a field's backing store.
bool FieldUnitIo(const Object& field, uint64_t byteOffset, uint32_t width,
bool write, uint64_t& value);
// Region access at byte `offset` into the region, `width` bytes.
bool ReadRegion(int32_t regionNode, uint64_t offset, uint32_t width, uint64_t& value);
bool WriteRegion(int32_t regionNode, uint64_t offset, uint32_t width, uint64_t value);
bool RegionIo(int32_t regionNode, uint64_t offset, uint32_t width,
bool write, uint64_t& value);
bool ReadBufferField(const Object& field, Object& value);
bool WriteBufferField(const Object& field, const Object& value);
// ============================================================================
@@ -310,16 +477,23 @@ namespace Hal {
ExecContext* AcquireContext();
void ReleaseContext();
// Run `body` as a top-level evaluation: serialized, with the stack
// budget anchored here.
template<typename Fn> bool TopLevel(Fn body);
Namespace m_ns;
const uint8_t* m_dsdt;
uint32_t m_dsdtLength;
bool m_initialized;
bool m_predefined;
int32_t m_dsdtNodeCount;
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
ExecContext* m_loadCtx; // table-load operand context
uint64_t m_delayedUs; // total Sleep/Stall time requested
int m_methodScan; // >0 while scanning a method body at load
kcp::Mutex m_evalLock;
};
+10
View File
@@ -89,6 +89,10 @@ namespace Hal {
s_transientNext = 0;
}
bool IsPersistent(int32_t index) {
return index > 0 && index <= MaxPersistent;
}
static Entry* Lookup(int32_t index) {
if (index <= 0) return nullptr;
int32_t i = index - 1;
@@ -293,6 +297,12 @@ namespace Hal {
return -1;
}
int32_t Namespace::FindChild(int32_t parentIndex, const char* seg) const {
char padded[MaxNameSegLen + 1];
PadSegment(seg, padded);
return FindChildByName(parentIndex, padded);
}
int32_t Namespace::CreateNode(const char* absolutePath) {
EnsureRoot();
char segments[MaxPathDepth][MaxNameSegLen + 1];
+66 -3
View File
@@ -31,6 +31,16 @@ namespace Hal {
ThermalZone,
PowerResource,
BufferField,
Event,
// A reference to a namespace node: what RefOf() yields, and what a
// name evaluates to when it names something that is not data (a
// device or power resource listed in a _PRx package, say).
Reference,
// A name inside a constant package, kept as written ("PG00",
// "\_SB.PCI0.PEG0") because its scope is only known to whoever
// reads the package. Interpreter::ResolveReference() turns it
// into a node.
NameRef,
};
// ============================================================================
@@ -63,7 +73,7 @@ namespace Hal {
static constexpr int InitialChildren = 8;
static constexpr int MaxStringLen = 64;
static constexpr int MaxBufferLen = 256;
static constexpr int MaxPackageElements = 32;
static constexpr int MaxPackageElements = 64;
static constexpr int MaxMethodArgs = 7;
static constexpr int MaxMethodLocals = 8;
@@ -102,10 +112,23 @@ namespace Hal {
uint32_t Length;
} Buffer;
// A package's constant form: its element bytes, left in the
// table they were declared in, and the declared element count. ACPI tables stay
// mapped for the life of the kernel, so pointing at them is
// both cheaper and free of the size cap a copy would carry --
// a GPIO community table runs well past MaxBufferLen.
struct {
const uint8_t* Data; // first element
uint32_t Length; // bytes of element encodings
uint32_t Count; // NumElements
} Package;
struct {
uint8_t ArgCount; // bits 0-2 of method flags
bool Serialized; // bit 3
uint32_t AmlOffset; // offset into DSDT AML where the method body starts
uint8_t Native; // NativeMethod; 0 = AML
const uint8_t* Table; // the table the body lives in
uint32_t AmlOffset; // offset into Table where the method body starts
uint32_t AmlLength; // length of the method body
} Method;
@@ -116,12 +139,33 @@ namespace Hal {
} Region;
struct {
uint32_t RegionNodeIndex; // index of the parent OperationRegion node
// Field / BankField: the OperationRegion node.
// IndexField: the index register field.
uint32_t RegionNodeIndex;
uint32_t BitOffset;
uint32_t BitLength;
uint8_t AccessType; // 0=Any, 1=Byte, 2=Word, 3=DWord, 4=QWord, 5=Buffer
uint8_t UpdateRule; // 0=Preserve, 1=WriteAsOnes, 2=WriteAsZeros
uint8_t Kind; // FieldKind
int32_t DataNodeIndex; // IndexField: the data register field
int32_t BankNodeIndex; // BankField: the bank select field
uint64_t BankValue; // BankField: value selecting this bank
} Field;
// CreateXxxField over a buffer. Source points at the object
// holding the buffer -- a namespace node's, or a local/arg slot
// of the method that created the field -- so reads and writes
// go through to it.
struct {
Object* Source;
uint32_t BitOffset;
uint32_t BitLength;
} BufferField;
struct {
int32_t Node;
} Reference;
struct {
uint8_t ProcId;
uint32_t PblkAddr;
@@ -132,6 +176,18 @@ namespace Hal {
Object() : Type(ObjectType::None), PkgIndex(-1), Integer(0) {}
};
enum class FieldKind : uint8_t {
Normal = 0,
Index = 1,
Bank = 2,
};
// Methods the OS provides rather than the firmware.
enum class NativeMethod : uint8_t {
None = 0,
Osi = 1, // \_OSI
};
// ============================================================================
// Package element store
@@ -155,6 +211,9 @@ namespace Hal {
// Rewind the transient ring. Called once per top-level evaluation.
void ResetTransient();
// True for an entry that outlives the evaluation that made it.
bool IsPersistent(int32_t index);
// Element access. Returns nullptr for an unknown entry or an
// out-of-range element.
Object* Element(int32_t index, int element);
@@ -219,6 +278,10 @@ namespace Hal {
// rebased onto a scope. Returns -1 if no scope contains it.
int32_t SearchUpward(const char* relative, int32_t scopeNodeIndex) const;
// Find the child of `parent` named `seg`; a short name is padded
// with '_' the way node names are stored. Returns -1 if absent.
int32_t FindChild(int32_t parentIndex, const char* seg) const;
// Get a node by index.
NamespaceNode* GetNode(int32_t index);
const NamespaceNode* GetNode(int32_t index) const;
+132 -1
View File
@@ -9,6 +9,8 @@
#include <ACPI/ACPI.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Heap.hpp>
using namespace Kt;
@@ -239,15 +241,144 @@ namespace Hal {
// Full interpreter initialization
// ============================================================================
void InitializeInterpreter(void* dsdtData) {
void InitializeInterpreter(void* dsdtData, ACPI::CommonSDTHeader* xsdt) {
auto& interp = GetInterpreter();
if (!interp.LoadTable(dsdtData)) {
KernelLogStream(ERROR, "AML") << "Failed to load DSDT into AML interpreter";
}
// SSDTs extend the DSDT's namespace and are loaded after it, in
// XSDT order. They are where most laptops keep CPU power tables,
// and -- the reason they matter here -- the discrete GPU's power
// control (_PR3 power resources, _PS3, NVIDIA/AMD _DSM).
int ssdts = 0;
if (xsdt) {
uint32_t entryCount = (xsdt->Length - sizeof(ACPI::CommonSDTHeader)) / 8;
auto* entries = (uint64_t*)((uint64_t)xsdt + sizeof(ACPI::CommonSDTHeader));
for (uint32_t i = 0; i < entryCount; i++) {
if (entries[i] == 0) continue;
auto* table = (ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
const char* sig = table->Signature;
bool isSsdt = sig[0] == 'S' && sig[1] == 'S' && sig[2] == 'D' && sig[3] == 'T';
bool isPsdt = sig[0] == 'P' && sig[1] == 'S' && sig[2] == 'D' && sig[3] == 'T';
if (!isSsdt && !isPsdt) continue;
if (table->Length <= sizeof(ACPI::CommonSDTHeader)) continue;
if (interp.LoadTable(table)) ssdts++;
}
}
KernelLogStream(OK, "AML") << "Loaded DSDT + " << base::dec << (uint64_t)ssdts
<< " SSDT(s)";
auto& ns = interp.GetNamespace();
KernelLogStream(OK, "AML") << "Namespace: " << base::dec
<< (uint64_t)ns.NodeCount() << " / " << (uint64_t)ns.MaxCapacity() << " nodes (dynamic)";
}
// ============================================================================
// Namespace initialization (_REG, _INI)
// ============================================================================
static bool SegIs(const char* name, const char* seg) {
return name[0] == seg[0] && name[1] == seg[1] &&
name[2] == seg[2] && name[3] == seg[3];
}
void InitializeNamespaceObjects() {
auto& interp = GetInterpreter();
if (!interp.IsInitialized()) return;
auto& ns = interp.GetNamespace();
// Collect first, evaluate second: executing AML creates nodes,
// which reallocates a parent's child array under a live walk.
int32_t capacity = ns.NodeCount();
auto* regs = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * capacity);
auto* devices = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * capacity);
if (!regs || !devices) {
if (regs) Memory::g_heap->Free(regs);
if (devices) Memory::g_heap->Free(devices);
return;
}
int regCount = 0, deviceCount = 0;
ns.WalkDescendants(ns.RootIndex(), ObjectType::None,
[&](int32_t idx, const NamespaceNode* node) {
if (node->Obj.Type == ObjectType::Method && SegIs(node->Name, "_REG") &&
regCount < capacity)
regs[regCount++] = idx;
if (node->Obj.Type == ObjectType::Device && deviceCount < capacity)
devices[deviceCount++] = idx;
});
// _REG(PCI_Config, connect). A _REG for another space checks Arg0
// and ignores the call, as with the EC's.
Object args[2]{};
args[0].Type = ObjectType::Integer;
args[0].Integer = (uint64_t)RegionSpace::PciConfig;
args[1].Type = ObjectType::Integer;
args[1].Integer = 1;
for (int i = 0; i < regCount; i++) {
Object result{};
interp.EvaluateNode(regs[i], args, 2, result);
}
// \_SB._INI runs before any device's.
int32_t sb = ns.FindNode("\\_SB_");
if (sb >= 0) {
Object result{};
if (interp.EvaluateChild(sb, "_INI", nullptr, 0, result))
KernelLogStream(INFO, "AML") << "Ran \\_SB._INI";
}
// Then each device, parents first (WalkDescendants is pre-order).
// A device that is neither present nor functional takes its whole
// subtree with it; one that is functional but absent skips only
// its own _INI.
static constexpr int MaxSkipped = 256;
int32_t skipped[MaxSkipped];
int skippedCount = 0;
int ran = 0;
for (int i = 0; i < deviceCount; i++) {
int32_t dev = devices[i];
bool underSkipped = false;
for (int32_t p = ns.GetNode(dev) ? ns.GetNode(dev)->ParentIndex : -1;
p > 0 && !underSkipped; p = ns.GetNode(p) ? ns.GetNode(p)->ParentIndex : -1) {
for (int k = 0; k < skippedCount; k++)
if (skipped[k] == p) { underSkipped = true; break; }
}
if (underSkipped) continue;
uint64_t sta = 0x0F;
Object staObj{};
if (interp.EvaluateChild(dev, "_STA", nullptr, 0, staObj) &&
staObj.Type == ObjectType::Integer)
sta = staObj.Integer;
if (!(sta & 0x01)) {
if (!(sta & 0x08) && skippedCount < MaxSkipped)
skipped[skippedCount++] = dev;
continue;
}
if (interp.FindChild(dev, "_INI") < 0) continue;
Object result{};
bool ok = interp.EvaluateChild(dev, "_INI", nullptr, 0, result);
ran++;
char path[128];
ns.GetNodePath(dev, path, sizeof(path));
KernelLogStream(DEBUG, "AML") << " _INI " << path << (ok ? "" : " FAILED");
}
KernelLogStream(OK, "AML") << "Namespace initialized: " << base::dec
<< (uint64_t)regCount << " _REG, " << (uint64_t)ran << " device _INI";
Memory::g_heap->Free(regs);
Memory::g_heap->Free(devices);
}
};
};
+13 -3
View File
@@ -5,6 +5,7 @@
*/
#pragma once
#include <ACPI/ACPI.hpp>
#include <cstdint>
namespace Hal {
@@ -27,11 +28,20 @@ namespace Hal {
// interpreter or namespace.
SleepObject FindSleepState(void* dsdtData, int state);
// Initialize the AML interpreter with the DSDT.
// This loads the full table into the namespace and enables
// Initialize the AML interpreter with the DSDT, then every SSDT the
// XSDT lists (xsdt may be null to load the DSDT alone).
// This loads the full tables into the namespace and enables
// method evaluation, device enumeration, and field access.
// Should be called during boot after ACPI table discovery.
void InitializeInterpreter(void* dsdtData);
void InitializeInterpreter(void* dsdtData, ACPI::CommonSDTHeader* xsdt);
// Run the namespace initialization the OS owes the firmware once its
// address-space handlers are up: _REG for PCI_Config, then \_SB._INI
// and every present device's _INI, parents first. Firmware works out
// which OS it is running under (OSYS, from _OSI) in these, and gates
// much of its later behaviour -- device power control included -- on
// the answer. Call once the Embedded Controller and PCI are up.
void InitializeNamespaceObjects();
};
};
+7 -8
View File
@@ -453,15 +453,14 @@ namespace Hal {
auto* nsNode = ns.GetNode(node);
if (!nsNode) return result;
if (nsNode->Obj.Type == AML::ObjectType::Package) {
// Parse the raw package buffer to extract SLP_TYP values
const uint8_t* data = nsNode->Obj.Buffer.Data;
uint32_t len = nsNode->Obj.Buffer.Length;
if (len < 2) return result;
if (nsNode->Obj.Type == AML::ObjectType::Package && nsNode->Obj.Package.Data) {
// Parse the raw package elements to extract SLP_TYP values
const uint8_t* data = nsNode->Obj.Package.Data;
uint32_t len = nsNode->Obj.Package.Length;
if (len < 1) return result;
// NumElements
uint8_t numElements = data[0];
uint32_t pos = 1;
uint32_t numElements = nsNode->Obj.Package.Count;
uint32_t pos = 0;
if (numElements >= 1 && pos < len) {
// Decode first element
+336
View File
@@ -0,0 +1,336 @@
/*
* AcpiPower.cpp
* ACPI device power management: _PSx methods, _PRx power resources,
* _DSM calls, and finding the namespace node behind a PCI function
* Copyright (c) 2026 Daniel Hammer
*/
#include "AcpiPower.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <Memory/Heap.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Hal {
namespace AcpiPower {
// ============================================================================
// PCI <-> ACPI
// ============================================================================
struct PciMapEntry {
int32_t Node;
uint8_t Bus, Dev, Fn;
uint8_t Score;
};
static PciMapEntry* g_map = nullptr;
static int g_mapCount = 0;
static bool g_mapBuilt = false;
bool HasObject(int32_t device, const char* name) {
return AML::GetInterpreter().FindChild(device, name) >= 0;
}
// How much power management a node carries. Picks the real node out
// of duplicates at one address.
static uint8_t PowerScore(int32_t node) {
uint8_t score = 0;
if (HasObject(node, "_PR0") || HasObject(node, "_PR3")) score += 8;
if (HasObject(node, "_PS3")) score += 4;
if (HasObject(node, "_DSM")) score += 2;
if (HasObject(node, "_OFF") || HasObject(node, "_PS0")) score += 1;
return score;
}
static void BuildPciMap() {
auto& interp = AML::GetInterpreter();
auto& ns = interp.GetNamespace();
g_mapBuilt = true;
g_mapCount = 0;
if (!interp.IsInitialized()) return;
// Device nodes with an _ADR, collected before anything is
// evaluated (evaluation may add nodes).
int32_t capacity = ns.NodeCount();
auto* nodes = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * capacity);
if (!nodes) return;
int count = 0;
int32_t sb = ns.FindNode("\\_SB_");
if (sb < 0) sb = ns.RootIndex();
ns.WalkDescendants(sb, AML::ObjectType::Device,
[&](int32_t idx, const AML::NamespaceNode*) {
if (count < capacity && ns.FindChild(idx, "_ADR") >= 0)
nodes[count++] = idx;
});
if (g_map) Memory::g_heap->Free(g_map);
g_map = (PciMapEntry*)Memory::g_heap->Request(sizeof(PciMapEntry) * (count ? count : 1));
if (!g_map) { Memory::g_heap->Free(nodes); return; }
for (int i = 0; i < count; i++) {
uint8_t bus, dev, fn;
if (!interp.GetPciAddress(nodes[i], bus, dev, fn)) continue;
g_map[g_mapCount++] = { nodes[i], bus, dev, fn, PowerScore(nodes[i]) };
}
Memory::g_heap->Free(nodes);
}
void InvalidatePciMap() {
g_mapBuilt = false;
}
int32_t FindPciDevice(uint8_t bus, uint8_t dev, uint8_t fn) {
if (!g_mapBuilt) BuildPciMap();
int32_t best = -1;
int bestScore = -1;
for (int i = 0; i < g_mapCount; i++) {
const auto& e = g_map[i];
if (e.Bus != bus || e.Dev != dev || e.Fn != fn) continue;
if (e.Score > bestScore) {
best = e.Node;
bestScore = e.Score;
}
}
return best;
}
// ============================================================================
// Power resources
// ============================================================================
static int AddUnique(int32_t* out, int count, int max, int32_t node) {
if (node < 0) return count;
for (int i = 0; i < count; i++)
if (out[i] == node) return count;
if (count < max) out[count++] = node;
return count;
}
// The resources one _PRx object lists. The list is copied out before
// anything else is evaluated: a computed package lives in storage the
// next evaluation reuses.
static int CollectList(int32_t device, const char* name, int32_t* out, int count, int max) {
auto& interp = AML::GetInterpreter();
auto& ns = interp.GetNamespace();
AML::Object pkg{};
if (!interp.EvaluateChild(device, name, nullptr, 0, pkg)) return count;
if (pkg.Type != AML::ObjectType::Package) return count;
int n = interp.GetPackageCount(pkg);
int32_t found[MaxResources];
int foundCount = 0;
for (int i = 0; i < n && foundCount < MaxResources; i++) {
AML::Object elem{};
if (!interp.GetPackageElement(pkg, i, elem)) break;
int32_t node = interp.ResolveReference(elem, device);
auto* nn = ns.GetNode(node);
if (nn && nn->Obj.Type == AML::ObjectType::PowerResource)
found[foundCount++] = node;
}
for (int i = 0; i < foundCount; i++)
count = AddUnique(out, count, max, found[i]);
return count;
}
int CollectResources(int32_t device, int32_t* out, int max) {
int count = 0;
count = CollectList(device, "_PR0", out, count, max);
count = CollectList(device, "_PR1", out, count, max);
count = CollectList(device, "_PR2", out, count, max);
count = CollectList(device, "_PR3", out, count, max);
return count;
}
int ResourceState(int32_t resource) {
AML::Object sta{};
if (!AML::GetInterpreter().EvaluateChild(resource, "_STA", nullptr, 0, sta))
return -1;
return (ResultInteger(sta) & 1) ? 1 : 0;
}
// Does a present device outside `owners` list this resource for D0?
// Turning off a rail another device is running on is how a GPU
// power-down would take out, say, an NVMe drive sharing a slot rail.
static bool NeededElsewhere(int32_t resource, const int32_t* owners, int ownerCount) {
auto& interp = AML::GetInterpreter();
auto& ns = interp.GetNamespace();
int32_t capacity = ns.NodeCount();
auto* users = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * capacity);
if (!users) return true; // cannot tell: assume shared
int count = 0;
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
[&](int32_t idx, const AML::NamespaceNode*) {
if (count < capacity && ns.FindChild(idx, "_PR0") >= 0)
users[count++] = idx;
});
bool shared = false;
for (int i = 0; i < count && !shared; i++) {
int32_t dev = users[i];
// Everything at or below an owner goes down with it: the
// card's own child nodes, a duplicate placeholder node at the
// same address, a function the firmware hid from PCI.
bool own = false;
for (int32_t a = dev; a > 0 && !own; ) {
for (int k = 0; k < ownerCount; k++)
if (owners[k] == a) { own = true; break; }
auto* an = ns.GetNode(a);
a = an ? an->ParentIndex : -1;
}
if (own) continue;
int32_t list[MaxResources];
int n = CollectList(dev, "_PR0", list, 0, MaxResources);
bool listed = false;
for (int k = 0; k < n; k++)
if (list[k] == resource) { listed = true; break; }
if (!listed) continue;
// Only a device that is actually there counts.
AML::Object sta{};
uint64_t status = 0x0F;
if (interp.EvaluateChild(dev, "_STA", nullptr, 0, sta))
status = ResultInteger(sta);
if (status & 0x01) {
char path[128];
ns.GetNodePath(dev, path, sizeof(path));
KernelLogStream(INFO, "AcpiPower") << " power resource also used by "
<< path << "; leaving it on";
shared = true;
}
}
Memory::g_heap->Free(users);
return shared;
}
// ============================================================================
// Device power transitions
// ============================================================================
void EnterD3Cold(int32_t device, const int32_t* owners, int ownerCount,
D3ColdReport& report) {
auto& interp = AML::GetInterpreter();
auto& ns = interp.GetNamespace();
report = D3ColdReport{};
if (device < 0) return;
char path[128];
ns.GetNodePath(device, path, sizeof(path));
// Gather the resources first: _PS3 may change what _PRx reports.
int32_t resources[MaxResources];
int count = CollectResources(device, resources, MaxResources);
if (HasObject(device, "_PS3")) {
AML::Object result{};
bool ok = interp.EvaluateChild(device, "_PS3", nullptr, 0, result);
report.RanPs3 = true;
KernelLogStream(INFO, "AcpiPower") << " " << path << "._PS3"
<< (ok ? "" : " (failed)");
}
// Off in reverse order of listing (ACPI turns resources off in
// reverse ResourceOrder; firmware lists them in that order).
for (int i = count - 1; i >= 0; i--) {
int32_t res = resources[i];
char rpath[128];
ns.GetNodePath(res, rpath, sizeof(rpath));
if (ResourceState(res) == 0) {
KernelLogStream(INFO, "AcpiPower") << " " << rpath << " already off";
continue;
}
if (NeededElsewhere(res, owners, ownerCount)) {
report.ResourcesShared++;
continue;
}
AML::Object result{};
bool ok = interp.EvaluateChild(res, "_OFF", nullptr, 0, result);
int after = ResourceState(res);
if (ok) report.ResourcesOff++;
if (after == 1) report.ResourcesStillOn++;
KernelLogStream(INFO, "AcpiPower") << " " << rpath << "._OFF"
<< (ok ? "" : " (failed)")
<< (after == 0 ? ", now off" : after == 1 ? ", still on" : "");
}
}
// ============================================================================
// _DSM
// ============================================================================
AML::Object IntegerArg(uint64_t value) {
AML::Object o{};
o.Type = AML::ObjectType::Integer;
o.Integer = value;
return o;
}
AML::Object BufferArg(const uint8_t* bytes, uint32_t length) {
AML::Object o{};
o.Type = AML::ObjectType::Buffer;
if (length > AML::MaxBufferLen) length = AML::MaxBufferLen;
memcpy(o.Buffer.Data, bytes, length);
o.Buffer.Length = length;
return o;
}
AML::Object EmptyPackageArg() {
AML::Object o{};
o.Type = AML::ObjectType::Package;
o.Package.Data = nullptr;
o.Package.Length = 0;
o.Package.Count = 0;
return o;
}
uint64_t ResultInteger(const AML::Object& result) {
if (result.Type == AML::ObjectType::Integer) return result.Integer;
if (result.Type == AML::ObjectType::Buffer) {
uint64_t v = 0;
for (uint32_t i = 0; i < result.Buffer.Length && i < 8; i++)
v |= (uint64_t)result.Buffer.Data[i] << (8 * i);
return v;
}
return 0;
}
bool EvaluateDsm(int32_t device, const uint8_t uuid[16], uint64_t revision,
uint64_t function, const AML::Object& arg3, AML::Object& result) {
auto& interp = AML::GetInterpreter();
if (device < 0 || interp.FindChild(device, "_DSM") < 0) return false;
AML::Object args[4];
args[0] = BufferArg(uuid, 16);
args[1] = IntegerArg(revision);
args[2] = IntegerArg(function);
args[3] = arg3;
return interp.EvaluateChild(device, "_DSM", args, 4, result);
}
uint64_t DsmFunctions(int32_t device, const uint8_t uuid[16], uint64_t revision,
const AML::Object& arg3) {
AML::Object result{};
if (!EvaluateDsm(device, uuid, revision, 0, arg3, result)) return 0;
uint64_t mask = ResultInteger(result);
// Bit 0 clear means "no functions for this UUID/revision".
return (mask & 1) ? mask : 0;
}
};
};
+92
View File
@@ -0,0 +1,92 @@
/*
* AcpiPower.hpp
* ACPI device power management: _PSx methods, _PRx power resources,
* _DSM calls, and finding the namespace node behind a PCI function
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <ACPI/AML/AmlNamespace.hpp>
namespace Hal {
namespace AcpiPower {
// ============================================================================
// PCI <-> ACPI
// ============================================================================
// The Device node describing PCI function bus:dev.fn, or -1. When
// firmware declares more than one node at the same address (a generic
// placeholder plus a vendor SSDT's real one is common for GPUs), the
// one carrying power-management objects wins.
int32_t FindPciDevice(uint8_t bus, uint8_t dev, uint8_t fn);
// Forget the PCI address map built by FindPciDevice (bus numbers may
// change across a resume).
void InvalidatePciMap();
// True when `device` has a child object named `name` ("_PS3").
bool HasObject(int32_t device, const char* name);
// ============================================================================
// Power resources
// ============================================================================
static constexpr int MaxResources = 8;
// The power resources the device lists in _PR0.._PR3 (deduplicated).
int CollectResources(int32_t device, int32_t* out, int max);
// _STA of a power resource: 1 on, 0 off, -1 unknown.
int ResourceState(int32_t resource);
// ============================================================================
// Device power transitions
// ============================================================================
struct D3ColdReport {
bool RanPs3;
int ResourcesOff; // resources whose _OFF was run
int ResourcesShared; // left on: some other present device needs them
int ResourcesStillOn; // _OFF ran but _STA still says on
};
// Put an ACPI device into D3cold the way the spec orders it: _PS3,
// then every power resource it depends on off, last-listed first. A
// resource that a device outside `owners` also depends on is left
// on -- `owners` lists the nodes being powered down together (the
// functions of one card and the port above them), which may freely
// share, as may anything below them in the namespace. The PCI side
// (PMCSR to D3hot) is the caller's to do first.
void EnterD3Cold(int32_t device, const int32_t* owners, int ownerCount,
D3ColdReport& report);
// ============================================================================
// _DSM
// ============================================================================
// Evaluate _DSM(uuid, revision, function, arg3) on a device. False
// when the device has no _DSM or it failed.
bool EvaluateDsm(int32_t device, const uint8_t uuid[16], uint64_t revision,
uint64_t function, const AML::Object& arg3, AML::Object& result);
// The function bitmap _DSM function 0 reports for (uuid, revision),
// 0 when unsupported. Bit 0 set means "some functions exist".
uint64_t DsmFunctions(int32_t device, const uint8_t uuid[16], uint64_t revision,
const AML::Object& arg3);
// Helpers for building _DSM / method arguments.
AML::Object IntegerArg(uint64_t value);
AML::Object BufferArg(const uint8_t* bytes, uint32_t length);
AML::Object EmptyPackageArg();
// Integer value of a method result: an integer as is, a buffer read
// little-endian (up to 8 bytes).
uint64_t ResultInteger(const AML::Object& result);
};
};
+1 -1
View File
@@ -58,7 +58,7 @@ namespace Hal {
// Initialize the full AML interpreter with the DSDT.
// This loads the entire DSDT namespace, enabling device enumeration,
// method evaluation, and field I/O for the rest of the kernel.
AML::InitializeInterpreter(dsdt);
AML::InitializeInterpreter(dsdt, xsdt);
// Enumerate ACPI devices now that the namespace is loaded
if (AML::GetInterpreter().IsInitialized()) {
+6 -3
View File
@@ -21,6 +21,7 @@
#include <Timekeeping/ApicTimer.hpp>
#include <Hal/CpuPower.hpp>
#include <Drivers/Graphics/IntelGPU.hpp>
#include <Drivers/Graphics/DgpuPower.hpp>
#include <Drivers/PS2/PS2Controller.hpp>
#include <Graphics/Framebuffer.hpp>
#include <Libraries/Memory.hpp>
@@ -128,9 +129,9 @@ namespace Hal {
const char* stepNames[] = {
"?", "GDT", "TSS", "Syscalls", "PAT", "PIC",
"LocalAPIC", "IoAPIC", "APICTimer", "PM1Clear",
"WAK", "PS2", "IntelGPU", "sti"
"WAK", "PS2", "IntelGPU", "dGPU", "sti"
};
const char* stepName = reinitStep <= 0x0D ? stepNames[reinitStep] : "?";
const char* stepName = reinitStep <= 0x0E ? stepNames[reinitStep] : "?";
KernelLogStream(WARNING, "S3") << "Reinit stopped after step 0x"
<< base::hex << (uint64_t)reinitStep << " (" << stepName
<< ") - next step crashed/hung";
@@ -459,7 +460,9 @@ namespace Hal {
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
Drivers::Graphics::IntelGPU::Reinitialize();
}
reinitProgress(0x0D); // sti
reinitProgress(0x0D); // discrete GPU back off
Drivers::Graphics::DgpuPower::Resume();
reinitProgress(0x0E); // sti
asm volatile("sti");
KernelLogStream(OK, "S3") << "Resumed from S3 suspend";
+19 -6
View File
@@ -249,13 +249,17 @@ namespace Hal {
}
static bool ParseCstObject(const char* path, const AML::Object& obj) {
if (obj.Type != AML::ObjectType::Package || obj.Buffer.Length < 2) {
if (obj.Type != AML::ObjectType::Package || !obj.Package.Data ||
obj.Package.Length < 1) {
return false;
}
uint32_t pos = 1; // package element count byte
// Package.Data starts at the first element (the state count).
const uint8_t* data = obj.Package.Data;
uint32_t length = obj.Package.Length;
uint32_t pos = 0;
uint64_t declaredStateCount = 0;
if (!DecodeInteger(obj.Buffer.Data, obj.Buffer.Length, pos, declaredStateCount)) {
if (!DecodeInteger(data, length, pos, declaredStateCount)) {
return false;
}
@@ -263,7 +267,7 @@ namespace Hal {
for (uint64_t i = 0; i < declaredStateCount && parsedCount < MaxIdleStates; i++) {
const uint8_t* nestedData = nullptr;
uint32_t nestedLength = 0;
if (!DecodePackageObject(obj.Buffer.Data, obj.Buffer.Length, pos, nestedData, nestedLength)) {
if (!DecodePackageObject(data, length, pos, nestedData, nestedLength)) {
break;
}
@@ -310,7 +314,10 @@ namespace Hal {
auto& ns = interp.GetNamespace();
char path[256];
for (int32_t nodeIndex = 0; nodeIndex < ns.NodeCount(); nodeIndex++) {
// DSDT processors only, for the same reason as the _CST scan.
int32_t limit = interp.DsdtNodeCount();
if (limit == 0) limit = ns.NodeCount();
for (int32_t nodeIndex = 0; nodeIndex < limit; nodeIndex++) {
auto* node = ns.GetNode(nodeIndex);
if (node == nullptr || node->Obj.Type != AML::ObjectType::Processor) {
continue;
@@ -522,7 +529,13 @@ namespace Hal {
auto& ns = interp.GetNamespace();
char path[256];
for (int32_t nodeIndex = 0; nodeIndex < ns.NodeCount(); nodeIndex++) {
// DSDT objects only. SSDTs are loaded too now (for device power
// methods), and many carry processor _CST tables; those C-state
// lists have never been exercised against the deep-idle timer
// behaviour on real laptops, so they stay out until they have.
int32_t limit = interp.DsdtNodeCount();
if (limit == 0) limit = ns.NodeCount();
for (int32_t nodeIndex = 0; nodeIndex < limit; nodeIndex++) {
auto* node = ns.GetNode(nodeIndex);
if (node == nullptr) continue;
if (!IsName(node->Name, "_CST")) continue;