feat: add laptop/display brightness modulation
This commit is contained in:
1 parent
ef1e69ae83
commit
7979acc2eb
34 files changed
+1308
-78
No files matched your search
@@ -82,8 +82,7 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
static uint8_t g_connectorType = montauk::abi::DISPLAY_CONNECTOR_UNKNOWN;
|
||||
static bool g_connectorConnected = false;
|
||||
static bool g_edidValid = false;
|
||||
static bool g_pwmSupported = false;
|
||||
static uint32_t g_pwmCtlReg = 0;
|
||||
static int g_ddiPort = -1; // DDI port driving the output, -1 unknown
|
||||
static char g_connectorName[32] = "Unknown";
|
||||
static char g_monitorName[64] = "Unknown display";
|
||||
static uint8_t g_auxEdid[256] = {};
|
||||
@@ -556,10 +555,11 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
}
|
||||
|
||||
static bool AuxI2cTransfer(uint8_t request, const uint8_t* writeData,
|
||||
int writeSize, uint8_t* readData, int readSize) {
|
||||
int writeSize, uint8_t* readData, int readSize,
|
||||
uint8_t i2cAddress = 0x50) {
|
||||
for (int retry = 0; retry < 16; retry++) {
|
||||
uint8_t reply = 0xFF;
|
||||
int result = AuxTransfer(request, 0x50, writeData, writeSize,
|
||||
int result = AuxTransfer(request, i2cAddress, writeData, writeSize,
|
||||
readData, readSize, &reply);
|
||||
uint8_t nativeReply = reply & 0x3;
|
||||
uint8_t i2cReply = reply & 0xC;
|
||||
@@ -600,7 +600,8 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
|
||||
static bool ReadDpEdid() {
|
||||
g_auxEdidSize = 0;
|
||||
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP
|
||||
if ((g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP
|
||||
&& g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP)
|
||||
|| g_auxControlReg == 0)
|
||||
return false;
|
||||
|
||||
@@ -789,9 +790,16 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
g_connectorConnected = (ReadReg(trans.conf) & PIPECONF_ENABLE) != 0;
|
||||
uint32_t ddi = ReadReg(trans.ddi);
|
||||
g_auxControlReg = 0;
|
||||
g_ddiPort = -1;
|
||||
|
||||
if (g_gpuGen <= 7) {
|
||||
if (ReadReg(LVDS) & LVDS_PORT_ENABLE) {
|
||||
// Haswell (listed as Gen 7) has the eDP transcoder; Ironlake to
|
||||
// Ivy Bridge drive eDP from the CPU's port A.
|
||||
if (trans.edp || (ReadReg(DP_A) & (1u << 31))) {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1");
|
||||
g_ddiPort = 0;
|
||||
} else if (ReadReg(LVDS) & LVDS_PORT_ENABLE) {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_LVDS;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "LVDS-1");
|
||||
} else if (ReadReg(ADPA) & ADPA_DAC_ENABLE) {
|
||||
@@ -807,14 +815,30 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
} else if (trans.edp) {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1");
|
||||
g_ddiPort = 0;
|
||||
g_auxControlReg = DP_AUX_A_CTL;
|
||||
} else {
|
||||
uint32_t mode = (ddi & TRANS_DDI_MODE_MASK) >> 24;
|
||||
uint32_t port = (ddi & TRANS_DDI_PORT_MASK) >> 27;
|
||||
const char* suffix = port == 0 ? "A" : port == 1 ? "B"
|
||||
: port == 2 ? "C" : port == 3 ? "D"
|
||||
: port == 8 ? "TC1" : port == 9 ? "TC2"
|
||||
: port == 10 ? "TC3" : port == 11 ? "TC4"
|
||||
: "unknown";
|
||||
// Gen12 widened the port select to bits 30:27 and stores port + 1
|
||||
// (0 = none); earlier DDI parts store the port in bits 30:28.
|
||||
// Gen12 numbering puts the Type-C ports from D (index 3) onward.
|
||||
int port;
|
||||
if (g_gpuGen >= 12) {
|
||||
uint32_t field = (ddi & TRANS_DDI_PORT_MASK) >> 27;
|
||||
port = field ? (int)field - 1 : -1;
|
||||
} else {
|
||||
port = (int)((ddi >> 28) & 0x7);
|
||||
}
|
||||
g_ddiPort = port;
|
||||
static const char* const legacyNames[] = {"A", "B", "C", "D", "E", "F"};
|
||||
static const char* const tcNames[] = {"TC1", "TC2", "TC3", "TC4", "TC5", "TC6"};
|
||||
const char* suffix = "unknown";
|
||||
if (port >= 0 && g_gpuGen >= 12) {
|
||||
if (port < 3) suffix = legacyNames[port];
|
||||
else if (port - 3 < 6) suffix = tcNames[port - 3];
|
||||
} else if (port >= 0 && port < 6) {
|
||||
suffix = legacyNames[port];
|
||||
}
|
||||
if (mode == 0) {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_HDMI;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "HDMI-");
|
||||
@@ -824,11 +848,9 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
} else {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_DP;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "DP-");
|
||||
static constexpr uint32_t auxControls[] = {
|
||||
DP_AUX_A_CTL, DP_AUX_B_CTL, DP_AUX_C_CTL, DP_AUX_D_CTL,
|
||||
};
|
||||
if (port < sizeof(auxControls) / sizeof(auxControls[0]))
|
||||
g_auxControlReg = auxControls[port];
|
||||
// AUX channels are laid out per port, Type-C ones included.
|
||||
if (port >= 0 && port <= 8)
|
||||
g_auxControlReg = DP_AUX_A_CTL + (uint32_t)port * 0x100;
|
||||
}
|
||||
uint64_t len = 0;
|
||||
while (len + 1 < sizeof(g_connectorName) && g_connectorName[len]) len++;
|
||||
@@ -913,25 +935,544 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
}
|
||||
}
|
||||
|
||||
static void DiscoverPwm() {
|
||||
g_pwmSupported = false;
|
||||
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP
|
||||
&& g_connectorType != montauk::abi::DISPLAY_CONNECTOR_LVDS)
|
||||
return;
|
||||
// =========================================================================
|
||||
// Backlight
|
||||
// =========================================================================
|
||||
|
||||
uint32_t pch = ReadReg(BLC_PWM_PCH_CTL2);
|
||||
if ((pch >> 16) != 0) {
|
||||
g_pwmCtlReg = BLC_PWM_PCH_CTL2;
|
||||
g_pwmSupported = true;
|
||||
// South-display family. The PWM register layout and the GMBUS pin map
|
||||
// follow the PCH, not the GPU: a Coffee Lake on Cannon Point and a Kaby
|
||||
// Lake on Sunrise Point are both Gen 9 but have incompatible PWM blocks.
|
||||
enum class PchFamily : uint8_t { Unknown, Ibx, Lpt, Cnp, Icp };
|
||||
|
||||
enum class BacklightMethod : uint8_t { None, PwmIlk, PwmLpt, PwmCnp, DpcdAux, DdcCi };
|
||||
|
||||
static PchFamily g_pch = PchFamily::Unknown;
|
||||
static BacklightMethod g_blMethod = BacklightMethod::None;
|
||||
static uint32_t g_blFreqReg = 0; // PWM period register
|
||||
static uint32_t g_blDutyReg = 0; // PWM duty register
|
||||
static uint32_t g_blMax = 0; // DPCD / DDC full-scale value
|
||||
static int g_blPercent = -1; // last level read or written
|
||||
static bool g_dpcdModePending = false; // panel still on its PWM pin
|
||||
static bool g_ddcCandidate = false; // external monitor, DDC/CI untried
|
||||
static uint64_t g_ddcRetryMs = 0; // earliest re-probe after a failure
|
||||
static uint64_t g_ddcLastMs = 0; // MCCS needs 50ms between commands
|
||||
// Serializes backlight access: DDC/CI and DPCD changes are multi-step
|
||||
// AUX/GMBUS transactions that must not interleave.
|
||||
static kcp::Mutex g_blLock;
|
||||
|
||||
// Lowest level a panel is driven to. A zero duty cycle turns some panels
|
||||
// fully dark, which leaves nothing on screen to turn them back up with.
|
||||
static constexpr int MinPanelPercent = 1;
|
||||
|
||||
// MCCS DDC/CI (VESA Monitor Control Command Set over I2C address 0x37).
|
||||
static constexpr uint8_t DdcAddress = 0x37;
|
||||
static constexpr uint8_t DdcHostAddress = 0x51;
|
||||
static constexpr uint8_t DdcVcpBrightness = 0x10;
|
||||
static constexpr uint64_t DdcCommandGapMs = 50;
|
||||
static constexpr uint64_t DdcReplyDelayMs = 40;
|
||||
static constexpr uint64_t DdcRetryIntervalMs = 60000;
|
||||
|
||||
static constexpr uint32_t SOUTH_DSPCLK_GATE_D = 0xC2020;
|
||||
static constexpr uint32_t PCH_GMBUSUNIT_CLOCK_GATE_DISABLE = (1u << 31);
|
||||
|
||||
static PchFamily DetectPch() {
|
||||
auto& devices = Pci::GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.VendorId != VendorIntel || d.Bus != 0 || d.Device != 0x1F
|
||||
|| d.Function != 0)
|
||||
continue;
|
||||
// Device-ID families as i915 groups them (id & 0xFF80).
|
||||
switch (d.DeviceId & 0xFF80) {
|
||||
case 0x3B00: case 0x1C00: case 0x1E00:
|
||||
return PchFamily::Ibx;
|
||||
case 0x8C00: case 0x9C00: case 0x8C80: case 0x9C80: // LPT/WPT
|
||||
case 0xA100: case 0x9D00: case 0xA280: case 0xA380: // SPT/KBP/CMP-V
|
||||
return PchFamily::Lpt;
|
||||
case 0xA300: case 0x9D80: case 0x0280: case 0x0680: // CNP/CMP
|
||||
return PchFamily::Cnp;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Ice Point and everything after it share the Cannon Point PWM block
|
||||
// and the Ice Point pin map, and new PCH IDs keep appearing.
|
||||
return g_gpuGen >= 11 ? PchFamily::Icp : PchFamily::Unknown;
|
||||
}
|
||||
|
||||
static int RawToPercent(uint32_t raw, uint32_t maximum) {
|
||||
if (maximum == 0) return -1;
|
||||
if (raw > maximum) raw = maximum;
|
||||
return (int)(((uint64_t)raw * 100 + maximum / 2) / maximum);
|
||||
}
|
||||
|
||||
static uint32_t PercentToRaw(int percent, uint32_t maximum) {
|
||||
return (uint32_t)(((uint64_t)maximum * (uint32_t)percent + 50) / 100);
|
||||
}
|
||||
|
||||
static void ShortSpin() {
|
||||
for (int i = 0; i < 20000; i++) asm volatile("pause");
|
||||
}
|
||||
|
||||
// ---- Panel PWM ----------------------------------------------------------
|
||||
|
||||
static bool PwmState(uint32_t& maximum, uint32_t& duty) {
|
||||
uint32_t freq = ReadReg(g_blFreqReg);
|
||||
uint32_t value = ReadReg(g_blDutyReg);
|
||||
if (freq == 0xFFFFFFFF) return false;
|
||||
switch (g_blMethod) {
|
||||
case BacklightMethod::PwmCnp:
|
||||
maximum = freq;
|
||||
duty = value;
|
||||
break;
|
||||
case BacklightMethod::PwmLpt:
|
||||
maximum = value >> 16;
|
||||
duty = value & 0xFFFF;
|
||||
break;
|
||||
case BacklightMethod::PwmIlk:
|
||||
maximum = freq >> 16;
|
||||
duty = value & 0xFFFF;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return maximum != 0;
|
||||
}
|
||||
|
||||
static void PwmWriteDuty(uint32_t duty) {
|
||||
if (g_blMethod == BacklightMethod::PwmCnp) {
|
||||
WriteReg(g_blDutyReg, duty);
|
||||
} else {
|
||||
uint32_t cpu = ReadReg(BLC_PWM_CPU_CTL);
|
||||
if ((cpu >> 16) != 0) {
|
||||
g_pwmCtlReg = BLC_PWM_CPU_CTL;
|
||||
g_pwmSupported = true;
|
||||
uint32_t value = ReadReg(g_blDutyReg);
|
||||
WriteReg(g_blDutyReg, (value & 0xFFFF0000u) | (duty & 0xFFFF));
|
||||
}
|
||||
(void)ReadReg(g_blDutyReg);
|
||||
}
|
||||
|
||||
// Adopt the PWM controller firmware left driving the panel. A controller
|
||||
// that is not enabled is not what lights this panel, so it is skipped.
|
||||
static bool DiscoverPwmBacklight() {
|
||||
bool cnpLayout = g_pch == PchFamily::Cnp || g_pch == PchFamily::Icp;
|
||||
if (g_pch == PchFamily::Unknown && g_gpuGen >= 8) {
|
||||
// Unrecognized bridge: the LPT layout has nothing at DUTY1, so a
|
||||
// plausible duty there identifies the CNP block.
|
||||
uint32_t freq = ReadReg(BXT_BLC_PWM_FREQ1);
|
||||
uint32_t duty = ReadReg(BXT_BLC_PWM_DUTY1);
|
||||
cnpLayout = duty != 0 && duty != 0xFFFFFFFF && duty <= freq;
|
||||
}
|
||||
|
||||
if (cnpLayout) {
|
||||
// BXT/CNP firmware may route the panel through either controller.
|
||||
for (uint32_t c = 0; c < 2; c++) {
|
||||
uint32_t delta = c * BXT_BLC_PWM_CONTROLLER_DELTA;
|
||||
uint32_t ctl = ReadReg(BXT_BLC_PWM_CTL1 + delta);
|
||||
uint32_t freq = ReadReg(BXT_BLC_PWM_FREQ1 + delta);
|
||||
if (ctl == 0xFFFFFFFF || !(ctl & BLC_PWM_ENABLE)
|
||||
|| freq == 0 || freq == 0xFFFFFFFF)
|
||||
continue;
|
||||
g_blMethod = BacklightMethod::PwmCnp;
|
||||
g_blFreqReg = BXT_BLC_PWM_FREQ1 + delta;
|
||||
g_blDutyReg = BXT_BLC_PWM_DUTY1 + delta;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g_pch == PchFamily::Lpt || g_gpuGen >= 8) {
|
||||
uint32_t ctl1 = ReadReg(BLC_PWM_PCH_CTL1);
|
||||
uint32_t ctl2 = ReadReg(BLC_PWM_PCH_CTL2);
|
||||
if (ctl1 == 0xFFFFFFFF || !(ctl1 & BLC_PWM_ENABLE) || (ctl2 >> 16) == 0)
|
||||
return false;
|
||||
g_blMethod = BacklightMethod::PwmLpt;
|
||||
g_blFreqReg = BLC_PWM_PCH_CTL2;
|
||||
g_blDutyReg = BLC_PWM_PCH_CTL2;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t pch2 = ReadReg(BLC_PWM_PCH_CTL2);
|
||||
uint32_t cpu2 = ReadReg(BLC_PWM_CPU_CTL2);
|
||||
if (pch2 == 0xFFFFFFFF || (pch2 >> 16) == 0 || !(cpu2 & BLC_PWM_ENABLE))
|
||||
return false;
|
||||
g_blMethod = BacklightMethod::PwmIlk;
|
||||
g_blFreqReg = BLC_PWM_PCH_CTL2;
|
||||
g_blDutyReg = BLC_PWM_CPU_CTL;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- eDP DPCD backlight -------------------------------------------------
|
||||
|
||||
// Native AUX read or write of DPCD registers, retrying sink DEFERs.
|
||||
static bool DpcdAccess(bool write, uint32_t address, uint8_t* data, int size) {
|
||||
for (int attempt = 0; attempt < 8; attempt++) {
|
||||
uint8_t reply = 0xFF;
|
||||
int result = write
|
||||
? AuxTransfer(0x8, address, data, size, nullptr, 0, &reply)
|
||||
: AuxTransfer(0x9, address, nullptr, 0, data, size, &reply);
|
||||
if (result == size && (reply & 0x3) == 0) return true;
|
||||
if (result < 0 || (reply & 0x3) != 0x2) return false;
|
||||
ShortSpin();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Before Gen 12 the eDP output has its own transcoder; from Gen 12 it is
|
||||
// an ordinary DDI in DP mode, so ask the sink. Only eDP sinks implement
|
||||
// the 0x700 eDP register block or EDP_CONFIGURATION_CAP.
|
||||
static void DetectEmbeddedDp() {
|
||||
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP || g_auxControlReg == 0)
|
||||
return;
|
||||
uint8_t edpRev = 0;
|
||||
uint8_t edpConfig = 0;
|
||||
bool edp = DpcdAccess(false, DPCD_EDP_REV, &edpRev, 1) && edpRev != 0;
|
||||
if (!edp) edp = DpcdAccess(false, 0x00D, &edpConfig, 1) && edpConfig != 0;
|
||||
if (!edp) return;
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1");
|
||||
KernelLogStream(INFO, "IntelGPU") << "DDI output identified as eDP (eDP DPCD rev "
|
||||
<< base::hex << (uint64_t)edpRev << ")";
|
||||
}
|
||||
|
||||
static int DpcdBrightnessBytes() {
|
||||
return g_blMax > 0xFF ? 2 : 1;
|
||||
}
|
||||
|
||||
// Returns 0 when the panel offers no AUX brightness control, 1 when it
|
||||
// does but is still driven from its PWM pin, 2 when DPCD already owns it.
|
||||
static int ProbeDpcdBacklight() {
|
||||
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP || g_auxControlReg == 0)
|
||||
return 0;
|
||||
uint8_t caps[3] = {};
|
||||
if (!DpcdAccess(false, DPCD_EDP_REV, caps, 3)) return 0;
|
||||
if (caps[0] == 0 || !(caps[2] & EDP_BLCAP_AUX_SET)) return 0;
|
||||
uint8_t mode = 0;
|
||||
if (!DpcdAccess(false, DPCD_EDP_BACKLIGHT_MODE, &mode, 1)) return 0;
|
||||
g_blMax = (caps[2] & EDP_BLCAP_BYTE_COUNT_16) ? 0xFFFF : 0xFF;
|
||||
return (mode & EDP_BLMODE_MASK) == EDP_BLMODE_DPCD ? 2 : 1;
|
||||
}
|
||||
|
||||
static int DpcdReadPercent() {
|
||||
uint8_t bytes[2] = {};
|
||||
int count = DpcdBrightnessBytes();
|
||||
if (!DpcdAccess(false, DPCD_EDP_BRIGHTNESS_MSB, bytes, count)) return -1;
|
||||
uint32_t raw = count == 2 ? ((uint32_t)bytes[0] << 8 | bytes[1]) : bytes[0];
|
||||
return RawToPercent(raw, g_blMax);
|
||||
}
|
||||
|
||||
static bool DpcdWritePercent(int percent) {
|
||||
uint32_t raw = PercentToRaw(percent, g_blMax);
|
||||
if (raw == 0) raw = 1;
|
||||
uint8_t bytes[2];
|
||||
int count = DpcdBrightnessBytes();
|
||||
if (count == 2) {
|
||||
bytes[0] = (uint8_t)(raw >> 8);
|
||||
bytes[1] = (uint8_t)raw;
|
||||
} else {
|
||||
bytes[0] = (uint8_t)raw;
|
||||
}
|
||||
if (!DpcdAccess(true, DPCD_EDP_BRIGHTNESS_MSB, bytes, count)) return false;
|
||||
if (!g_dpcdModePending) return true;
|
||||
|
||||
// First change on a panel that was on its PWM pin: the level is
|
||||
// already programmed, so switching the source cannot flash it dark.
|
||||
uint8_t mode = 0;
|
||||
if (!DpcdAccess(false, DPCD_EDP_BACKLIGHT_MODE, &mode, 1)) return false;
|
||||
mode = (uint8_t)((mode & ~EDP_BLMODE_MASK) | EDP_BLMODE_DPCD);
|
||||
if (!DpcdAccess(true, DPCD_EDP_BACKLIGHT_MODE, &mode, 1)) return false;
|
||||
g_dpcdModePending = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- GMBUS (HDMI/DVI DDC) -----------------------------------------------
|
||||
|
||||
static int GmbusPinForPort(int port) {
|
||||
if (port < 0) return -1;
|
||||
switch (g_pch) {
|
||||
case PchFamily::Icp: {
|
||||
// Combo PHY ports use pins 1-3; Type-C ports start at pin 9.
|
||||
// Gen 11 numbers its first Type-C port C, Gen 12 numbers it D.
|
||||
int firstTc = g_gpuGen >= 12 ? 3 : 2;
|
||||
if (port < firstTc) return port + 1;
|
||||
int pin = 9 + (port - firstTc);
|
||||
return pin <= 14 ? pin : -1;
|
||||
}
|
||||
case PchFamily::Cnp:
|
||||
return port == 1 ? 1 : port == 2 ? 2 : port == 3 ? 4 : -1;
|
||||
default:
|
||||
return port == 1 ? 5 : port == 2 ? 4 : port == 3 ? 6 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool GmbusWait(uint32_t bit) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
for (;;) {
|
||||
uint32_t status = ReadReg(GMBUS2);
|
||||
if (status & GMBUS_SATOER) return false;
|
||||
if (status & bit) return true;
|
||||
if (Timekeeping::GetMilliseconds() - start > 50) return false;
|
||||
asm volatile("pause");
|
||||
}
|
||||
}
|
||||
|
||||
static bool GmbusWaitIdle() {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (ReadReg(GMBUS2) & GMBUS_ACTIVE) {
|
||||
if (Timekeeping::GetMilliseconds() - start > 50) return false;
|
||||
asm volatile("pause");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// One complete I2C message (START .. STOP) at 100 kHz.
|
||||
static bool GmbusTransfer(int pin, uint8_t address, bool read,
|
||||
uint8_t* data, int size) {
|
||||
if (pin < 0 || size <= 0 || size > 256) return false;
|
||||
|
||||
uint32_t gate = 0;
|
||||
if (g_pch == PchFamily::Cnp) {
|
||||
// Cannon Point drops GMBUS transfers while the unit clock gates.
|
||||
gate = ReadReg(SOUTH_DSPCLK_GATE_D);
|
||||
WriteReg(SOUTH_DSPCLK_GATE_D, gate | PCH_GMBUSUNIT_CLOCK_GATE_DISABLE);
|
||||
}
|
||||
|
||||
WriteReg(GMBUS0, (uint32_t)pin);
|
||||
uint32_t command = GMBUS_SW_RDY | GMBUS_CYCLE_WAIT
|
||||
| ((uint32_t)size << GMBUS_BYTE_COUNT_SHIFT)
|
||||
| ((uint32_t)address << 1) | (read ? GMBUS_SLAVE_READ : 0);
|
||||
bool ok = true;
|
||||
int pos = 0;
|
||||
if (read) {
|
||||
WriteReg(GMBUS1, command);
|
||||
while (ok && pos < size) {
|
||||
ok = GmbusWait(GMBUS_HW_RDY);
|
||||
if (!ok) break;
|
||||
uint32_t value = ReadReg(GMBUS3);
|
||||
for (int i = 0; i < 4 && pos < size; i++, pos++)
|
||||
data[pos] = (uint8_t)(value >> (8 * i));
|
||||
}
|
||||
} else {
|
||||
// The first dword is loaded before the command starts the cycle.
|
||||
uint32_t value = 0;
|
||||
for (int i = 0; i < 4 && pos < size; i++, pos++)
|
||||
value |= (uint32_t)data[pos] << (8 * i);
|
||||
WriteReg(GMBUS3, value);
|
||||
WriteReg(GMBUS1, command);
|
||||
while (ok && pos < size) {
|
||||
ok = GmbusWait(GMBUS_HW_RDY);
|
||||
if (!ok) break;
|
||||
value = 0;
|
||||
for (int i = 0; i < 4 && pos < size; i++, pos++)
|
||||
value |= (uint32_t)data[pos] << (8 * i);
|
||||
WriteReg(GMBUS3, value);
|
||||
}
|
||||
}
|
||||
KernelLogStream(INFO, "IntelGPU") << "Panel PWM "
|
||||
<< (g_pwmSupported ? "available" : "not exposed");
|
||||
if (ok) ok = GmbusWait(GMBUS_HW_WAIT_PHASE);
|
||||
|
||||
if (ok) {
|
||||
WriteReg(GMBUS1, GMBUS_SW_RDY | GMBUS_CYCLE_STOP);
|
||||
ok = GmbusWaitIdle();
|
||||
} else {
|
||||
// NAK or timeout: clear the error, let the bus go idle.
|
||||
WriteReg(GMBUS1, GMBUS_SW_CLR_INT);
|
||||
(void)GmbusWaitIdle();
|
||||
WriteReg(GMBUS1, 0);
|
||||
}
|
||||
WriteReg(GMBUS0, 0);
|
||||
|
||||
if (g_pch == PchFamily::Cnp) WriteReg(SOUTH_DSPCLK_GATE_D, gate);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---- DDC/CI (external monitors) -----------------------------------------
|
||||
|
||||
static bool DdcUsesAux() {
|
||||
return g_connectorType == montauk::abi::DISPLAY_CONNECTOR_DP;
|
||||
}
|
||||
|
||||
static bool DdcWrite(uint8_t* data, int size) {
|
||||
if (DdcUsesAux()) {
|
||||
if (size > 16) return false;
|
||||
bool ok = AuxI2cTransfer(0x4, data, size, nullptr, 0, DdcAddress);
|
||||
// Address-only write without MOT ends the I2C transaction.
|
||||
bool stopped = AuxI2cTransfer(0x0, nullptr, 0, nullptr, 0, DdcAddress);
|
||||
return ok && stopped;
|
||||
}
|
||||
return GmbusTransfer(GmbusPinForPort(g_ddiPort), DdcAddress, false, data, size);
|
||||
}
|
||||
|
||||
static bool DdcRead(uint8_t* data, int size) {
|
||||
if (DdcUsesAux()) {
|
||||
if (size > 16) return false;
|
||||
if (!AuxI2cTransfer(0x5, nullptr, 0, nullptr, 0, DdcAddress)) return false;
|
||||
return AuxI2cTransfer(0x1, nullptr, 0, data, size, DdcAddress);
|
||||
}
|
||||
return GmbusTransfer(GmbusPinForPort(g_ddiPort), DdcAddress, true, data, size);
|
||||
}
|
||||
|
||||
static void DdcPace() {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_ddcLastMs != 0 && now - g_ddcLastMs < DdcCommandGapMs)
|
||||
Timekeeping::Sleep(DdcCommandGapMs - (now - g_ddcLastMs));
|
||||
}
|
||||
|
||||
// Send an MCCS command: host source address, length, payload, checksum.
|
||||
// The checksum covers the monitor's 8-bit write address (0x6E) too.
|
||||
static bool DdcCommand(const uint8_t* payload, int length) {
|
||||
uint8_t packet[16];
|
||||
if (length + 3 > (int)sizeof(packet)) return false;
|
||||
packet[0] = DdcHostAddress;
|
||||
packet[1] = (uint8_t)(0x80 | length);
|
||||
uint8_t checksum = (uint8_t)(DdcAddress << 1) ^ packet[0] ^ packet[1];
|
||||
for (int i = 0; i < length; i++) {
|
||||
packet[2 + i] = payload[i];
|
||||
checksum ^= payload[i];
|
||||
}
|
||||
packet[2 + length] = checksum;
|
||||
DdcPace();
|
||||
bool ok = DdcWrite(packet, length + 3);
|
||||
g_ddcLastMs = Timekeeping::GetMilliseconds();
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool DdcGetBrightness(uint32_t& current, uint32_t& maximum) {
|
||||
uint8_t request[2] = {0x01, DdcVcpBrightness}; // Get VCP Feature
|
||||
if (!DdcCommand(request, 2)) return false;
|
||||
Timekeeping::Sleep(DdcReplyDelayMs);
|
||||
|
||||
// Reply: 6E 88 02 rc 10 type maxH maxL curH curL checksum. The
|
||||
// checksum covers the host's virtual address 0x50.
|
||||
uint8_t reply[11] = {};
|
||||
bool ok = DdcRead(reply, sizeof(reply));
|
||||
g_ddcLastMs = Timekeeping::GetMilliseconds();
|
||||
if (!ok) return false;
|
||||
uint8_t checksum = 0x50;
|
||||
for (int i = 0; i < 10; i++) checksum ^= reply[i];
|
||||
if (checksum != reply[10] || reply[1] != 0x88 || reply[2] != 0x02
|
||||
|| reply[3] != 0x00 || reply[4] != DdcVcpBrightness)
|
||||
return false;
|
||||
maximum = (uint32_t)reply[6] << 8 | reply[7];
|
||||
current = (uint32_t)reply[8] << 8 | reply[9];
|
||||
return maximum != 0;
|
||||
}
|
||||
|
||||
static bool DdcSetBrightness(uint32_t value) {
|
||||
uint8_t request[4] = {0x03, DdcVcpBrightness, // Set VCP Feature
|
||||
(uint8_t)(value >> 8), (uint8_t)value};
|
||||
return DdcCommand(request, 4);
|
||||
}
|
||||
|
||||
// DDC/CI takes tens of milliseconds and needs the scheduler, so it is
|
||||
// probed on first use from a syscall rather than during boot.
|
||||
static bool EnsureDdc() {
|
||||
if (g_blMethod == BacklightMethod::DdcCi) return true;
|
||||
if (!g_ddcCandidate) return false;
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_ddcRetryMs != 0 && now < g_ddcRetryMs) return false;
|
||||
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
uint32_t current = 0, maximum = 0;
|
||||
if (!DdcGetBrightness(current, maximum)) continue;
|
||||
g_blMethod = BacklightMethod::DdcCi;
|
||||
g_blMax = maximum;
|
||||
g_blPercent = RawToPercent(current, maximum);
|
||||
KernelLogStream(OK, "IntelGPU") << "Monitor brightness via DDC/CI ("
|
||||
<< base::dec << (uint64_t)current << "/" << (uint64_t)maximum << ")";
|
||||
return true;
|
||||
}
|
||||
g_ddcRetryMs = Timekeeping::GetMilliseconds() + DdcRetryIntervalMs;
|
||||
KernelLogStream(INFO, "IntelGPU") << "Monitor did not answer DDC/CI brightness query";
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Discovery and dispatch ---------------------------------------------
|
||||
|
||||
static void DiscoverBacklight() {
|
||||
g_blMethod = BacklightMethod::None;
|
||||
g_blPercent = -1;
|
||||
g_dpcdModePending = false;
|
||||
g_ddcCandidate = false;
|
||||
g_pch = DetectPch();
|
||||
DetectEmbeddedDp();
|
||||
|
||||
bool panel = g_connectorType == montauk::abi::DISPLAY_CONNECTOR_EDP
|
||||
|| g_connectorType == montauk::abi::DISPLAY_CONNECTOR_LVDS;
|
||||
if (panel) {
|
||||
// A panel firmware already put in DPCD mode ignores its PWM pin.
|
||||
// Otherwise PWM is what firmware lit it with; take DPCD over only
|
||||
// when there is no live PWM controller.
|
||||
int dpcd = ProbeDpcdBacklight();
|
||||
if (dpcd == 2) {
|
||||
g_blMethod = BacklightMethod::DpcdAux;
|
||||
} else if (!DiscoverPwmBacklight() && dpcd == 1) {
|
||||
g_blMethod = BacklightMethod::DpcdAux;
|
||||
g_dpcdModePending = true;
|
||||
g_blPercent = 100;
|
||||
}
|
||||
} else if (g_gpuGen >= 8 && g_ddiPort >= 0
|
||||
&& (g_connectorType == montauk::abi::DISPLAY_CONNECTOR_HDMI
|
||||
|| g_connectorType == montauk::abi::DISPLAY_CONNECTOR_DVI
|
||||
|| (g_connectorType == montauk::abi::DISPLAY_CONNECTOR_DP
|
||||
&& g_auxControlReg != 0))) {
|
||||
g_ddcCandidate = DdcUsesAux() || GmbusPinForPort(g_ddiPort) >= 0;
|
||||
}
|
||||
|
||||
const char* pch = g_pch == PchFamily::Ibx ? "IBX" : g_pch == PchFamily::Lpt ? "LPT"
|
||||
: g_pch == PchFamily::Cnp ? "CNP" : g_pch == PchFamily::Icp ? "ICP+"
|
||||
: "unknown";
|
||||
if (g_blMethod != BacklightMethod::None) {
|
||||
KernelLogStream(INFO, "IntelGPU") << "Backlight: " << BrightnessMethod()
|
||||
<< (g_dpcdModePending ? " (on first change)" : "")
|
||||
<< ", PCH " << pch;
|
||||
} else {
|
||||
KernelLogStream(INFO, "IntelGPU") << "Backlight: "
|
||||
<< (g_ddcCandidate ? "DDC/CI probe deferred" : "not exposed")
|
||||
<< ", PCH " << pch;
|
||||
}
|
||||
}
|
||||
|
||||
static int BrightnessLocked(int percent) {
|
||||
if (percent > 100) percent = 100;
|
||||
switch (g_blMethod) {
|
||||
case BacklightMethod::PwmIlk:
|
||||
case BacklightMethod::PwmLpt:
|
||||
case BacklightMethod::PwmCnp: {
|
||||
uint32_t maximum = 0, duty = 0;
|
||||
if (!PwmState(maximum, duty)) return -1;
|
||||
if (percent >= 0) {
|
||||
if (percent < MinPanelPercent) percent = MinPanelPercent;
|
||||
uint32_t raw = PercentToRaw(percent, maximum);
|
||||
PwmWriteDuty(raw ? raw : 1);
|
||||
if (!PwmState(maximum, duty)) return -2;
|
||||
}
|
||||
g_blPercent = RawToPercent(duty, maximum);
|
||||
return g_blPercent;
|
||||
}
|
||||
case BacklightMethod::DpcdAux: {
|
||||
if (percent < 0) {
|
||||
if (g_dpcdModePending) return g_blPercent;
|
||||
int level = DpcdReadPercent();
|
||||
if (level >= 0) g_blPercent = level;
|
||||
return g_blPercent;
|
||||
}
|
||||
if (percent < MinPanelPercent) percent = MinPanelPercent;
|
||||
if (!DpcdWritePercent(percent)) return -2;
|
||||
int level = DpcdReadPercent();
|
||||
g_blPercent = level >= 0 ? level : percent;
|
||||
return g_blPercent;
|
||||
}
|
||||
case BacklightMethod::None:
|
||||
if (!EnsureDdc()) return -1;
|
||||
[[fallthrough]];
|
||||
case BacklightMethod::DdcCi: {
|
||||
// Monitors answer queries slowly, and the OSD is the only
|
||||
// other writer, so the last known level stands in.
|
||||
if (percent < 0) return g_blPercent;
|
||||
if (!DdcSetBrightness(PercentToRaw(percent, g_blMax))) return -2;
|
||||
g_blPercent = percent;
|
||||
return g_blPercent;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void InitializeDisplayManagement() {
|
||||
@@ -942,7 +1483,7 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
DiscoverActiveTranscoder();
|
||||
ParseEdid();
|
||||
SelectCurrentMode();
|
||||
DiscoverPwm();
|
||||
DiscoverBacklight();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -1660,7 +2201,6 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
|
||||
out->capabilities = montauk::abi::DISPLAY_CAP_MODESET;
|
||||
if (g_edidValid) out->capabilities |= montauk::abi::DISPLAY_CAP_EDID;
|
||||
if (g_pwmSupported) out->capabilities |= montauk::abi::DISPLAY_CAP_BRIGHTNESS;
|
||||
if (g_flipSupported) out->capabilities |= montauk::abi::DISPLAY_CAP_PAGE_FLIP;
|
||||
for (int i = 0; i < g_modeCount; i++) {
|
||||
if (i != g_currentMode
|
||||
@@ -1672,6 +2212,7 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
out->modeCount = (uint32_t)g_modeCount;
|
||||
out->currentMode = g_currentMode;
|
||||
out->brightness = Brightness(-1);
|
||||
if (out->brightness >= 0) out->capabilities |= montauk::abi::DISPLAY_CAP_BRIGHTNESS;
|
||||
out->deviceId = g_gpuInfo.deviceId;
|
||||
out->generation = g_gpuInfo.gen;
|
||||
out->connectorType = g_connectorType;
|
||||
@@ -1737,20 +2278,28 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
}
|
||||
|
||||
int Brightness(int percent) {
|
||||
if (!g_initialized || !g_pwmSupported) return -1;
|
||||
uint32_t value = ReadReg(g_pwmCtlReg);
|
||||
uint32_t maximum = value >> 16;
|
||||
if (maximum == 0) return -1;
|
||||
if (percent < 0) {
|
||||
uint32_t duty = value & 0xFFFF;
|
||||
if (duty > maximum) duty = maximum;
|
||||
return (int)(((uint64_t)duty * 100 + maximum / 2) / maximum);
|
||||
if (!g_initialized) return -1;
|
||||
if (g_blMethod == BacklightMethod::None && !g_ddcCandidate) return -1;
|
||||
g_blLock.Acquire();
|
||||
int result = BrightnessLocked(percent);
|
||||
g_blLock.Release();
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t ActiveConnectorType() {
|
||||
return g_connectorType;
|
||||
}
|
||||
|
||||
const char* BrightnessMethod() {
|
||||
switch (g_blMethod) {
|
||||
case BacklightMethod::PwmIlk:
|
||||
case BacklightMethod::PwmLpt:
|
||||
case BacklightMethod::PwmCnp: return "PWM";
|
||||
case BacklightMethod::DpcdAux: return "eDP AUX";
|
||||
case BacklightMethod::DdcCi: return "DDC/CI";
|
||||
case BacklightMethod::None: break;
|
||||
}
|
||||
if (percent > 100) percent = 100;
|
||||
uint32_t duty = (uint32_t)(((uint64_t)maximum * (uint32_t)percent + 50) / 100);
|
||||
WriteReg(g_pwmCtlReg, (value & 0xFFFF0000u) | duty);
|
||||
(void)ReadReg(g_pwmCtlReg);
|
||||
return Brightness(-1);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -1993,6 +2542,12 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
<< base::dec << (uint64_t)g_frontBuffer << ")";
|
||||
}
|
||||
|
||||
// 7. Firmware POST resets the PWM duty to its own default. Put back
|
||||
// the level the user chose (register writes only; no AUX here).
|
||||
uint32_t pwmMax = 0, pwmDuty = 0;
|
||||
if (g_blPercent > 0 && PwmState(pwmMax, pwmDuty))
|
||||
PwmWriteDuty(PercentToRaw(g_blPercent, pwmMax));
|
||||
|
||||
KernelLogStream(OK, "IntelGPU") << "Display restored after S3 resume";
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user