feat: power down dGPUs on hybrid graphics laptops
This commit is contained in:
1 parent
7f12e1a150
commit
24f88ec4c6
23 files changed
+4325
-1130
No files matched your search
@@ -0,0 +1,560 @@
|
||||
/*
|
||||
* DgpuPower.cpp
|
||||
* Powers down discrete GPUs that no MontaukOS driver runs
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "DgpuPower.hpp"
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <ACPI/AcpiPower.hpp>
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <Boot/Boot.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
namespace AcpiPower = Hal::AcpiPower;
|
||||
namespace AML = Hal::AML;
|
||||
|
||||
namespace Drivers::Graphics::DgpuPower {
|
||||
|
||||
// =========================================================================
|
||||
// Vendor interfaces
|
||||
// =========================================================================
|
||||
|
||||
static constexpr uint16_t VendorNvidia = 0x10DE;
|
||||
static constexpr uint16_t VendorAmd = 0x1002;
|
||||
|
||||
// NVIDIA Optimus _DSM (GUID a486d8f8-0bda-471b-a72b-6042a6b5bee0, rev
|
||||
// 0x100). Function 0x1A (OPTIMUS_CAPS) with "power down in PS3" set
|
||||
// arms the GPU's _PS3 to cut its power; 0x1B (OPTIMUS_FLAGS) 0x3 is
|
||||
// what the drivers send alongside it.
|
||||
static constexpr uint8_t OptimusUuid[16] = {
|
||||
0xF8, 0xD8, 0x86, 0xA4, 0xDA, 0x0B, 0x1B, 0x47,
|
||||
0xA7, 0x2B, 0x60, 0x42, 0xA6, 0xB5, 0xBE, 0xE0,
|
||||
};
|
||||
static constexpr uint64_t OptimusRevision = 0x100;
|
||||
static constexpr uint64_t OptimusCaps = 0x1A;
|
||||
static constexpr uint64_t OptimusFlags = 0x1B;
|
||||
static constexpr uint32_t OptimusPowerdown = (3u << 24) | 1u; // POWERDOWN_PS3 | FLAGS_CHANGED
|
||||
|
||||
// The older NVIDIA hybrid _DSM (GUID d4a50b75-65c7-46f7-bfb7-41514cea0244,
|
||||
// rev 0x102): function 3 (POWER) with 2 (STAMINA) switches the discrete
|
||||
// GPU off on pre-Optimus muxed laptops.
|
||||
static constexpr uint8_t NvidiaDsmUuid[16] = {
|
||||
0x75, 0x0B, 0xA5, 0xD4, 0xC7, 0x65, 0xF7, 0x46,
|
||||
0xBF, 0xB7, 0x41, 0x51, 0x4C, 0xEA, 0x02, 0x44,
|
||||
};
|
||||
static constexpr uint64_t NvidiaDsmRevision = 0x102;
|
||||
static constexpr uint64_t NvidiaDsmPower = 0x03;
|
||||
static constexpr uint64_t NvidiaDsmStamina = 0x02;
|
||||
|
||||
// AMD PowerXpress ATPX(function, buffer).
|
||||
static constexpr uint64_t AtpxVerifyInterface = 0;
|
||||
static constexpr uint64_t AtpxGetParameters = 1;
|
||||
static constexpr uint64_t AtpxPowerControl = 2;
|
||||
static constexpr uint32_t AtpxParamsSupported = 1u << 0; // function mask bits
|
||||
static constexpr uint32_t AtpxPowerSupported = 1u << 1;
|
||||
static constexpr uint32_t AtpxHybridGraphics = 1u << 14; // PX parameter flag
|
||||
|
||||
// =========================================================================
|
||||
// State
|
||||
// =========================================================================
|
||||
|
||||
static constexpr int MaxCards = 4;
|
||||
static constexpr int MaxFunctions = 8;
|
||||
static constexpr int MaxChain = 4;
|
||||
|
||||
struct PciAddr {
|
||||
uint8_t Bus, Dev, Fn;
|
||||
};
|
||||
|
||||
struct Card {
|
||||
uint8_t Bus, Dev, DisplayFn;
|
||||
uint16_t Vendor, DeviceId;
|
||||
uint8_t Functions[MaxFunctions];
|
||||
int FunctionCount;
|
||||
PciAddr Chain[MaxChain]; // bridges above it, nearest first
|
||||
int ChainCount;
|
||||
};
|
||||
|
||||
static Card g_cards[MaxCards];
|
||||
static int g_cardCount = 0;
|
||||
|
||||
int PoweredDownCount() {
|
||||
return g_cardCount;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PCI helpers
|
||||
// =========================================================================
|
||||
|
||||
static uint32_t Cfg(uint8_t bus, uint8_t dev, uint8_t fn, uint16_t off, uint8_t width) {
|
||||
return Pci::ConfigRead(bus, dev, fn, off, width);
|
||||
}
|
||||
|
||||
static bool Present(uint8_t bus, uint8_t dev, uint8_t fn) {
|
||||
return (Cfg(bus, dev, fn, 0x00, 2) & 0xFFFF) != 0xFFFF;
|
||||
}
|
||||
|
||||
// The bridge whose secondary bus is `bus`, or nullptr for a root bus.
|
||||
static const Pci::PciDevice* BridgeAbove(uint8_t bus) {
|
||||
auto& devices = Pci::GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
if (!Pci::IsBridge(d)) continue;
|
||||
if (Cfg(d.Bus, d.Device, d.Function, Pci::PCI_REG_SECONDARY_BUS, 1) == bus)
|
||||
return &d;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void BusRange(const Pci::PciDevice& b, uint8_t& secondary, uint8_t& subordinate) {
|
||||
secondary = (uint8_t)Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_SECONDARY_BUS, 1);
|
||||
subordinate = (uint8_t)Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_SUBORDINATE_BUS, 1);
|
||||
}
|
||||
|
||||
// Does anything other than the card and its own bridges live below b?
|
||||
static bool SubtreeIsCard(const Pci::PciDevice& b, const Card& card) {
|
||||
uint8_t sec, sub;
|
||||
BusRange(b, sec, sub);
|
||||
auto& devices = Pci::GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.Bus < sec || d.Bus > sub) continue;
|
||||
if (d.Bus == card.Bus && d.Device == card.Dev) continue;
|
||||
bool chain = false;
|
||||
for (int k = 0; k < card.ChainCount; k++) {
|
||||
if (card.Chain[k].Bus == d.Bus && card.Chain[k].Dev == d.Device &&
|
||||
card.Chain[k].Fn == d.Function) { chain = true; break; }
|
||||
}
|
||||
if (!chain) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Does the bridge forward this physical address downstream?
|
||||
static bool WindowContains(const Pci::PciDevice& b, uint64_t phys) {
|
||||
uint32_t mem = Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_MEM_BASE, 4);
|
||||
uint64_t base = (uint64_t)(mem & 0xFFF0) << 16;
|
||||
uint64_t limit = ((uint64_t)((mem >> 16) & 0xFFF0) << 16) | 0xFFFFF;
|
||||
if (base <= limit && phys >= base && phys <= limit) return true;
|
||||
|
||||
uint32_t pref = Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_PREF_BASE, 4);
|
||||
uint64_t pbase = (uint64_t)(pref & 0xFFF0) << 16;
|
||||
uint64_t plimit = ((uint64_t)((pref >> 16) & 0xFFF0) << 16) | 0xFFFFF;
|
||||
if ((pref & 0xF) == 0x1) {
|
||||
pbase |= (uint64_t)Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_PREF_BASE_UPPER, 4) << 32;
|
||||
plimit |= (uint64_t)Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_PREF_LIMIT_UPPER, 4) << 32;
|
||||
}
|
||||
return pbase <= plimit && phys >= pbase && phys <= plimit;
|
||||
}
|
||||
|
||||
// Functions that ship on a graphics card: the GPU itself, its HDMI/DP
|
||||
// audio, and (NVIDIA Turing+, AMD Navi) a USB-C xHCI and UCSI controller.
|
||||
// An integrated GPU behind an internal bridge -- AMD APUs -- shares its
|
||||
// device with the platform security processor, audio DSP and the
|
||||
// machine's own USB, and must never be mistaken for a card.
|
||||
static bool IsCardFunction(const Pci::PciDevice& d) {
|
||||
if (d.ClassCode == 0x03) return true; // display
|
||||
if (d.ClassCode == 0x04 && d.SubClass == 0x03) return true; // HD audio
|
||||
if (d.ClassCode == 0x0C && d.SubClass == 0x03) return true; // USB
|
||||
if (d.ClassCode == 0x0C && d.SubClass == 0x80) return true; // UCSI
|
||||
return false;
|
||||
}
|
||||
|
||||
static int PciePortType(uint8_t bus, uint8_t dev, uint8_t fn) {
|
||||
uint8_t cap = Pci::FindCapability(bus, dev, fn, Pci::PCI_CAP_PCIE);
|
||||
if (cap == 0) return -1;
|
||||
return (int)((Cfg(bus, dev, fn, cap + 2, 2) >> 4) & 0xF);
|
||||
}
|
||||
static constexpr int PortTypeRootPort = 0x4;
|
||||
|
||||
static void LogAddr(KernelLogStream& log, uint8_t bus, uint8_t dev, uint8_t fn) {
|
||||
log << base::hex << (uint64_t)bus << ":" << (uint64_t)dev << "." << (uint64_t)fn;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Detection
|
||||
// =========================================================================
|
||||
|
||||
static bool BootFramebuffer(uint64_t& phys) {
|
||||
const auto& info = montauk::boot::Info();
|
||||
if (!info.has(montauk::boot::FeatureFramebuffer) || info.framebuffer.address == 0)
|
||||
return false;
|
||||
phys = Memory::SubHHDM(info.framebuffer.address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decide whether `gpu` is a discrete card we may switch off, filling in
|
||||
// `card` if so. Every reason to leave it alone is logged.
|
||||
static bool Qualifies(const Pci::PciDevice& gpu, Card& card) {
|
||||
card = Card{};
|
||||
card.Bus = gpu.Bus;
|
||||
card.Dev = gpu.Device;
|
||||
card.DisplayFn = gpu.Function;
|
||||
card.Vendor = gpu.VendorId;
|
||||
card.DeviceId = gpu.DeviceId;
|
||||
|
||||
auto reject = [&](const char* why) {
|
||||
KernelLogStream log(INFO, "dGPU");
|
||||
log << "Leaving ";
|
||||
LogAddr(log, gpu.Bus, gpu.Device, gpu.Function);
|
||||
log << " (" << base::hex << (uint64_t)gpu.VendorId << ":" << (uint64_t)gpu.DeviceId
|
||||
<< ") on: " << why;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (gpu.Claimed) return reject("a driver runs it");
|
||||
|
||||
// An integrated GPU sits on the root bus. A card is behind a port.
|
||||
const Pci::PciDevice* port = BridgeAbove(gpu.Bus);
|
||||
if (!port) return reject("on the root bus (integrated)");
|
||||
|
||||
// The whole device: every function must be one a card carries, and
|
||||
// none may already belong to a driver.
|
||||
auto& devices = Pci::GetDevices();
|
||||
bool otherDisplay = false;
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.Bus == gpu.Bus && d.Device == gpu.Device) {
|
||||
if (!IsCardFunction(d)) return reject("shares its device with platform functions");
|
||||
if (d.Claimed) return reject("a driver runs one of its functions");
|
||||
if (card.FunctionCount < MaxFunctions)
|
||||
card.Functions[card.FunctionCount++] = d.Function;
|
||||
} else if (d.ClassCode == 0x03) {
|
||||
otherDisplay = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Hybrid graphics only: something else has to drive the screen.
|
||||
if (!otherDisplay) return reject("it is the only display controller");
|
||||
|
||||
// The firmware's boot display (the GOP framebuffer, or legacy VGA)
|
||||
// must not be scanned out by this card -- on a muxed laptop switched
|
||||
// to discrete mode it is, and the panel would go dark.
|
||||
uint64_t fb = 0;
|
||||
if (BootFramebuffer(fb)) {
|
||||
if (WindowContains(*port, fb)) return reject("it scans out the boot display");
|
||||
} else {
|
||||
uint16_t ctl = (uint16_t)Cfg(port->Bus, port->Device, port->Function,
|
||||
Pci::PCI_REG_BRIDGE_CONTROL, 2);
|
||||
if (ctl & Pci::PCI_BRIDGE_CTL_VGA) return reject("legacy VGA is routed to it");
|
||||
}
|
||||
|
||||
// The port, and any switch above it that serves nothing else, power
|
||||
// down with the card. The first bridge must serve only the card.
|
||||
const Pci::PciDevice* b = port;
|
||||
while (b && card.ChainCount < MaxChain) {
|
||||
card.Chain[card.ChainCount++] = { b->Bus, b->Device, b->Function };
|
||||
if (!SubtreeIsCard(*b, card)) {
|
||||
card.ChainCount--;
|
||||
break;
|
||||
}
|
||||
if (b->Bus == 0) break;
|
||||
b = BridgeAbove(b->Bus);
|
||||
}
|
||||
if (card.ChainCount == 0) return reject("its port also serves other devices");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Vendor methods
|
||||
// =========================================================================
|
||||
|
||||
// The ACPI nodes of every display controller, where the vendor _DSM or
|
||||
// ATPX may live (the integrated GPU's node, on some muxed designs).
|
||||
static int DisplayNodes(int32_t* out, int max) {
|
||||
int count = 0;
|
||||
auto& devices = Pci::GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size() && count < max; i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.ClassCode != 0x03) continue;
|
||||
int32_t node = AcpiPower::FindPciDevice(d.Bus, d.Device, d.Function);
|
||||
if (node >= 0) out[count++] = node;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static AML::Object Dword(uint32_t v) {
|
||||
uint8_t b[4] = { (uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24) };
|
||||
return AcpiPower::BufferArg(b, 4);
|
||||
}
|
||||
|
||||
// Arm Optimus power-down in _PS3. Returns true when firmware took it.
|
||||
static bool ArmOptimus(int32_t gpuNode) {
|
||||
if (gpuNode < 0) return false;
|
||||
uint64_t funcs = AcpiPower::DsmFunctions(gpuNode, OptimusUuid, OptimusRevision,
|
||||
AcpiPower::EmptyPackageArg());
|
||||
if (!(funcs & (1ULL << OptimusCaps))) return false;
|
||||
|
||||
AML::Object result{};
|
||||
AcpiPower::EvaluateDsm(gpuNode, OptimusUuid, OptimusRevision, OptimusCaps,
|
||||
Dword(OptimusPowerdown), result);
|
||||
if (funcs & (1ULL << OptimusFlags))
|
||||
AcpiPower::EvaluateDsm(gpuNode, OptimusUuid, OptimusRevision, OptimusFlags,
|
||||
Dword(0x3), result);
|
||||
KernelLogStream(INFO, "dGPU") << " Optimus _DSM: power-down on _PS3 armed";
|
||||
return true;
|
||||
}
|
||||
|
||||
// The node offering the legacy NVIDIA power _DSM, or -1.
|
||||
static int32_t FindNvidiaDsm(int32_t gpuNode) {
|
||||
int32_t nodes[8];
|
||||
int count = 0;
|
||||
if (gpuNode >= 0) nodes[count++] = gpuNode;
|
||||
count += DisplayNodes(nodes + count, 8 - count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
uint64_t funcs = AcpiPower::DsmFunctions(nodes[i], NvidiaDsmUuid, NvidiaDsmRevision,
|
||||
AcpiPower::EmptyPackageArg());
|
||||
if (funcs & (1ULL << NvidiaDsmPower)) return nodes[i];
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The ATPX method, if the platform offers one with power control that
|
||||
// is meant to be used (not on "hybrid" designs, which use _PR3).
|
||||
static int32_t FindAtpx() {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
auto& ns = interp.GetNamespace();
|
||||
|
||||
int32_t atpx = -1;
|
||||
int32_t nodes[8];
|
||||
int count = DisplayNodes(nodes, 8);
|
||||
for (int i = 0; i < count && atpx < 0; i++)
|
||||
atpx = interp.FindChild(nodes[i], "ATPX");
|
||||
if (atpx < 0) {
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Method,
|
||||
[&](int32_t idx, const AML::NamespaceNode* n) {
|
||||
if (atpx < 0 && n->Name[0] == 'A' && n->Name[1] == 'T' &&
|
||||
n->Name[2] == 'P' && n->Name[3] == 'X')
|
||||
atpx = idx;
|
||||
});
|
||||
}
|
||||
if (atpx < 0) return -1;
|
||||
|
||||
AML::Object args[2] = { AcpiPower::IntegerArg(AtpxVerifyInterface),
|
||||
AcpiPower::IntegerArg(0) };
|
||||
AML::Object result{};
|
||||
if (!interp.EvaluateNode(atpx, args, 2, result) ||
|
||||
result.Type != AML::ObjectType::Buffer || result.Buffer.Length < 8)
|
||||
return -1;
|
||||
const uint8_t* r = result.Buffer.Data;
|
||||
uint32_t functions = r[4] | (r[5] << 8) | (r[6] << 16) | ((uint32_t)r[7] << 24);
|
||||
|
||||
if (functions & AtpxParamsSupported) {
|
||||
args[0] = AcpiPower::IntegerArg(AtpxGetParameters);
|
||||
if (interp.EvaluateNode(atpx, args, 2, result) &&
|
||||
result.Type == AML::ObjectType::Buffer && result.Buffer.Length >= 10) {
|
||||
const uint8_t* p = result.Buffer.Data;
|
||||
uint32_t valid = p[2] | (p[3] << 8) | (p[4] << 16) | ((uint32_t)p[5] << 24);
|
||||
uint32_t flags = p[6] | (p[7] << 8) | (p[8] << 16) | ((uint32_t)p[9] << 24);
|
||||
if (valid & flags & AtpxHybridGraphics) {
|
||||
KernelLogStream(INFO, "dGPU") << " ATPX: hybrid design, power via _PR3";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(functions & AtpxPowerSupported)) return -1;
|
||||
return atpx;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Power-down sequence
|
||||
// =========================================================================
|
||||
|
||||
static void PowerDownCard(const Card& card) {
|
||||
{
|
||||
KernelLogStream log(INFO, "dGPU");
|
||||
log << "Powering down ";
|
||||
LogAddr(log, card.Bus, card.Dev, card.DisplayFn);
|
||||
log << " (" << base::hex << (uint64_t)card.Vendor << ":"
|
||||
<< (uint64_t)card.DeviceId << ")";
|
||||
}
|
||||
|
||||
bool haveAcpi = AML::GetInterpreter().IsInitialized();
|
||||
|
||||
// 1. Quiesce every function: no decode, no DMA, no interrupts.
|
||||
for (int i = 0; i < card.FunctionCount; i++) {
|
||||
uint8_t fn = card.Functions[i];
|
||||
if (!Present(card.Bus, card.Dev, fn)) continue;
|
||||
uint16_t cmd = (uint16_t)Cfg(card.Bus, card.Dev, fn, Pci::PCI_REG_COMMAND, 2);
|
||||
cmd &= (uint16_t)~(0x7u); // I/O, memory, bus master
|
||||
Pci::ConfigWrite(card.Bus, card.Dev, fn, Pci::PCI_REG_COMMAND, 2, cmd);
|
||||
Pci::DisableInterruptDelivery(card.Bus, card.Dev, fn);
|
||||
}
|
||||
|
||||
// 2. The namespace side of the card and its ports.
|
||||
int32_t gpuNode = -1;
|
||||
int32_t owners[MaxFunctions + MaxChain];
|
||||
int ownerCount = 0;
|
||||
int32_t chainNodes[MaxChain];
|
||||
bool portHasPr3 = false;
|
||||
|
||||
if (haveAcpi) {
|
||||
AcpiPower::InvalidatePciMap();
|
||||
gpuNode = AcpiPower::FindPciDevice(card.Bus, card.Dev, card.DisplayFn);
|
||||
for (int i = 0; i < card.FunctionCount; i++) {
|
||||
int32_t n = AcpiPower::FindPciDevice(card.Bus, card.Dev, card.Functions[i]);
|
||||
if (n >= 0) owners[ownerCount++] = n;
|
||||
}
|
||||
for (int i = 0; i < card.ChainCount; i++) {
|
||||
chainNodes[i] = AcpiPower::FindPciDevice(card.Chain[i].Bus, card.Chain[i].Dev,
|
||||
card.Chain[i].Fn);
|
||||
if (chainNodes[i] >= 0) {
|
||||
owners[ownerCount++] = chainNodes[i];
|
||||
if (AcpiPower::HasObject(chainNodes[i], "_PR3")) portHasPr3 = true;
|
||||
}
|
||||
}
|
||||
|
||||
char path[128];
|
||||
if (gpuNode >= 0) {
|
||||
AML::GetInterpreter().GetNamespace().GetNodePath(gpuNode, path, sizeof(path));
|
||||
KernelLogStream(INFO, "dGPU") << " ACPI node " << path;
|
||||
} else {
|
||||
KernelLogStream(INFO, "dGPU") << " no ACPI node for the GPU";
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < card.ChainCount; i++) chainNodes[i] = -1;
|
||||
}
|
||||
|
||||
// 3. Vendor methods. With _PR3 on the port the platform does runtime
|
||||
// D3cold through power resources, and the drivers leave the vendor
|
||||
// methods alone there (they would make _PS3 cut power underneath
|
||||
// the power resources' own sequencing).
|
||||
int32_t nvidiaDsm = -1;
|
||||
int32_t atpx = -1;
|
||||
if (haveAcpi && !portHasPr3) {
|
||||
if (card.Vendor == VendorNvidia) {
|
||||
if (!ArmOptimus(gpuNode)) nvidiaDsm = FindNvidiaDsm(gpuNode);
|
||||
} else if (card.Vendor == VendorAmd) {
|
||||
atpx = FindAtpx();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. PCI D3hot, companions first, the GPU last.
|
||||
int d3hot = 0;
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
for (int i = 0; i < card.FunctionCount; i++) {
|
||||
uint8_t fn = card.Functions[i];
|
||||
if ((fn == card.DisplayFn) != (pass == 1)) continue;
|
||||
if (!Present(card.Bus, card.Dev, fn)) continue;
|
||||
if (Pci::SetPowerState(card.Bus, card.Dev, fn, 3)) d3hot++;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Vendor power switch.
|
||||
if (nvidiaDsm >= 0) {
|
||||
AML::Object result{};
|
||||
AcpiPower::EvaluateDsm(nvidiaDsm, NvidiaDsmUuid, NvidiaDsmRevision, NvidiaDsmPower,
|
||||
AcpiPower::IntegerArg(NvidiaDsmStamina), result);
|
||||
KernelLogStream(INFO, "dGPU") << " NVIDIA _DSM: discrete GPU switched off";
|
||||
}
|
||||
if (atpx >= 0) {
|
||||
uint8_t params[3] = { 3, 0, 0 }; // size = 3, dGPU state = off
|
||||
AML::Object args[2] = { AcpiPower::IntegerArg(AtpxPowerControl),
|
||||
AcpiPower::BufferArg(params, 3) };
|
||||
AML::Object result{};
|
||||
AML::GetInterpreter().EvaluateNode(atpx, args, 2, result);
|
||||
Timekeeping::BusyWaitMs(200); // the off transition needs it
|
||||
KernelLogStream(INFO, "dGPU") << " ATPX: discrete GPU switched off";
|
||||
}
|
||||
|
||||
// 6. The GPU's own ACPI D3cold: _PS3 and its power resources.
|
||||
bool d3cold = false;
|
||||
int shared = 0;
|
||||
if (gpuNode >= 0) {
|
||||
AcpiPower::D3ColdReport r{};
|
||||
AcpiPower::EnterD3Cold(gpuNode, owners, ownerCount, r);
|
||||
if (r.ResourcesOff > 0 && r.ResourcesStillOn == 0) d3cold = true;
|
||||
shared += r.ResourcesShared;
|
||||
|
||||
// Older Optimus firmware hangs the power switch on a _OFF method
|
||||
// of the GPU device itself and only calls it from _PS3 when the
|
||||
// driver's flags say so. Call it directly if the card is still
|
||||
// answering.
|
||||
if (Present(card.Bus, card.Dev, card.DisplayFn) && !portHasPr3) {
|
||||
int32_t off = AML::GetInterpreter().FindChild(gpuNode, "_OFF");
|
||||
auto* n = AML::GetInterpreter().GetNamespace().GetNode(off);
|
||||
if (n && n->Obj.Type == AML::ObjectType::Method) {
|
||||
AML::Object result{};
|
||||
AML::GetInterpreter().EvaluateNode(off, nullptr, 0, result);
|
||||
KernelLogStream(INFO, "dGPU") << " ran the GPU's _OFF";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Present(card.Bus, card.Dev, card.DisplayFn)) d3cold = true;
|
||||
|
||||
// 7. The ports above it, nearest first. A switch port belongs to the
|
||||
// card outright; a root port only goes down when the platform says
|
||||
// it can (power resources or _PS3), as Linux requires.
|
||||
for (int i = 0; i < card.ChainCount; i++) {
|
||||
const PciAddr& p = card.Chain[i];
|
||||
int32_t node = chainNodes[i];
|
||||
bool rootPort = PciePortType(p.Bus, p.Dev, p.Fn) == PortTypeRootPort;
|
||||
bool acpiPm = node >= 0 && (AcpiPower::HasObject(node, "_PR3") ||
|
||||
AcpiPower::HasObject(node, "_PR0") ||
|
||||
AcpiPower::HasObject(node, "_PS3"));
|
||||
if (rootPort && !acpiPm) break;
|
||||
|
||||
Pci::SetPowerState(p.Bus, p.Dev, p.Fn, 3);
|
||||
if (node >= 0) {
|
||||
AcpiPower::D3ColdReport r{};
|
||||
AcpiPower::EnterD3Cold(node, owners, ownerCount, r);
|
||||
if (r.ResourcesOff > 0 && r.ResourcesStillOn == 0) d3cold = true;
|
||||
shared += r.ResourcesShared;
|
||||
}
|
||||
if (rootPort) break;
|
||||
}
|
||||
|
||||
KernelLogStream log(d3cold || d3hot ? OK : WARNING, "dGPU");
|
||||
LogAddr(log, card.Bus, card.Dev, card.DisplayFn);
|
||||
if (d3cold)
|
||||
log << " is powered off (D3cold)";
|
||||
else if (d3hot && shared)
|
||||
log << " is in D3hot; its power rail also feeds another device, so it stays on";
|
||||
else if (d3hot)
|
||||
log << " is in D3hot; the platform offered no way to cut its power";
|
||||
else
|
||||
log << " could not be powered down (no PCI or ACPI power management)";
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Public API
|
||||
// =========================================================================
|
||||
|
||||
void PowerDownUnused() {
|
||||
auto& devices = Pci::GetDevices();
|
||||
g_cardCount = 0;
|
||||
|
||||
for (uint64_t i = 0; i < devices.size() && g_cardCount < MaxCards; i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.ClassCode != 0x03 || d.Reserved) continue;
|
||||
|
||||
Card card;
|
||||
if (!Qualifies(d, card)) continue;
|
||||
|
||||
// Withhold all of its functions from the driver probe that
|
||||
// follows: the xHCI driver would otherwise claim the card's
|
||||
// USB-C controller and lose it when the card powers off.
|
||||
for (uint64_t k = 0; k < devices.size(); k++) {
|
||||
if (devices[k].Bus == card.Bus && devices[k].Device == card.Dev)
|
||||
devices[k].Reserved = true;
|
||||
}
|
||||
|
||||
g_cards[g_cardCount++] = card;
|
||||
PowerDownCard(card);
|
||||
}
|
||||
}
|
||||
|
||||
void Resume() {
|
||||
for (int i = 0; i < g_cardCount; i++) {
|
||||
KernelLogStream(INFO, "dGPU") << "Re-applying power-down after resume";
|
||||
PowerDownCard(g_cards[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* DgpuPower.hpp
|
||||
* Powers down discrete GPUs that no MontaukOS driver runs
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::Graphics::DgpuPower {
|
||||
|
||||
// A hybrid-graphics laptop boots with its discrete GPU (NVIDIA Optimus,
|
||||
// AMD PowerXpress / hybrid, Intel Arc) powered and idling at boot clocks.
|
||||
// With no driver to manage it, it stays there: several watts of heat and
|
||||
// a fan that never settles. This finds every display controller that
|
||||
// nothing claimed and that does not carry the boot display, takes its
|
||||
// whole card (GPU, HDMI audio, USB-C controller) away from the driver
|
||||
// probe, and powers it down: PCI D3hot, then the platform's D3cold path
|
||||
// through ACPI (_PR3 power resources on the port above it, _PS3, and the
|
||||
// NVIDIA Optimus / legacy and AMD ATPX vendor methods where firmware
|
||||
// relies on them).
|
||||
//
|
||||
// Call after the early (graphics) driver probe and before the normal
|
||||
// one, with the AML namespace initialized.
|
||||
void PowerDownUnused();
|
||||
|
||||
// Firmware repowers the card during S3 resume. Call once the platform is
|
||||
// back up to put every card powered down at boot back to sleep.
|
||||
void Resume();
|
||||
|
||||
// Number of cards powered down (for diagnostics).
|
||||
int PoweredDownCount();
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Init.hpp"
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||
#include <Drivers/Graphics/DgpuPower.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
@@ -222,6 +223,10 @@ namespace Drivers {
|
||||
}
|
||||
}
|
||||
|
||||
void PowerDownUnusedGpus() {
|
||||
Graphics::DgpuPower::PowerDownUnused();
|
||||
}
|
||||
|
||||
void ProbeNormal() {
|
||||
Pci::ProbeAll(g_driverTable, g_driverTableCount, Pci::ProbePhase::Normal);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ namespace Drivers {
|
||||
// Post-probe: wire up GPU framebuffer to cursor subsystem.
|
||||
void InitializeGraphics();
|
||||
|
||||
// Power down discrete GPUs no driver claimed (hybrid-graphics laptops),
|
||||
// withholding their functions from the Normal-phase probe. Run between
|
||||
// ProbeEarly() and ProbeNormal(), once the timer is running.
|
||||
void PowerDownUnusedGpus();
|
||||
|
||||
// Probe PCI devices for Normal-phase drivers (xHCI, E1000, E1000E, AHCI, NVMe, IntelHDA).
|
||||
// USB class drivers (Bluetooth, etc.) initialize automatically during xHCI enumeration.
|
||||
void ProbeNormal();
|
||||
|
||||
Reference in new issue
Block a user