2555 lines
106 KiB
C++
2555 lines
106 KiB
C++
/*
|
|
* IntelGPU.cpp
|
|
* Intel integrated graphics (i915) modesetting driver
|
|
* Scans PCI for Intel display controllers, maps MMIO, initializes GTT,
|
|
* and sets up a framebuffer using the firmware's existing display timings.
|
|
* Copyright (c) 2025 Daniel Hammer
|
|
*/
|
|
|
|
#include "IntelGPU.hpp"
|
|
#include <Pci/Pci.hpp>
|
|
#include <Terminal/Terminal.hpp>
|
|
#include <CppLib/Stream.hpp>
|
|
#include <CppLib/Spinlock.hpp>
|
|
#include <Memory/HHDM.hpp>
|
|
#include <Memory/Paging.hpp>
|
|
#include <Memory/PageFrameAllocator.hpp>
|
|
#include <Io/IoPort.hpp>
|
|
#include <Graphics/Framebuffer.hpp>
|
|
#include <Libraries/Memory.hpp>
|
|
#include <Timekeeping/ApicTimer.hpp>
|
|
|
|
using namespace Kt;
|
|
|
|
namespace Drivers::Graphics::IntelGPU {
|
|
|
|
// =========================================================================
|
|
// Driver state
|
|
// =========================================================================
|
|
|
|
static bool g_initialized = false;
|
|
|
|
static GpuInfo g_gpuInfo = {};
|
|
|
|
static volatile uint8_t* g_mmioBase = nullptr;
|
|
|
|
// GTT state
|
|
static volatile void* g_gttBase = nullptr; // Virtual address of GTT entries
|
|
static uint64_t g_gttEntryCount = 0; // Number of GTT entries
|
|
static uint8_t g_gpuGen = 0; // Cached generation number
|
|
|
|
// Framebuffer state
|
|
static uint32_t* g_fbBase = nullptr; // HHDM virtual address of first FB page
|
|
static uint64_t g_fbPhysBase = 0; // Physical address of first FB page
|
|
static uint64_t g_fbWidth = 0;
|
|
static uint64_t g_fbHeight = 0;
|
|
static uint64_t g_fbPitch = 0; // Stride in bytes
|
|
static uint64_t g_fbSize = 0; // Total framebuffer size in bytes
|
|
static uint64_t g_fbGttOffset = 0; // GTT offset where FB starts (in bytes)
|
|
|
|
// Page flip state
|
|
static volatile void* g_ggtt = nullptr; // GGTT at the gen-correct BAR0 offset
|
|
static uint64_t g_ggttEntries = 0;
|
|
static uint64_t g_fbGttOffsetA = 0; // GGTT offset of buffer 0 (firmware FB)
|
|
static uint32_t* g_buf1Virt = nullptr; // buffer 1 kernel VA (HHDM, WC)
|
|
static uint64_t g_buf1Phys = 0;
|
|
static uint64_t g_buf1Pages = 0; // allocation/mapping bound
|
|
static uint64_t g_buf1GttOffset = 0; // GGTT offset of buffer 1 (in bytes)
|
|
static bool g_flipSupported = false;
|
|
static int g_frontBuffer = 0;
|
|
static int g_flipOwnerPid = -1;
|
|
// Serializes surface programming, flip-owner teardown, and timing changes.
|
|
// Without this, a process exit or modeset on another CPU can rewrite
|
|
// DSPASURF/PIPE state in the middle of a desktop flip transaction.
|
|
static kcp::Mutex g_flipLock;
|
|
|
|
// Modesetting/display-management state. The firmware-trained port and
|
|
// link are deliberately retained; the driver owns the active transcoder,
|
|
// timing catalogue, panel PWM, and validated timing transactions.
|
|
static constexpr int MaxDisplayModes = 32;
|
|
struct ModeRecord {
|
|
DisplayMode timing;
|
|
uint32_t refreshMilliHz;
|
|
uint32_t flags;
|
|
};
|
|
static ModeRecord g_modes[MaxDisplayModes] = {};
|
|
static int g_modeCount = 0;
|
|
static int g_currentMode = -1;
|
|
static uint32_t g_transTimingBase = TRANS_TIMING_A;
|
|
static uint32_t g_transConfReg = TRANS_CONF_A;
|
|
static uint32_t g_transDdiReg = TRANS_DDI_A;
|
|
static uint32_t g_auxControlReg = 0;
|
|
static uint8_t g_connectorType = montauk::abi::DISPLAY_CONNECTOR_UNKNOWN;
|
|
static bool g_connectorConnected = false;
|
|
static bool g_edidValid = false;
|
|
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] = {};
|
|
static uint64_t g_auxEdidSize = 0;
|
|
static uint32_t g_auxLastStatus = 0;
|
|
static uint8_t g_auxLastReply = 0xFF;
|
|
|
|
static uint64_t ReadGgttPte(uint64_t index);
|
|
|
|
// =========================================================================
|
|
// Register access helpers
|
|
// =========================================================================
|
|
|
|
static void WriteReg(uint32_t reg, uint32_t val) {
|
|
*(volatile uint32_t*)(g_mmioBase + reg) = val;
|
|
}
|
|
|
|
static uint32_t ReadReg(uint32_t reg) {
|
|
return *(volatile uint32_t*)(g_mmioBase + reg);
|
|
}
|
|
|
|
static void CopyText(char* dst, uint64_t cap, const char* src) {
|
|
if (!dst || cap == 0) return;
|
|
uint64_t i = 0;
|
|
if (src) {
|
|
while (i + 1 < cap && src[i]) {
|
|
dst[i] = src[i];
|
|
i++;
|
|
}
|
|
}
|
|
dst[i] = '\0';
|
|
}
|
|
|
|
static void CopyEdidText(char* dst, uint64_t cap, const uint8_t* src, int len) {
|
|
if (!dst || cap == 0) return;
|
|
int end = len;
|
|
while (end > 0 && (src[end - 1] == ' ' || src[end - 1] == '\n'
|
|
|| src[end - 1] == '\r' || src[end - 1] == '\0')) {
|
|
end--;
|
|
}
|
|
uint64_t out = 0;
|
|
for (int i = 0; i < end && out + 1 < cap; i++) {
|
|
uint8_t ch = src[i];
|
|
dst[out++] = (ch >= 0x20 && ch <= 0x7e) ? (char)ch : '?';
|
|
}
|
|
dst[out] = '\0';
|
|
}
|
|
|
|
// =========================================================================
|
|
// PCI Detection
|
|
// =========================================================================
|
|
|
|
static bool DetectGpu() {
|
|
auto& devices = Pci::GetDevices();
|
|
const Pci::PciDevice* found = nullptr;
|
|
const DeviceInfo* matchedInfo = nullptr;
|
|
|
|
for (uint64_t i = 0; i < devices.size(); i++) {
|
|
if (devices[i].VendorId != VendorIntel) {
|
|
continue;
|
|
}
|
|
if (devices[i].ClassCode != ClassDisplay) {
|
|
continue;
|
|
}
|
|
|
|
// Found an Intel display controller; try to match device ID
|
|
found = &devices[i];
|
|
|
|
for (int j = 0; j < SupportedDeviceCount; j++) {
|
|
if (devices[i].DeviceId == SupportedDevices[j].deviceId) {
|
|
matchedInfo = &SupportedDevices[j];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Stop at first Intel display controller
|
|
break;
|
|
}
|
|
|
|
if (found == nullptr) {
|
|
KernelLogStream(WARNING, "IntelGPU") << "No Intel display controller found";
|
|
return false;
|
|
}
|
|
|
|
g_gpuInfo.pciBus = found->Bus;
|
|
g_gpuInfo.pciDevice = found->Device;
|
|
g_gpuInfo.pciFunction = found->Function;
|
|
g_gpuInfo.deviceId = found->DeviceId;
|
|
|
|
if (matchedInfo != nullptr) {
|
|
g_gpuInfo.gen = matchedInfo->gen;
|
|
g_gpuInfo.name = matchedInfo->name;
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "Found " << matchedInfo->name
|
|
<< " (device " << base::hex << (uint64_t)found->DeviceId << ")"
|
|
<< " at PCI " << (uint64_t)found->Bus << ":"
|
|
<< (uint64_t)found->Device << "." << (uint64_t)found->Function;
|
|
} else {
|
|
// Unknown device ID - infer generation from device ID range.
|
|
// Getting this wrong is catastrophic (32-bit vs 64-bit GTT PTEs),
|
|
// so use known Intel device ID patterns.
|
|
uint16_t did = found->DeviceId;
|
|
uint8_t inferredGen;
|
|
if (did >= 0xA700 || (did >= 0x4600 && did < 0x4700) ||
|
|
(did >= 0x5600 && did < 0x5700) || (did >= 0x7D00 && did < 0x7E00)) {
|
|
inferredGen = 12; // Raptor Lake, Alder Lake, DG2, Meteor Lake range
|
|
} else if ((did >= 0x8A00 && did < 0x8B00) ||
|
|
(did >= 0x9A00 && did < 0x9B00)) {
|
|
inferredGen = 12; // Ice Lake / Tiger Lake range
|
|
} else if ((did >= 0x3E00 && did < 0x3F00) ||
|
|
(did >= 0x5900 && did < 0x5A00) ||
|
|
(did >= 0x9B00 && did < 0x9C00) ||
|
|
(did >= 0x1900 && did < 0x1A00)) {
|
|
inferredGen = 9; // Skylake / Kaby Lake / Coffee Lake / Comet Lake
|
|
} else if (did >= 0x1600 && did < 0x1700) {
|
|
inferredGen = 8; // Broadwell
|
|
} else {
|
|
inferredGen = 7; // Older (Haswell and below)
|
|
}
|
|
g_gpuInfo.gen = inferredGen;
|
|
g_gpuInfo.name = "Intel GPU";
|
|
|
|
KernelLogStream(WARNING, "IntelGPU") << "Unknown Intel display controller "
|
|
<< "(device " << base::hex << (uint64_t)found->DeviceId << ")"
|
|
<< " at PCI " << (uint64_t)found->Bus << ":"
|
|
<< (uint64_t)found->Device << "." << (uint64_t)found->Function
|
|
<< " - inferred gen " << base::dec << (uint64_t)inferredGen
|
|
<< ", attempting generic initialization";
|
|
}
|
|
|
|
g_gpuGen = g_gpuInfo.gen;
|
|
return true;
|
|
}
|
|
|
|
// This driver intentionally uses register polling for display latches and
|
|
// does not own the GPU's many generation-specific interrupt sources. Mask
|
|
// PCI delivery before touching display state: otherwise firmware-left MSI
|
|
// enables can route an unhandled GT interrupt and create an interrupt
|
|
// storm even though Montauk never requested one.
|
|
static void DisableInterruptDelivery() {
|
|
uint8_t bus = g_gpuInfo.pciBus;
|
|
uint8_t dev = g_gpuInfo.pciDevice;
|
|
uint8_t func = g_gpuInfo.pciFunction;
|
|
if (Pci::DisableInterruptDelivery(bus, dev, func)) {
|
|
KernelLogStream(WARNING, "IntelGPU")
|
|
<< "Disabled pre-enabled GPU MSI/MSI-X; using bounded latch polling";
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// BAR0 MMIO Mapping
|
|
// =========================================================================
|
|
|
|
static bool MapMmio() {
|
|
uint8_t bus = g_gpuInfo.pciBus;
|
|
uint8_t dev = g_gpuInfo.pciDevice;
|
|
uint8_t func = g_gpuInfo.pciFunction;
|
|
|
|
// Read BAR0
|
|
uint64_t mmioPhys = Pci::ReadBar0(bus, dev, func);
|
|
|
|
g_gpuInfo.mmioPhys = mmioPhys;
|
|
g_gpuInfo.mmioSize = 0x200000; // Map 2MB of MMIO space
|
|
|
|
KernelLogStream(INFO, "IntelGPU") << "BAR0 physical: " << base::hex << mmioPhys;
|
|
|
|
// Map 2MB of MMIO space (0x200 pages)
|
|
for (uint64_t offset = 0; offset < 0x200000; offset += 0x1000) {
|
|
Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset));
|
|
}
|
|
|
|
g_mmioBase = (volatile uint8_t*)Memory::HHDM(mmioPhys);
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "MMIO mapped at virtual " << base::hex << (uint64_t)g_mmioBase;
|
|
|
|
// Enable memory space and bus master
|
|
Pci::EnableBusMaster(bus, dev, func);
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "PCI memory space and bus mastering enabled";
|
|
|
|
return true;
|
|
}
|
|
|
|
// =========================================================================
|
|
// VGA Disable
|
|
// =========================================================================
|
|
|
|
static void DisableVga() {
|
|
// Check if VGA plane is already disabled (bit 31 of VGACNTRL).
|
|
// On modern Intel iGPUs with eDP/LVDS panels, the firmware typically
|
|
// disables VGA when it sets up GOP. Skip if already done.
|
|
uint32_t vgaCtrl = ReadReg(VGACNTRL);
|
|
if (vgaCtrl & VGACNTRL_DISABLE) {
|
|
KernelLogStream(INFO, "IntelGPU") << "VGA plane already disabled by firmware";
|
|
return;
|
|
}
|
|
|
|
// Step 1: Disable VGA screen via the VGA sequencer I/O ports
|
|
// SR01 bit 5 must be set BEFORE disabling the VGA plane register
|
|
Io::Out8(0x01, 0x3C4);
|
|
uint8_t sr01 = Io::In8(0x3C5);
|
|
sr01 |= (1 << 5);
|
|
Io::Out8(0x01, 0x3C4);
|
|
Io::Out8(sr01, 0x3C5);
|
|
|
|
// Step 2: Set bit 31 of VGACNTRL MMIO register to disable VGA display plane
|
|
vgaCtrl |= VGACNTRL_DISABLE;
|
|
WriteReg(VGACNTRL, vgaCtrl);
|
|
|
|
// Step 3: Read back to flush the write
|
|
(void)ReadReg(VGACNTRL);
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "VGA plane disabled";
|
|
}
|
|
|
|
// =========================================================================
|
|
// Read Current Display State
|
|
// =========================================================================
|
|
|
|
static bool ReadDisplayState() {
|
|
// Read pipe A configuration
|
|
uint32_t pipeConf = ReadReg(PIPEACONF);
|
|
bool pipeEnabled = (pipeConf & PIPECONF_ENABLE) != 0;
|
|
|
|
// Read plane A control
|
|
uint32_t dspaCntr = ReadReg(DSPACNTR);
|
|
bool planeEnabled = (dspaCntr & DISP_ENABLE) != 0;
|
|
|
|
// Read current surface address and stride
|
|
uint32_t dspaSurf = ReadReg(DSPASURF);
|
|
uint32_t dspaStride = ReadReg(DSPASTRIDE);
|
|
|
|
// Read timing registers
|
|
uint32_t htotal = ReadReg(HTOTAL_A);
|
|
uint32_t vtotal = ReadReg(VTOTAL_A);
|
|
uint32_t pipeSrc = ReadReg(PIPEASRC);
|
|
|
|
KernelLogStream(INFO, "IntelGPU") << "Pipe A: "
|
|
<< (pipeEnabled ? "ENABLED" : "DISABLED")
|
|
<< ", Plane A: " << (planeEnabled ? "ENABLED" : "DISABLED");
|
|
KernelLogStream(INFO, "IntelGPU") << "DSPASURF: " << base::hex << (uint64_t)dspaSurf
|
|
<< ", DSPASTRIDE: " << (uint64_t)dspaStride;
|
|
KernelLogStream(INFO, "IntelGPU") << "HTOTAL_A: " << base::hex << (uint64_t)htotal
|
|
<< ", VTOTAL_A: " << (uint64_t)vtotal
|
|
<< ", PIPEASRC: " << (uint64_t)pipeSrc;
|
|
|
|
// Extract resolution from PIPEASRC (preferred) or timing registers
|
|
if (pipeSrc != 0) {
|
|
// PIPEASRC: bits [31:16] = horizontal size - 1, bits [15:0] = vertical size - 1
|
|
g_fbWidth = ((pipeSrc >> 16) & 0xFFFF) + 1;
|
|
g_fbHeight = (pipeSrc & 0xFFFF) + 1;
|
|
} else if (pipeEnabled) {
|
|
// Fallback to timing registers
|
|
g_fbWidth = (htotal & 0xFFF) + 1;
|
|
g_fbHeight = (vtotal & 0xFFF) + 1;
|
|
}
|
|
|
|
// Read stride from hardware.
|
|
// On Gen 9+ (Skylake and later), PLANE_STRIDE (same offset as DSPASTRIDE)
|
|
// stores the stride in 64-byte units, not bytes. Detect this by checking
|
|
// whether the raw value is too small to be a byte stride.
|
|
if (dspaStride != 0) {
|
|
g_fbPitch = dspaStride;
|
|
if (g_fbWidth > 0 && g_fbPitch < g_fbWidth * 4) {
|
|
KernelLogStream(INFO, "IntelGPU") << "DSPASTRIDE=" << base::dec << g_fbPitch
|
|
<< " is in 64-byte units (Gen 9+), converting to bytes";
|
|
g_fbPitch *= 64;
|
|
}
|
|
}
|
|
|
|
// If we still don't have valid dimensions, fall back to the firmware framebuffer
|
|
if (g_fbWidth == 0 || g_fbHeight == 0 || g_fbPitch == 0) {
|
|
g_fbWidth = ::Graphics::Framebuffer::GetWidth();
|
|
g_fbHeight = ::Graphics::Framebuffer::GetHeight();
|
|
g_fbPitch = ::Graphics::Framebuffer::GetPitch();
|
|
|
|
KernelLogStream(INFO, "IntelGPU") << "Using firmware framebuffer dimensions: "
|
|
<< base::dec << g_fbWidth << "x" << g_fbHeight
|
|
<< " pitch=" << g_fbPitch;
|
|
} else {
|
|
KernelLogStream(INFO, "IntelGPU") << "Detected resolution: "
|
|
<< base::dec << g_fbWidth << "x" << g_fbHeight
|
|
<< " pitch=" << g_fbPitch;
|
|
}
|
|
|
|
if (g_fbWidth == 0 || g_fbHeight == 0) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Could not determine display resolution";
|
|
return false;
|
|
}
|
|
|
|
// Ensure pitch is at least width * 4 (BGRX8888), and reject geometry
|
|
// whose size arithmetic could wrap before it reaches the allocator.
|
|
if (g_fbPitch == 0) {
|
|
g_fbPitch = g_fbWidth * 4;
|
|
KernelLogStream(WARNING, "IntelGPU") << "Stride not available, assuming "
|
|
<< base::dec << g_fbPitch << " bytes";
|
|
}
|
|
if (g_fbWidth > (~0ULL / 4)
|
|
|| g_fbPitch < g_fbWidth * 4
|
|
|| g_fbHeight > (~0ULL / g_fbPitch)) {
|
|
KernelLogStream(ERROR, "IntelGPU")
|
|
<< "Invalid or overflowing framebuffer geometry";
|
|
return false;
|
|
}
|
|
|
|
g_fbSize = g_fbHeight * g_fbPitch;
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "Display state: "
|
|
<< base::dec << g_fbWidth << "x" << g_fbHeight
|
|
<< ", stride=" << g_fbPitch
|
|
<< ", FB size=" << g_fbSize << " bytes";
|
|
|
|
return true;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Connector discovery, EDID, and mode catalogue
|
|
// =========================================================================
|
|
|
|
static uint32_t ModeRefreshMilliHz(const DisplayMode& mode) {
|
|
uint64_t pixels = (uint64_t)mode.htotal * mode.vtotal;
|
|
if (pixels == 0 || mode.pixelClock == 0) return 0;
|
|
return (uint32_t)(((uint64_t)mode.pixelClock * 1000000ULL + pixels / 2) / pixels);
|
|
}
|
|
|
|
static bool SameMode(const ModeRecord& a, const DisplayMode& mode,
|
|
uint32_t refreshMilliHz) {
|
|
if (a.timing.hdisplay != mode.hdisplay || a.timing.vdisplay != mode.vdisplay)
|
|
return false;
|
|
if (a.timing.htotal && mode.htotal
|
|
&& (a.timing.htotal != mode.htotal || a.timing.vtotal != mode.vtotal))
|
|
return false;
|
|
uint32_t ar = a.refreshMilliHz;
|
|
uint32_t diff = ar > refreshMilliHz ? ar - refreshMilliHz : refreshMilliHz - ar;
|
|
return ar == 0 || refreshMilliHz == 0 || diff <= 500;
|
|
}
|
|
|
|
static int AddMode(const DisplayMode& mode, uint32_t refreshMilliHz,
|
|
uint32_t flags) {
|
|
if (mode.hdisplay < 320 || mode.vdisplay < 200
|
|
|| mode.hdisplay > 16384 || mode.vdisplay > 16384)
|
|
return -1;
|
|
if (refreshMilliHz == 0) refreshMilliHz = ModeRefreshMilliHz(mode);
|
|
|
|
for (int i = 0; i < g_modeCount; i++) {
|
|
if (SameMode(g_modes[i], mode, refreshMilliHz)) {
|
|
g_modes[i].flags |= flags;
|
|
if (g_modes[i].timing.pixelClock == 0 && mode.pixelClock)
|
|
g_modes[i].timing = mode;
|
|
return i;
|
|
}
|
|
}
|
|
if (g_modeCount >= MaxDisplayModes) return -1;
|
|
int index = g_modeCount++;
|
|
g_modes[index].timing = mode;
|
|
g_modes[index].refreshMilliHz = refreshMilliHz;
|
|
g_modes[index].flags = flags;
|
|
return index;
|
|
}
|
|
|
|
static void AddSimpleMode(uint32_t width, uint32_t height, uint32_t refreshHz,
|
|
uint32_t flags = 0) {
|
|
DisplayMode mode = {};
|
|
mode.hdisplay = width;
|
|
mode.vdisplay = height;
|
|
AddMode(mode, refreshHz * 1000, flags);
|
|
}
|
|
|
|
static bool EdidBlockValid(const uint8_t* block) {
|
|
uint8_t sum = 0;
|
|
for (int i = 0; i < 128; i++) sum = (uint8_t)(sum + block[i]);
|
|
return sum == 0;
|
|
}
|
|
|
|
static uint32_t PackAuxBytes(const uint8_t* bytes, int count) {
|
|
uint32_t value = 0;
|
|
if (count > 4) count = 4;
|
|
for (int i = 0; i < count; i++)
|
|
value |= (uint32_t)bytes[i] << ((3 - i) * 8);
|
|
return value;
|
|
}
|
|
|
|
static void UnpackAuxBytes(uint32_t value, uint8_t* bytes, int count) {
|
|
if (count > 4) count = 4;
|
|
for (int i = 0; i < count; i++)
|
|
bytes[i] = (uint8_t)(value >> ((3 - i) * 8));
|
|
}
|
|
|
|
// Submit one native or I2C-over-AUX packet. The active firmware display
|
|
// has already powered and trained this channel; Montauk only uses it for
|
|
// read-only monitor discovery.
|
|
static int AuxTransfer(uint8_t request, uint32_t address,
|
|
const uint8_t* writeData, int writeSize,
|
|
uint8_t* readData, int readCapacity,
|
|
uint8_t* reply) {
|
|
if (g_auxControlReg == 0 || writeSize < 0 || writeSize > 16
|
|
|| readCapacity < 0 || readCapacity > 16)
|
|
return -1;
|
|
g_auxLastReply = 0xFF;
|
|
|
|
uint8_t packet[20] = {};
|
|
packet[0] = (uint8_t)((request << 4) | ((address >> 16) & 0x0F));
|
|
packet[1] = (uint8_t)(address >> 8);
|
|
packet[2] = (uint8_t)address;
|
|
bool read = (request & 1) != 0;
|
|
int payloadSize = read ? readCapacity : writeSize;
|
|
int sendBytes = payloadSize ? 4 : 3;
|
|
if (payloadSize) packet[3] = (uint8_t)(payloadSize - 1);
|
|
if (!read && writeSize) {
|
|
memcpy(packet + 4, writeData, (uint64_t)writeSize);
|
|
sendBytes += writeSize;
|
|
}
|
|
|
|
for (int attempt = 0; attempt < 5; attempt++) {
|
|
int busyWait = 100000;
|
|
while (((g_auxLastStatus = ReadReg(g_auxControlReg)) & DP_AUX_SEND_BUSY)
|
|
&& --busyWait)
|
|
asm volatile("pause");
|
|
if (!busyWait) return -1;
|
|
|
|
for (int i = 0; i < sendBytes; i += 4) {
|
|
WriteReg(g_auxControlReg + DP_AUX_DATA_DELTA + (uint32_t)(i / 4) * 4,
|
|
PackAuxBytes(packet + i, sendBytes - i));
|
|
}
|
|
|
|
// Skylake and later derive the AUX clock automatically. The pulse
|
|
// lengths below are within the documented hardware ranges.
|
|
uint32_t control = DP_AUX_SEND_BUSY | DP_AUX_DONE | DP_AUX_INTERRUPT
|
|
| DP_AUX_TIMEOUT_ERROR | DP_AUX_TIMEOUT_MAX
|
|
| DP_AUX_RECEIVE_ERROR
|
|
| ((uint32_t)sendBytes << DP_AUX_MESSAGE_SHIFT)
|
|
| (17u << 5) | 31u;
|
|
WriteReg(g_auxControlReg, control);
|
|
|
|
int completeWait = 1000000;
|
|
uint32_t status;
|
|
do {
|
|
status = ReadReg(g_auxControlReg);
|
|
g_auxLastStatus = status;
|
|
if (!(status & DP_AUX_SEND_BUSY)) break;
|
|
asm volatile("pause");
|
|
} while (--completeWait);
|
|
|
|
if (!completeWait) return -1;
|
|
WriteReg(g_auxControlReg, status | DP_AUX_DONE
|
|
| DP_AUX_TIMEOUT_ERROR | DP_AUX_RECEIVE_ERROR);
|
|
if (!(status & DP_AUX_DONE)
|
|
|| (status & (DP_AUX_TIMEOUT_ERROR | DP_AUX_RECEIVE_ERROR)))
|
|
continue;
|
|
|
|
int received = (int)((status >> DP_AUX_MESSAGE_SHIFT) & 0x1F);
|
|
if (received <= 0 || received > 20) continue;
|
|
|
|
uint8_t response[20] = {};
|
|
for (int i = 0; i < received; i += 4) {
|
|
uint32_t value = ReadReg(g_auxControlReg + DP_AUX_DATA_DELTA
|
|
+ (uint32_t)(i / 4) * 4);
|
|
UnpackAuxBytes(value, response + i, received - i);
|
|
}
|
|
if (reply) *reply = response[0] >> 4;
|
|
g_auxLastReply = response[0] >> 4;
|
|
if (!read) return writeSize;
|
|
|
|
int bytes = received - 1;
|
|
if (bytes > readCapacity) bytes = readCapacity;
|
|
if (bytes > 0) memcpy(readData, response + 1, (uint64_t)bytes);
|
|
return bytes;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static bool AuxI2cTransfer(uint8_t request, const uint8_t* writeData,
|
|
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, i2cAddress, writeData, writeSize,
|
|
readData, readSize, &reply);
|
|
uint8_t nativeReply = reply & 0x3;
|
|
uint8_t i2cReply = reply & 0xC;
|
|
if (result == (readSize ? readSize : writeSize)
|
|
&& nativeReply == 0 && i2cReply == 0)
|
|
return true;
|
|
if (nativeReply != 2 && i2cReply != 8 && result >= 0) return false;
|
|
for (int delay = 0; delay < 40000; delay++)
|
|
asm volatile("pause");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool ReadDpEdidBlock(uint8_t offset, uint8_t* block) {
|
|
// Keep the I2C transaction open while setting the EEPROM offset and
|
|
// reading the block in the AUX controller's 16-byte payload chunks.
|
|
if (!AuxI2cTransfer(0x4, nullptr, 0, nullptr, 0)) return false;
|
|
if (!AuxI2cTransfer(0x4, &offset, 1, nullptr, 0)) return false;
|
|
if (!AuxI2cTransfer(0x5, nullptr, 0, nullptr, 0)) return false;
|
|
for (int pos = 0; pos < 128; pos += 16) {
|
|
uint8_t request = (pos + 16 < 128) ? 0x5 : 0x1;
|
|
if (!AuxI2cTransfer(request, nullptr, 0, block + pos, 16))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool ProbeAuxChannel(uint32_t controlReg, uint8_t* revision) {
|
|
g_auxControlReg = controlReg;
|
|
uint8_t reply = 0xFF;
|
|
uint8_t dpcd = 0;
|
|
int result = AuxTransfer(0x9, 0, nullptr, 0, &dpcd, 1, &reply);
|
|
if (result != 1 || (reply & 0x3) != 0 || dpcd < 0x10 || dpcd > 0x30)
|
|
return false;
|
|
if (revision) *revision = dpcd;
|
|
return true;
|
|
}
|
|
|
|
static bool ReadDpEdid() {
|
|
g_auxEdidSize = 0;
|
|
if ((g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP
|
|
&& g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP)
|
|
|| g_auxControlReg == 0)
|
|
return false;
|
|
|
|
uint32_t preferred = g_auxControlReg;
|
|
uint32_t candidates[] = {
|
|
preferred, DP_AUX_A_CTL, DP_AUX_B_CTL, DP_AUX_C_CTL, DP_AUX_D_CTL,
|
|
};
|
|
for (uint32_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
|
bool duplicate = false;
|
|
for (uint32_t j = 0; j < i; j++) {
|
|
if (candidates[j] == candidates[i]) duplicate = true;
|
|
}
|
|
if (duplicate) continue;
|
|
|
|
uint8_t revision = 0;
|
|
if (!ProbeAuxChannel(candidates[i], &revision)) {
|
|
KernelLogStream(DEBUG, "IntelGPU") << "DP AUX probe failed at "
|
|
<< base::hex << candidates[i] << " (status=" << g_auxLastStatus
|
|
<< ", reply=" << (uint64_t)g_auxLastReply << ")";
|
|
continue;
|
|
}
|
|
|
|
KernelLogStream(INFO, "IntelGPU") << "DP AUX channel at "
|
|
<< base::hex << candidates[i] << ", DPCD revision "
|
|
<< (uint64_t)revision;
|
|
if (!ReadDpEdidBlock(0, g_auxEdid) || !EdidBlockValid(g_auxEdid)) {
|
|
KernelLogStream(WARNING, "IntelGPU") << "EDID read failed on DP AUX "
|
|
<< base::hex << candidates[i] << " (status=" << g_auxLastStatus
|
|
<< ", reply=" << (uint64_t)g_auxLastReply << ")";
|
|
continue;
|
|
}
|
|
|
|
g_auxEdidSize = 128;
|
|
if (g_auxEdid[126] != 0
|
|
&& ReadDpEdidBlock(128, g_auxEdid + 128)
|
|
&& EdidBlockValid(g_auxEdid + 128))
|
|
g_auxEdidSize = 256;
|
|
return true;
|
|
}
|
|
g_auxControlReg = preferred;
|
|
return false;
|
|
}
|
|
|
|
static bool ParseDetailedTiming(const uint8_t* dtd, DisplayMode& mode) {
|
|
uint32_t clock10KHz = (uint32_t)dtd[0] | ((uint32_t)dtd[1] << 8);
|
|
if (clock10KHz == 0) return false;
|
|
|
|
uint32_t hactive = dtd[2] | ((uint32_t)(dtd[4] & 0xF0) << 4);
|
|
uint32_t hblank = dtd[3] | ((uint32_t)(dtd[4] & 0x0F) << 8);
|
|
uint32_t vactive = dtd[5] | ((uint32_t)(dtd[7] & 0xF0) << 4);
|
|
uint32_t vblank = dtd[6] | ((uint32_t)(dtd[7] & 0x0F) << 8);
|
|
uint32_t hsyncOffset = dtd[8] | ((uint32_t)(dtd[11] & 0xC0) << 2);
|
|
uint32_t hsyncWidth = dtd[9] | ((uint32_t)(dtd[11] & 0x30) << 4);
|
|
uint32_t vsyncOffset = ((dtd[10] >> 4) & 0x0F)
|
|
| ((uint32_t)(dtd[11] & 0x0C) << 2);
|
|
uint32_t vsyncWidth = (dtd[10] & 0x0F)
|
|
| ((uint32_t)(dtd[11] & 0x03) << 4);
|
|
if (!hactive || !vactive || !hblank || !vblank) return false;
|
|
|
|
mode = {};
|
|
mode.hdisplay = hactive;
|
|
mode.hsyncStart = hactive + hsyncOffset;
|
|
mode.hsyncEnd = mode.hsyncStart + hsyncWidth;
|
|
mode.htotal = hactive + hblank;
|
|
mode.vdisplay = vactive;
|
|
mode.vsyncStart = vactive + vsyncOffset;
|
|
mode.vsyncEnd = mode.vsyncStart + vsyncWidth;
|
|
mode.vtotal = vactive + vblank;
|
|
mode.pixelClock = clock10KHz * 10;
|
|
mode.hsyncPositive = (dtd[17] & (1u << 1)) != 0;
|
|
mode.vsyncPositive = (dtd[17] & (1u << 2)) != 0;
|
|
return true;
|
|
}
|
|
|
|
static void ParseEdid() {
|
|
uint64_t edidSize = 0;
|
|
const uint8_t* edid = ::Graphics::Framebuffer::GetEdid(&edidSize);
|
|
if (!edid || edidSize < 128) {
|
|
if (!ReadDpEdid()) {
|
|
KernelLogStream(INFO, "IntelGPU") << "EDID unavailable from boot data"
|
|
<< (g_auxControlReg ? " and DP AUX" : "");
|
|
return;
|
|
}
|
|
edid = g_auxEdid;
|
|
edidSize = g_auxEdidSize;
|
|
KernelLogStream(OK, "IntelGPU") << "Read " << base::dec << edidSize
|
|
<< " bytes of EDID over DP AUX";
|
|
}
|
|
static constexpr uint8_t header[8] = {0x00, 0xFF, 0xFF, 0xFF,
|
|
0xFF, 0xFF, 0xFF, 0x00};
|
|
if (memcmp(edid, header, sizeof(header)) != 0 || !EdidBlockValid(edid)) {
|
|
KernelLogStream(WARNING, "IntelGPU") << "Boot EDID failed validation";
|
|
return;
|
|
}
|
|
g_edidValid = true;
|
|
|
|
// Established timings from EDID 1.3 section 3.8.
|
|
if (edid[35] & 0x20) AddSimpleMode(640, 480, 60);
|
|
if (edid[35] & 0x08) AddSimpleMode(640, 480, 72);
|
|
if (edid[35] & 0x04) AddSimpleMode(640, 480, 75);
|
|
if (edid[35] & 0x02) AddSimpleMode(800, 600, 56);
|
|
if (edid[35] & 0x01) AddSimpleMode(800, 600, 60);
|
|
if (edid[36] & 0x80) AddSimpleMode(800, 600, 72);
|
|
if (edid[36] & 0x40) AddSimpleMode(800, 600, 75);
|
|
if (edid[36] & 0x08) AddSimpleMode(1024, 768, 60);
|
|
if (edid[36] & 0x04) AddSimpleMode(1024, 768, 70);
|
|
if (edid[36] & 0x02) AddSimpleMode(1024, 768, 75);
|
|
if (edid[36] & 0x01) AddSimpleMode(1280, 1024, 75);
|
|
|
|
// Standard timings carry geometry and refresh but no porch/sync data.
|
|
for (int off = 38; off < 54; off += 2) {
|
|
if (edid[off] == 0x01 && edid[off + 1] == 0x01) continue;
|
|
uint32_t width = ((uint32_t)edid[off] + 31) * 8;
|
|
uint32_t aspect = (edid[off + 1] >> 6) & 3;
|
|
uint32_t height = aspect == 0 ? width * 10 / 16
|
|
: aspect == 1 ? width * 3 / 4
|
|
: aspect == 2 ? width * 4 / 5
|
|
: width * 9 / 16;
|
|
AddSimpleMode(width, height, (edid[off + 1] & 0x3F) + 60);
|
|
}
|
|
|
|
// Base-block detailed timings and monitor name descriptor.
|
|
bool firstDetailed = true;
|
|
for (int off = 54; off + 18 <= 126; off += 18) {
|
|
DisplayMode mode;
|
|
if (ParseDetailedTiming(edid + off, mode)) {
|
|
AddMode(mode, 0, firstDetailed ? montauk::abi::DISPLAY_MODE_PREFERRED : 0);
|
|
firstDetailed = false;
|
|
} else if (edid[off + 3] == 0xFC) {
|
|
CopyEdidText(g_monitorName, sizeof(g_monitorName), edid + off + 5, 13);
|
|
}
|
|
}
|
|
|
|
// CTA-861 extension detailed timings. Other extension types are kept
|
|
// untouched; their mode encodings need format-specific parsers.
|
|
uint32_t blocks = 1 + edid[126];
|
|
if (blocks * 128 > edidSize) blocks = (uint32_t)(edidSize / 128);
|
|
for (uint32_t block = 1; block < blocks; block++) {
|
|
const uint8_t* ext = edid + block * 128;
|
|
if (!EdidBlockValid(ext) || ext[0] != 0x02) continue;
|
|
int start = ext[2];
|
|
if (start == 0 || start < 4 || start > 126) continue;
|
|
for (int off = start; off + 18 <= 127; off += 18) {
|
|
DisplayMode mode;
|
|
if (ParseDetailedTiming(ext + off, mode)) AddMode(mode, 0, 0);
|
|
}
|
|
}
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "EDID: " << g_monitorName
|
|
<< ", " << base::dec << (uint64_t)g_modeCount << " modes";
|
|
}
|
|
|
|
struct TranscoderCandidate {
|
|
uint32_t timing;
|
|
uint32_t conf;
|
|
uint32_t ddi;
|
|
const char* name;
|
|
bool edp;
|
|
};
|
|
|
|
static void DiscoverActiveTranscoder() {
|
|
static constexpr TranscoderCandidate candidates[] = {
|
|
{TRANS_TIMING_EDP, TRANS_CONF_EDP, TRANS_DDI_EDP, "eDP-1", true},
|
|
{TRANS_TIMING_A, TRANS_CONF_A, TRANS_DDI_A, "Pipe A", false},
|
|
{TRANS_TIMING_B, TRANS_CONF_B, TRANS_DDI_B, "Pipe B", false},
|
|
{TRANS_TIMING_C, TRANS_CONF_C, TRANS_DDI_C, "Pipe C", false},
|
|
};
|
|
|
|
int selected = -1;
|
|
for (int pass = 0; pass < 2 && selected < 0; pass++) {
|
|
for (int i = 0; i < (int)(sizeof(candidates) / sizeof(candidates[0])); i++) {
|
|
uint32_t conf = ReadReg(candidates[i].conf);
|
|
uint32_t ddi = ReadReg(candidates[i].ddi);
|
|
if (!(conf & PIPECONF_ENABLE)) continue;
|
|
if (pass == 0 && !(ddi & TRANS_DDI_ENABLE)) continue;
|
|
selected = i;
|
|
break;
|
|
}
|
|
}
|
|
if (selected < 0) selected = 1; // conservative legacy Pipe A fallback
|
|
|
|
const auto& trans = candidates[selected];
|
|
g_transTimingBase = trans.timing;
|
|
g_transConfReg = trans.conf;
|
|
g_transDdiReg = trans.ddi;
|
|
g_connectorConnected = (ReadReg(trans.conf) & PIPECONF_ENABLE) != 0;
|
|
uint32_t ddi = ReadReg(trans.ddi);
|
|
g_auxControlReg = 0;
|
|
g_ddiPort = -1;
|
|
|
|
if (g_gpuGen <= 7) {
|
|
// 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) {
|
|
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_VGA;
|
|
CopyText(g_connectorName, sizeof(g_connectorName), "VGA-1");
|
|
} else if ((ReadReg(DP_B) | ReadReg(DP_C) | ReadReg(DP_D)) & (1u << 31)) {
|
|
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_DP;
|
|
CopyText(g_connectorName, sizeof(g_connectorName), "DP-1");
|
|
} else {
|
|
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_HDMI;
|
|
CopyText(g_connectorName, sizeof(g_connectorName), "HDMI-1");
|
|
}
|
|
} 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;
|
|
// 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-");
|
|
} else if (mode == 1) {
|
|
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_DVI;
|
|
CopyText(g_connectorName, sizeof(g_connectorName), "DVI-");
|
|
} else {
|
|
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_DP;
|
|
CopyText(g_connectorName, sizeof(g_connectorName), "DP-");
|
|
// 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++;
|
|
CopyText(g_connectorName + len, sizeof(g_connectorName) - len, suffix);
|
|
}
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "Active transcoder " << trans.name
|
|
<< ", connector " << g_connectorName
|
|
<< (g_connectorConnected ? " connected" : " disconnected");
|
|
}
|
|
|
|
static DisplayMode CaptureActiveMode() {
|
|
DisplayMode mode = {};
|
|
uint32_t htotal = ReadReg(g_transTimingBase + 0x00);
|
|
uint32_t hsync = ReadReg(g_transTimingBase + 0x08);
|
|
uint32_t vtotal = ReadReg(g_transTimingBase + 0x0C);
|
|
uint32_t vsync = ReadReg(g_transTimingBase + 0x14);
|
|
mode.hdisplay = (htotal & 0xFFFF) + 1;
|
|
mode.htotal = ((htotal >> 16) & 0xFFFF) + 1;
|
|
mode.hsyncStart = (hsync & 0xFFFF) + 1;
|
|
mode.hsyncEnd = ((hsync >> 16) & 0xFFFF) + 1;
|
|
mode.vdisplay = (vtotal & 0xFFFF) + 1;
|
|
mode.vtotal = ((vtotal >> 16) & 0xFFFF) + 1;
|
|
mode.vsyncStart = (vsync & 0xFFFF) + 1;
|
|
mode.vsyncEnd = ((vsync >> 16) & 0xFFFF) + 1;
|
|
return mode;
|
|
}
|
|
|
|
static void SelectCurrentMode() {
|
|
DisplayMode active = CaptureActiveMode();
|
|
if (active.hdisplay == 0 || active.hdisplay > 16384
|
|
|| active.vdisplay == 0 || active.vdisplay > 16384) {
|
|
active.hdisplay = (uint32_t)g_fbWidth;
|
|
active.vdisplay = (uint32_t)g_fbHeight;
|
|
}
|
|
|
|
// Match the register totals against a detailed EDID timing to recover
|
|
// the firmware-selected pixel clock exactly.
|
|
for (int i = 0; i < g_modeCount; i++) {
|
|
const auto& timing = g_modes[i].timing;
|
|
if (timing.hdisplay == active.hdisplay && timing.vdisplay == active.vdisplay
|
|
&& timing.htotal == active.htotal && timing.vtotal == active.vtotal
|
|
&& timing.pixelClock != 0) {
|
|
active.pixelClock = timing.pixelClock;
|
|
active.hsyncPositive = timing.hsyncPositive;
|
|
active.vsyncPositive = timing.vsyncPositive;
|
|
g_currentMode = i;
|
|
break;
|
|
}
|
|
}
|
|
uint32_t refresh = ModeRefreshMilliHz(active);
|
|
if (refresh == 0) refresh = 60000;
|
|
if (g_currentMode < 0) g_currentMode = AddMode(active, refresh, 0);
|
|
if (g_currentMode < 0 && g_modeCount == MaxDisplayModes) {
|
|
// A pathological EDID can fill the catalogue before the firmware
|
|
// mode is considered. The active mode is never optional: retain
|
|
// it by replacing the final non-preferred catalogue entry.
|
|
int replacement = MaxDisplayModes - 1;
|
|
for (int i = MaxDisplayModes - 1; i >= 0; i--) {
|
|
if (!(g_modes[i].flags & montauk::abi::DISPLAY_MODE_PREFERRED)) {
|
|
replacement = i;
|
|
break;
|
|
}
|
|
}
|
|
g_modes[replacement].timing = active;
|
|
g_modes[replacement].refreshMilliHz = refresh;
|
|
g_modes[replacement].flags = 0;
|
|
g_currentMode = replacement;
|
|
}
|
|
if (g_currentMode < 0) return;
|
|
|
|
g_modes[g_currentMode].flags |= montauk::abi::DISPLAY_MODE_CURRENT
|
|
| montauk::abi::DISPLAY_MODE_DRIVER_VALID;
|
|
uint32_t activeClock = g_modes[g_currentMode].timing.pixelClock;
|
|
for (int i = 0; i < g_modeCount; i++) {
|
|
const auto& mode = g_modes[i].timing;
|
|
if (mode.hdisplay == active.hdisplay && mode.vdisplay == active.vdisplay
|
|
&& mode.pixelClock != 0 && activeClock != 0
|
|
&& mode.pixelClock == activeClock) {
|
|
g_modes[i].flags |= montauk::abi::DISPLAY_MODE_DRIVER_VALID;
|
|
}
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Backlight
|
|
// =========================================================================
|
|
|
|
// 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 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);
|
|
}
|
|
}
|
|
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() {
|
|
g_modeCount = 0;
|
|
g_currentMode = -1;
|
|
g_edidValid = false;
|
|
CopyText(g_monitorName, sizeof(g_monitorName), "Unknown display");
|
|
DiscoverActiveTranscoder();
|
|
ParseEdid();
|
|
SelectCurrentMode();
|
|
DiscoverBacklight();
|
|
}
|
|
|
|
// =========================================================================
|
|
// GTT Initialization
|
|
// =========================================================================
|
|
|
|
static bool InitializeGtt() {
|
|
uint8_t bus = g_gpuInfo.pciBus;
|
|
uint8_t dev = g_gpuInfo.pciDevice;
|
|
uint8_t func = g_gpuInfo.pciFunction;
|
|
|
|
// Read GGC/GMCH_CTL to determine the GGTT modification window.
|
|
uint16_t gmchCtl = Pci::LegacyRead16(bus, dev, func, (uint8_t)PCI_REG_GMCH_CTL);
|
|
uint64_t gttSizeBytes = 0;
|
|
uint64_t gttOffset = 0;
|
|
|
|
if (g_gpuGen >= 8) {
|
|
uint8_t ggms = (gmchCtl >> 6) & 0x3;
|
|
if (ggms != 0) gttSizeBytes = (1ull << ggms) * 1024 * 1024;
|
|
gttOffset = 8 * 1024 * 1024;
|
|
} else {
|
|
uint8_t gttSizeBits = (gmchCtl >> 8) & 0x3;
|
|
// Gen 5-7
|
|
switch (gttSizeBits) {
|
|
case 0: gttSizeBytes = 0; break; // No GTT
|
|
case 1: gttSizeBytes = 1024 * 1024; break; // 1MB
|
|
case 2: gttSizeBytes = 2 * 1024 * 1024; break; // 2MB
|
|
case 3: gttSizeBytes = 2 * 1024 * 1024; break; // Depends on gen, default 2MB
|
|
default: gttSizeBytes = 1024 * 1024; break;
|
|
}
|
|
gttOffset = 2 * 1024 * 1024;
|
|
}
|
|
|
|
if (gttSizeBytes == 0) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "GGC reports no GGTT (GGC="
|
|
<< base::hex << (uint64_t)gmchCtl << ")";
|
|
return false;
|
|
}
|
|
|
|
g_gpuInfo.gttSize = gttSizeBytes;
|
|
|
|
KernelLogStream(INFO, "IntelGPU") << "GMCH_CTL: " << base::hex << (uint64_t)gmchCtl
|
|
<< ", GTT size: " << base::dec << (gttSizeBytes / 1024) << " KB";
|
|
|
|
uint64_t gttPhys = g_gpuInfo.mmioPhys + gttOffset;
|
|
|
|
// Map the GTT region (it may overlap with already-mapped MMIO, but we map
|
|
// additional pages beyond the initial 2MB MMIO mapping)
|
|
uint64_t gttMapSize = gttSizeBytes;
|
|
// Ensure we map at least up to the GTT region end
|
|
for (uint64_t offset = 0; offset < gttMapSize; offset += 0x1000) {
|
|
Memory::VMM::g_paging->MapMMIO(gttPhys + offset, Memory::HHDM(gttPhys + offset));
|
|
}
|
|
|
|
g_gttBase = (volatile void*)Memory::HHDM(gttPhys);
|
|
g_ggtt = g_gttBase;
|
|
|
|
// Calculate number of GTT entries
|
|
if (g_gpuGen >= 8) {
|
|
// 64-bit PTEs
|
|
g_gttEntryCount = gttSizeBytes / sizeof(uint64_t);
|
|
} else {
|
|
// 32-bit PTEs
|
|
g_gttEntryCount = gttSizeBytes / sizeof(uint32_t);
|
|
}
|
|
g_ggttEntries = g_gttEntryCount;
|
|
|
|
KernelLogStream(INFO, "IntelGPU") << "GTT at physical " << base::hex << gttPhys
|
|
<< ", " << base::dec << g_gttEntryCount << " entries"
|
|
<< (g_gpuGen >= 8 ? " (64-bit PTEs)" : " (32-bit PTEs)");
|
|
|
|
// Do NOT clear the entire GTT here. The firmware has active GTT mappings
|
|
// that the display engine is currently scanning out from. Clearing them
|
|
// would cause the display to go black (or worse) before we remap.
|
|
// Instead, we only write the entries we need in SetupFramebuffer().
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "GTT ready: " << base::dec
|
|
<< g_gttEntryCount << " entries";
|
|
|
|
return true;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Framebuffer Allocation and Setup
|
|
// =========================================================================
|
|
|
|
static bool SetupFramebuffer() {
|
|
// The boot framebuffer can be the CPU-visible graphics aperture rather
|
|
// than the scanout memory itself. On Gen 8+ retain the firmware's GGTT
|
|
// mapping and its surface offset; remapping the aperture address as a
|
|
// backing page would create an invalid translation.
|
|
uint32_t* fwFb = ::Graphics::Framebuffer::GetBase();
|
|
if (fwFb == nullptr) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "No firmware framebuffer available";
|
|
return false;
|
|
}
|
|
|
|
uint64_t fwFbPhys = Memory::SubHHDM((uint64_t)fwFb);
|
|
uint64_t pageCount = (g_fbSize + 0xFFF) / 0x1000;
|
|
|
|
if (pageCount > g_gttEntryCount) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Framebuffer requires " << base::dec << pageCount
|
|
<< " pages but GTT only has " << g_gttEntryCount << " entries";
|
|
return false;
|
|
}
|
|
|
|
if (g_gpuGen >= 8) {
|
|
g_fbGttOffset = ReadReg(DSPASURF) & ~0xFFFull;
|
|
uint64_t first = g_fbGttOffset >> 12;
|
|
uint64_t last = (g_fbGttOffset + g_fbSize - 1) >> 12;
|
|
if (last >= g_gttEntryCount || !(ReadGgttPte(first) & 1)
|
|
|| !(ReadGgttPte(last) & 1)) {
|
|
KernelLogStream(ERROR, "IntelGPU")
|
|
<< "Firmware scanout is not backed by valid GGTT entries";
|
|
return false;
|
|
}
|
|
KernelLogStream(OK, "IntelGPU") << "Preserving firmware GGTT scanout at +"
|
|
<< base::hex << g_fbGttOffset << " (CPU aperture " << fwFbPhys << ")";
|
|
} else {
|
|
KernelLogStream(INFO, "IntelGPU") << "Mapping " << base::dec << pageCount
|
|
<< " firmware FB pages through GTT (phys base "
|
|
<< base::hex << fwFbPhys << ")";
|
|
volatile uint32_t* gtt32 = (volatile uint32_t*)g_gttBase;
|
|
for (uint64_t i = 0; i < pageCount; i++) {
|
|
gtt32[i] = MakeGttPte32(fwFbPhys + i * 0x1000);
|
|
}
|
|
// Flush GTT writes
|
|
(void)gtt32[pageCount - 1];
|
|
g_fbGttOffset = 0;
|
|
}
|
|
|
|
// Keep using the same framebuffer memory
|
|
g_fbBase = fwFb;
|
|
g_fbPhysBase = fwFbPhys;
|
|
KernelLogStream(OK, "IntelGPU") << "Framebuffer ready: " << base::dec << pageCount
|
|
<< " pages, CPU phys=" << base::hex << fwFbPhys;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void ProgramDisplayPlane() {
|
|
// Preserve the firmware's DSPACNTR value entirely. The firmware already
|
|
// configured the correct pixel format, pipe assignment, and tiling mode.
|
|
// We only need to point DSPASURF to our GTT-mapped framebuffer.
|
|
uint32_t dspaCntr = ReadReg(DSPACNTR);
|
|
uint32_t fmtBits = (dspaCntr & DISP_FORMAT_MASK) >> DISP_FORMAT_SHIFT;
|
|
uint32_t oldDspaSurf = ReadReg(DSPASURF);
|
|
|
|
KernelLogStream(INFO, "IntelGPU") << "Preserving firmware DSPACNTR: " << base::hex
|
|
<< (uint64_t)dspaCntr << " (format=" << (uint64_t)fmtBits << ")";
|
|
KernelLogStream(INFO, "IntelGPU") << "Firmware DSPASURF was: " << base::hex
|
|
<< (uint64_t)oldDspaSurf;
|
|
|
|
// Ensure the plane is enabled (it should already be)
|
|
if (!(dspaCntr & DISP_ENABLE)) {
|
|
dspaCntr |= DISP_ENABLE;
|
|
WriteReg(DSPACNTR, dspaCntr);
|
|
}
|
|
|
|
// Do NOT write DSPASTRIDE — the firmware already set the correct value
|
|
// in the hardware's native format (bytes on Gen <9, 64-byte units on Gen 9+).
|
|
// Writing our byte-converted g_fbPitch would corrupt it on Gen 9+.
|
|
|
|
// Reassert the firmware surface offset to flush any plane update.
|
|
uint32_t surfAddr = (uint32_t)g_fbGttOffset;
|
|
WriteReg(DSPASURF, surfAddr);
|
|
|
|
// Read back to flush
|
|
(void)ReadReg(DSPASURF);
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "Display plane A: DSPASURF="
|
|
<< base::hex << (uint64_t)surfAddr
|
|
<< " (was " << (uint64_t)oldDspaSurf << ")"
|
|
<< ", stride=" << base::dec << g_fbPitch;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Page flipping: GGTT at the generation-correct offset
|
|
// =========================================================================
|
|
//
|
|
// The GTT window lives in the upper half of GTTMMADR (BAR0):
|
|
// Gen 6-7: 4 MB BAR, GTT at +2 MB, 32-bit entries
|
|
// Gen 8+: 16 MB BAR, GGTT at +8 MB, 64-bit entries
|
|
// Initialization maps this window once and preserves the firmware's active
|
|
// scanout PTEs. Page flipping only proceeds after those entries validate.
|
|
|
|
static uint64_t ReadGgttPte(uint64_t index) {
|
|
if (g_gpuGen >= 8) return ((volatile uint64_t*)g_ggtt)[index];
|
|
return ((volatile uint32_t*)g_ggtt)[index];
|
|
}
|
|
|
|
static int ReadLiveBufferIndex() {
|
|
if (!g_mmioBase) return -1;
|
|
uint32_t live = ReadReg(DSPASURFLIVE) & ~0xFFFu;
|
|
if (live == (uint32_t)g_fbGttOffsetA) return 0;
|
|
if (live == (uint32_t)g_buf1GttOffset) return 1;
|
|
return -1;
|
|
}
|
|
|
|
// Polling-only latch wait. This is safe during both ordinary syscalls and
|
|
// ExitProcess teardown because it never enters the scheduler. Keeping
|
|
// flips independent of GPU MSI is deliberate: MSI would route every
|
|
// firmware-enabled GPU source, not just the one source this driver owns.
|
|
// Three observed frames are the normal timeout. Laptop panel self-refresh
|
|
// can transiently stop the scanline counter, so also enforce a wall-clock
|
|
// bound: one million uncached MMIO reads can otherwise hold the desktop in
|
|
// SYS_FBFLIP long enough to look permanently frozen (cursor included).
|
|
static bool WaitForLiveBuffer(int index) {
|
|
uint32_t expected = (uint32_t)((index == 0) ? g_fbGttOffsetA
|
|
: g_buf1GttOffset);
|
|
uint32_t previous = ReadReg(PIPE_DSL) & 0x1FFFu;
|
|
uint64_t started = Timekeeping::GetMilliseconds();
|
|
int frameWraps = 0;
|
|
for (int i = 0; i < 200000; i++) {
|
|
uint32_t live = ReadReg(DSPASURFLIVE);
|
|
if (live == 0xFFFFFFFFu) return false;
|
|
if ((live & ~0xFFFu) == expected) return true;
|
|
uint32_t current = ReadReg(PIPE_DSL) & 0x1FFFu;
|
|
if (current < previous && ++frameWraps >= 3) return false;
|
|
previous = current;
|
|
if ((i & 0x3FF) == 0 &&
|
|
Timekeeping::GetMilliseconds() - started >= 100) return false;
|
|
asm volatile("pause");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void WriteGgttPte(uint64_t index, uint64_t physAddr) {
|
|
if (g_gpuGen >= 8) {
|
|
((volatile uint64_t*)g_ggtt)[index] = MakeGttPte64(physAddr);
|
|
} else {
|
|
((volatile uint32_t*)g_ggtt)[index] = MakeGttPte32(physAddr);
|
|
}
|
|
}
|
|
|
|
static bool MapCorrectGgtt() {
|
|
if (!g_gttBase || g_gttEntryCount == 0) return false;
|
|
g_ggtt = g_gttBase;
|
|
g_ggttEntries = g_gttEntryCount;
|
|
return true;
|
|
}
|
|
|
|
static bool VerifyFirmwareScanoutPtes() {
|
|
// The active scanout surface must be backed by valid PTEs in the
|
|
// window we mapped; if not, our idea of where the GGTT lives is wrong
|
|
// and writing to it could corrupt arbitrary state.
|
|
g_fbGttOffsetA = ReadReg(DSPASURF) & ~0xFFFull;
|
|
uint64_t firstIdx = g_fbGttOffsetA >> 12;
|
|
uint64_t lastIdx = (g_fbGttOffsetA + g_fbSize - 1) >> 12;
|
|
|
|
if (lastIdx >= g_ggttEntries) {
|
|
KernelLogStream(WARNING, "IntelGPU") << "Scanout range exceeds GGTT ("
|
|
<< base::dec << lastIdx << " >= " << g_ggttEntries << ")";
|
|
return false;
|
|
}
|
|
|
|
uint64_t checks[3] = { firstIdx, (firstIdx + lastIdx) / 2, lastIdx };
|
|
for (int i = 0; i < 3; i++) {
|
|
uint64_t pte = ReadGgttPte(checks[i]);
|
|
if (!(pte & 1)) {
|
|
KernelLogStream(WARNING, "IntelGPU") << "Scanout PTE " << base::dec
|
|
<< checks[i] << " invalid (" << base::hex << pte
|
|
<< ") - GGTT window not where expected, page flip unavailable";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "Firmware scanout PTEs verified (surface at GGTT+"
|
|
<< base::hex << g_fbGttOffsetA << ", PTE[" << base::dec << firstIdx
|
|
<< "]=" << base::hex << ReadGgttPte(firstIdx) << ")";
|
|
return true;
|
|
}
|
|
|
|
static bool AllocateBackBuffer() {
|
|
uint64_t pages = (g_fbSize + 0xFFF) >> 12;
|
|
if (pages == 0 || pages > 0x7FFFFFFFULL) {
|
|
KernelLogStream(WARNING, "IntelGPU")
|
|
<< "Invalid second scanout buffer page count " << base::dec << pages;
|
|
return false;
|
|
}
|
|
|
|
void* virt = Memory::g_pfa->ReallocConsecutive(nullptr, (int)pages);
|
|
if (!virt) {
|
|
KernelLogStream(WARNING, "IntelGPU") << "Failed to allocate " << base::dec
|
|
<< pages << " contiguous pages for the second scanout buffer";
|
|
return false;
|
|
}
|
|
uint64_t phys = Memory::SubHHDM(virt);
|
|
if ((pages - 1) > (UINT64_MAX - phys) / 0x1000ULL) {
|
|
Memory::g_pfa->Free(virt, (int)pages);
|
|
KernelLogStream(WARNING, "IntelGPU")
|
|
<< "Second scanout buffer physical range overflows";
|
|
return false;
|
|
}
|
|
uint64_t lastPhys = phys + (pages - 1) * 0x1000ULL;
|
|
if (!GgttCanAddress(phys, g_gpuGen) || !GgttCanAddress(lastPhys, g_gpuGen)) {
|
|
Memory::g_pfa->Free(virt, (int)pages);
|
|
KernelLogStream(WARNING, "IntelGPU")
|
|
<< "Second scanout buffer lies outside the GPU physical-address width ("
|
|
<< base::hex << phys << ".." << lastPhys << ")";
|
|
return false;
|
|
}
|
|
|
|
g_buf1Phys = phys;
|
|
g_buf1Virt = (uint32_t*)virt;
|
|
g_buf1Pages = pages;
|
|
|
|
// Display scanout does not snoop the CPU cache: remap the buffer
|
|
// write-combining before touching it, same as the firmware FB.
|
|
for (uint64_t i = 0; i < pages; i++) {
|
|
Memory::VMM::g_paging->MapWC(g_buf1Phys + i * 0x1000,
|
|
Memory::HHDM(g_buf1Phys + i * 0x1000));
|
|
}
|
|
Memory::VMM::FlushTLB();
|
|
|
|
memset(virt, 0, pages * 0x1000);
|
|
return true;
|
|
}
|
|
|
|
static bool FindAndMapGgttRange() {
|
|
uint64_t pages = (g_fbSize + 0xFFF) >> 12;
|
|
constexpr uint64_t guard = 16;
|
|
uint64_t need = pages + 2 * guard;
|
|
uint64_t avoidFirst = g_fbGttOffsetA >> 12;
|
|
uint64_t avoidLast = (g_fbGttOffsetA + g_fbSize - 1) >> 12;
|
|
|
|
// Firmware normally fills unused GGTT entries with one scratch-page
|
|
// PTE. Verify that invariant at the tail, then claim only a complete
|
|
// scratch-backed (or invalid) run. Never overwrite arbitrary valid
|
|
// mappings merely because they happen to sit in the upper half.
|
|
if (g_ggttEntries < 32 || need > g_ggttEntries / 2) return false;
|
|
uint64_t scratchPte = ReadGgttPte(g_ggttEntries - 1);
|
|
for (uint64_t probe = 2; probe <= 32; probe++) {
|
|
if (ReadGgttPte(g_ggttEntries - probe) != scratchPte) {
|
|
KernelLogStream(WARNING, "IntelGPU")
|
|
<< "GGTT tail is owned/non-uniform; page flip unavailable";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
uint64_t runStart = 0;
|
|
uint64_t run = 0;
|
|
bool found = false;
|
|
for (uint64_t idx = g_ggttEntries / 2; idx < g_ggttEntries; idx++) {
|
|
if (idx >= avoidFirst && idx <= avoidLast) {
|
|
run = 0;
|
|
continue;
|
|
}
|
|
uint64_t pte = ReadGgttPte(idx);
|
|
if ((pte & 1) && pte != scratchPte) {
|
|
run = 0;
|
|
continue;
|
|
}
|
|
if (run == 0) runStart = idx;
|
|
if (++run >= need) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
KernelLogStream(WARNING, "IntelGPU") << "No verified free GGTT run for "
|
|
<< base::dec << pages << " back-buffer pages";
|
|
return false;
|
|
}
|
|
|
|
uint64_t base = runStart + guard;
|
|
uint64_t replacedFirst = ReadGgttPte(base);
|
|
uint64_t replacedLast = ReadGgttPte(base + pages - 1);
|
|
for (uint64_t i = 0; i < pages; i++) {
|
|
WriteGgttPte(base + i, g_buf1Phys + i * 0x1000);
|
|
}
|
|
|
|
// Read back first and last entry to confirm the writes stuck
|
|
uint64_t expectFirst = (g_gpuGen >= 8) ? MakeGttPte64(g_buf1Phys)
|
|
: MakeGttPte32(g_buf1Phys);
|
|
uint64_t lastPhys = g_buf1Phys + (pages - 1) * 0x1000;
|
|
uint64_t expectLast = (g_gpuGen >= 8) ? MakeGttPte64(lastPhys)
|
|
: MakeGttPte32(lastPhys);
|
|
if (ReadGgttPte(base) != expectFirst || ReadGgttPte(base + pages - 1) != expectLast) {
|
|
KernelLogStream(WARNING, "IntelGPU") << "GGTT read-back mismatch at entry "
|
|
<< base::dec << base << ", page flip unavailable";
|
|
return false;
|
|
}
|
|
|
|
g_buf1GttOffset = base << 12;
|
|
KernelLogStream(OK, "IntelGPU") << "Claimed buffer 1 at GGTT+" << base::hex
|
|
<< g_buf1GttOffset << " (" << base::dec << pages << " pages, phys "
|
|
<< base::hex << g_buf1Phys << ", replaced PTEs "
|
|
<< replacedFirst << ".." << replacedLast << ")";
|
|
return true;
|
|
}
|
|
|
|
static void SetupPageFlip() {
|
|
if (!MapCorrectGgtt()) return;
|
|
if (!VerifyFirmwareScanoutPtes()) return;
|
|
if (!AllocateBackBuffer()) return;
|
|
if (!FindAndMapGgttRange()) return;
|
|
|
|
g_flipSupported = true;
|
|
int live = ReadLiveBufferIndex();
|
|
g_frontBuffer = (live >= 0) ? live : 0;
|
|
KernelLogStream(OK, "IntelGPU")
|
|
<< "Page flip ready: 2 buffers, bounded SURFLIVE polling";
|
|
}
|
|
|
|
static bool FirmwareFramebufferMatchesGpu() {
|
|
uint64_t fwWidth = ::Graphics::Framebuffer::GetWidth();
|
|
uint64_t fwHeight = ::Graphics::Framebuffer::GetHeight();
|
|
uint64_t fwPitch = ::Graphics::Framebuffer::GetPitch();
|
|
bool match = g_fbWidth == fwWidth
|
|
&& g_fbHeight == fwHeight
|
|
&& g_fbPitch == fwPitch;
|
|
if (match) return true;
|
|
|
|
// Userspace obtains its drawing dimensions from the firmware
|
|
// framebuffer API. A differently-sized GPU allocation would make the
|
|
// second mapping overrun (or alias only part of) the backing buffer.
|
|
KernelLogStream(ERROR, "IntelGPU")
|
|
<< "GPU dimensions differ from firmware; unsafe framebuffer setup skipped";
|
|
KernelLogStream(WARNING, "IntelGPU") << " GPU: "
|
|
<< base::dec << g_fbWidth << "x" << g_fbHeight << " pitch=" << g_fbPitch;
|
|
KernelLogStream(WARNING, "IntelGPU") << " Firmware: "
|
|
<< base::dec << fwWidth << "x" << fwHeight << " pitch=" << fwPitch;
|
|
return false;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Public API
|
|
// =========================================================================
|
|
|
|
// =========================================================================
|
|
// Probe (called by PCI driver matching framework)
|
|
// =========================================================================
|
|
|
|
bool Probe(const Pci::PciDevice& dev) {
|
|
if (g_initialized) return false;
|
|
|
|
// Fill in PCI location
|
|
g_gpuInfo.pciBus = dev.Bus;
|
|
g_gpuInfo.pciDevice = dev.Device;
|
|
g_gpuInfo.pciFunction = dev.Function;
|
|
g_gpuInfo.deviceId = dev.DeviceId;
|
|
|
|
// Match device ID against supported table
|
|
const DeviceInfo* matchedInfo = nullptr;
|
|
for (int j = 0; j < SupportedDeviceCount; j++) {
|
|
if (dev.DeviceId == SupportedDevices[j].deviceId) {
|
|
matchedInfo = &SupportedDevices[j];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (matchedInfo != nullptr) {
|
|
g_gpuInfo.gen = matchedInfo->gen;
|
|
g_gpuInfo.name = matchedInfo->name;
|
|
KernelLogStream(OK, "IntelGPU") << "Found " << matchedInfo->name
|
|
<< " (device " << base::hex << (uint64_t)dev.DeviceId << ")"
|
|
<< " at PCI " << (uint64_t)dev.Bus << ":"
|
|
<< (uint64_t)dev.Device << "." << (uint64_t)dev.Function;
|
|
} else {
|
|
uint16_t did = dev.DeviceId;
|
|
uint8_t inferredGen;
|
|
if (did >= 0xA700 || (did >= 0x4600 && did < 0x4700) ||
|
|
(did >= 0x5600 && did < 0x5700) || (did >= 0x7D00 && did < 0x7E00)) {
|
|
inferredGen = 12;
|
|
} else if ((did >= 0x8A00 && did < 0x8B00) ||
|
|
(did >= 0x9A00 && did < 0x9B00)) {
|
|
inferredGen = 12;
|
|
} else if ((did >= 0x3E00 && did < 0x3F00) ||
|
|
(did >= 0x5900 && did < 0x5A00) ||
|
|
(did >= 0x9B00 && did < 0x9C00) ||
|
|
(did >= 0x1900 && did < 0x1A00)) {
|
|
inferredGen = 9;
|
|
} else if (did >= 0x1600 && did < 0x1700) {
|
|
inferredGen = 8;
|
|
} else {
|
|
inferredGen = 7;
|
|
}
|
|
g_gpuInfo.gen = inferredGen;
|
|
g_gpuInfo.name = "Intel GPU";
|
|
KernelLogStream(WARNING, "IntelGPU") << "Unknown Intel display controller "
|
|
<< "(device " << base::hex << (uint64_t)dev.DeviceId << ")"
|
|
<< " at PCI " << (uint64_t)dev.Bus << ":"
|
|
<< (uint64_t)dev.Device << "." << (uint64_t)dev.Function
|
|
<< " - inferred gen " << base::dec << (uint64_t)inferredGen
|
|
<< ", attempting generic initialization";
|
|
}
|
|
|
|
g_gpuGen = g_gpuInfo.gen;
|
|
DisableInterruptDelivery();
|
|
|
|
if (!MapMmio()) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Failed to map MMIO region";
|
|
return false;
|
|
}
|
|
|
|
DisableVga();
|
|
|
|
if (!ReadDisplayState()) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Failed to read display state";
|
|
return false;
|
|
}
|
|
if (!FirmwareFramebufferMatchesGpu()) return false;
|
|
InitializeDisplayManagement();
|
|
|
|
if (!InitializeGtt()) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Failed to initialize GTT";
|
|
return false;
|
|
}
|
|
|
|
if (!SetupFramebuffer()) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Failed to set up framebuffer";
|
|
return false;
|
|
}
|
|
|
|
ProgramDisplayPlane();
|
|
g_initialized = true;
|
|
|
|
SetupPageFlip();
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "Initialization complete: "
|
|
<< base::dec << g_fbWidth << "x" << g_fbHeight
|
|
<< " @ " << base::hex << (uint64_t)g_fbBase;
|
|
return true;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Initialize (standalone fallback path)
|
|
// =========================================================================
|
|
|
|
void Initialize() {
|
|
KernelLogStream(INFO, "IntelGPU") << "Scanning for Intel integrated graphics...";
|
|
|
|
// Step 1: Detect GPU on PCI bus
|
|
if (!DetectGpu()) {
|
|
return;
|
|
}
|
|
DisableInterruptDelivery();
|
|
|
|
// Step 2: Map BAR0 MMIO region
|
|
if (!MapMmio()) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Failed to map MMIO region";
|
|
return;
|
|
}
|
|
|
|
// Step 3: Disable VGA plane
|
|
DisableVga();
|
|
|
|
// Step 4: Read current display state from firmware
|
|
if (!ReadDisplayState()) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Failed to read display state";
|
|
return;
|
|
}
|
|
if (!FirmwareFramebufferMatchesGpu()) return;
|
|
InitializeDisplayManagement();
|
|
|
|
// Step 5: Initialize GTT
|
|
if (!InitializeGtt()) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Failed to initialize GTT";
|
|
return;
|
|
}
|
|
|
|
// Step 6: Map firmware framebuffer pages through GTT
|
|
if (!SetupFramebuffer()) {
|
|
KernelLogStream(ERROR, "IntelGPU") << "Failed to set up framebuffer";
|
|
return;
|
|
}
|
|
|
|
// Step 7: Program the display plane to use our GTT-mapped framebuffer
|
|
ProgramDisplayPlane();
|
|
|
|
g_initialized = true;
|
|
|
|
KernelLogStream(OK, "IntelGPU") << "Initialization complete: "
|
|
<< base::dec << g_fbWidth << "x" << g_fbHeight
|
|
<< " @ " << base::hex << (uint64_t)g_fbBase;
|
|
}
|
|
|
|
bool IsInitialized() {
|
|
return g_initialized;
|
|
}
|
|
|
|
const GpuInfo* GetGpuInfo() {
|
|
return &g_gpuInfo;
|
|
}
|
|
|
|
uint32_t* GetFramebufferBase() {
|
|
return g_fbBase;
|
|
}
|
|
|
|
uint64_t GetFramebufferPhysBase() {
|
|
return g_fbPhysBase;
|
|
}
|
|
|
|
uint64_t GetWidth() {
|
|
return g_fbWidth;
|
|
}
|
|
|
|
uint64_t GetHeight() {
|
|
return g_fbHeight;
|
|
}
|
|
|
|
uint64_t GetPitch() {
|
|
return g_fbPitch;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Display management public API
|
|
// =========================================================================
|
|
|
|
static bool WaitTranscoderState(bool enabled) {
|
|
for (int i = 0; i < 200000; i++) {
|
|
bool state = (ReadReg(g_transConfReg) & PIPECONF_STATE) != 0;
|
|
if (state == enabled) return true;
|
|
asm volatile("pause");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
struct TimingSnapshot {
|
|
uint32_t htotal, hblank, hsync;
|
|
uint32_t vtotal, vblank, vsync;
|
|
};
|
|
|
|
static TimingSnapshot ReadTimingSnapshot() {
|
|
return {
|
|
ReadReg(g_transTimingBase + 0x00),
|
|
ReadReg(g_transTimingBase + 0x04),
|
|
ReadReg(g_transTimingBase + 0x08),
|
|
ReadReg(g_transTimingBase + 0x0C),
|
|
ReadReg(g_transTimingBase + 0x10),
|
|
ReadReg(g_transTimingBase + 0x14),
|
|
};
|
|
}
|
|
|
|
static void WriteTimingSnapshot(const TimingSnapshot& timing) {
|
|
WriteReg(g_transTimingBase + 0x00, timing.htotal);
|
|
WriteReg(g_transTimingBase + 0x04, timing.hblank);
|
|
WriteReg(g_transTimingBase + 0x08, timing.hsync);
|
|
WriteReg(g_transTimingBase + 0x0C, timing.vtotal);
|
|
WriteReg(g_transTimingBase + 0x10, timing.vblank);
|
|
WriteReg(g_transTimingBase + 0x14, timing.vsync);
|
|
}
|
|
|
|
static uint32_t PackTiming(uint32_t start, uint32_t end) {
|
|
return ((end - 1) << 16) | (start - 1);
|
|
}
|
|
|
|
static bool ApplyModeTiming(const DisplayMode& mode) {
|
|
if (!g_connectorConnected || mode.hdisplay == 0 || mode.vdisplay == 0
|
|
|| mode.htotal <= mode.hdisplay || mode.vtotal <= mode.vdisplay
|
|
|| mode.hsyncStart < mode.hdisplay || mode.hsyncEnd <= mode.hsyncStart
|
|
|| mode.hsyncEnd > mode.htotal || mode.vsyncStart < mode.vdisplay
|
|
|| mode.vsyncEnd <= mode.vsyncStart || mode.vsyncEnd > mode.vtotal)
|
|
return false;
|
|
|
|
TimingSnapshot oldTiming = ReadTimingSnapshot();
|
|
uint32_t oldConf = ReadReg(g_transConfReg);
|
|
uint32_t oldPlane = ReadReg(DSPACNTR);
|
|
uint64_t oldSurface = (g_flipSupported && g_frontBuffer == 1)
|
|
? g_buf1GttOffset
|
|
: (g_flipSupported ? g_fbGttOffsetA : g_fbGttOffset);
|
|
|
|
// A timing-only modeset retains the firmware-trained DDI, PLL and link.
|
|
// Disable fetch and the transcoder, program all timing registers as one
|
|
// transaction, then restore them in the inverse order.
|
|
if (oldPlane & DISP_ENABLE) {
|
|
WriteReg(DSPACNTR, oldPlane & ~DISP_ENABLE);
|
|
WriteReg(DSPASURF, (uint32_t)oldSurface);
|
|
(void)ReadReg(DSPASURF);
|
|
}
|
|
WriteReg(g_transConfReg, oldConf & ~PIPECONF_ENABLE);
|
|
if (!WaitTranscoderState(false)) {
|
|
WriteReg(DSPACNTR, oldPlane);
|
|
WriteReg(DSPASURF, (uint32_t)oldSurface);
|
|
return false;
|
|
}
|
|
|
|
TimingSnapshot next = {
|
|
PackTiming(mode.hdisplay, mode.htotal),
|
|
PackTiming(mode.hdisplay, mode.htotal),
|
|
PackTiming(mode.hsyncStart, mode.hsyncEnd),
|
|
PackTiming(mode.vdisplay, mode.vtotal),
|
|
PackTiming(mode.vdisplay, mode.vtotal),
|
|
PackTiming(mode.vsyncStart, mode.vsyncEnd),
|
|
};
|
|
WriteTimingSnapshot(next);
|
|
WriteReg(g_transConfReg, oldConf | PIPECONF_ENABLE);
|
|
if (!WaitTranscoderState(true)) {
|
|
// Roll back while the transcoder is stopped. A failed timing must
|
|
// never leave the panel black merely because userspace requested it.
|
|
WriteReg(g_transConfReg, oldConf & ~PIPECONF_ENABLE);
|
|
(void)WaitTranscoderState(false);
|
|
WriteTimingSnapshot(oldTiming);
|
|
WriteReg(g_transConfReg, oldConf);
|
|
(void)WaitTranscoderState((oldConf & PIPECONF_ENABLE) != 0);
|
|
WriteReg(DSPACNTR, oldPlane);
|
|
WriteReg(DSPASURF, (uint32_t)oldSurface);
|
|
(void)ReadReg(DSPASURF);
|
|
return false;
|
|
}
|
|
|
|
WriteReg(DSPACNTR, oldPlane);
|
|
WriteReg(DSPASURF, (uint32_t)oldSurface);
|
|
(void)ReadReg(DSPASURF);
|
|
return true;
|
|
}
|
|
|
|
bool GetDisplayInfo(montauk::abi::DisplayInfo* out) {
|
|
if (!out) return false;
|
|
memset(out, 0, sizeof(*out));
|
|
out->brightness = -1;
|
|
out->currentMode = -1;
|
|
if (!g_initialized) return false;
|
|
|
|
out->capabilities = montauk::abi::DISPLAY_CAP_MODESET;
|
|
if (g_edidValid) out->capabilities |= montauk::abi::DISPLAY_CAP_EDID;
|
|
if (g_flipSupported) out->capabilities |= montauk::abi::DISPLAY_CAP_PAGE_FLIP;
|
|
for (int i = 0; i < g_modeCount; i++) {
|
|
if (i != g_currentMode
|
|
&& (g_modes[i].flags & montauk::abi::DISPLAY_MODE_DRIVER_VALID)) {
|
|
out->capabilities |= montauk::abi::DISPLAY_CAP_SAFE_SWITCH;
|
|
break;
|
|
}
|
|
}
|
|
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;
|
|
out->connected = g_connectorConnected ? 1 : 0;
|
|
CopyText(out->gpuName, sizeof(out->gpuName),
|
|
g_gpuInfo.name ? g_gpuInfo.name : "Intel GPU");
|
|
CopyText(out->connectorName, sizeof(out->connectorName), g_connectorName);
|
|
CopyText(out->monitorName, sizeof(out->monitorName), g_monitorName);
|
|
return true;
|
|
}
|
|
|
|
int GetDisplayModes(montauk::abi::DisplayModeInfo* out, int maxCount) {
|
|
if (!g_initialized || maxCount < 0) return -1;
|
|
int count = g_modeCount < maxCount ? g_modeCount : maxCount;
|
|
if (count > 0 && !out) return -1;
|
|
for (int i = 0; i < count; i++) {
|
|
out[i].width = g_modes[i].timing.hdisplay;
|
|
out[i].height = g_modes[i].timing.vdisplay;
|
|
out[i].refreshMilliHz = g_modes[i].refreshMilliHz;
|
|
out[i].pixelClockKHz = g_modes[i].timing.pixelClock;
|
|
out[i].flags = g_modes[i].flags;
|
|
out[i]._pad = 0;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int SetDisplayMode(int modeIndex) {
|
|
if (!g_initialized || modeIndex < 0 || modeIndex >= g_modeCount) return -1;
|
|
if (g_currentMode < 0 || g_currentMode >= g_modeCount) return -1;
|
|
if (!(g_modes[modeIndex].flags & montauk::abi::DISPLAY_MODE_DRIVER_VALID))
|
|
return -2;
|
|
if (modeIndex == g_currentMode) return modeIndex;
|
|
|
|
g_flipLock.Acquire();
|
|
|
|
// Link rate and framebuffer geometry remain fixed. Only timings with
|
|
// the same active area and pixel clock are safe without retraining the
|
|
// firmware-established HDMI/DP/eDP link.
|
|
const DisplayMode& current = g_modes[g_currentMode].timing;
|
|
const DisplayMode& requested = g_modes[modeIndex].timing;
|
|
if (requested.hdisplay != current.hdisplay
|
|
|| requested.vdisplay != current.vdisplay
|
|
|| requested.pixelClock == 0
|
|
|| requested.pixelClock != current.pixelClock) {
|
|
g_flipLock.Release();
|
|
return -2;
|
|
}
|
|
|
|
if (!ApplyModeTiming(requested)) {
|
|
g_flipLock.Release();
|
|
KernelLogStream(ERROR, "IntelGPU") << "Modeset transaction failed; previous mode restored";
|
|
return -3;
|
|
}
|
|
|
|
g_modes[g_currentMode].flags &= ~montauk::abi::DISPLAY_MODE_CURRENT;
|
|
g_currentMode = modeIndex;
|
|
g_modes[g_currentMode].flags |= montauk::abi::DISPLAY_MODE_CURRENT;
|
|
g_flipLock.Release();
|
|
KernelLogStream(OK, "IntelGPU") << "Mode set to " << base::dec
|
|
<< (uint64_t)requested.hdisplay << "x" << (uint64_t)requested.vdisplay
|
|
<< " @ " << (uint64_t)(g_modes[modeIndex].refreshMilliHz / 1000) << "Hz";
|
|
return modeIndex;
|
|
}
|
|
|
|
int Brightness(int percent) {
|
|
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;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Page flip public API
|
|
// =========================================================================
|
|
|
|
bool FlipSupported() {
|
|
return g_flipSupported;
|
|
}
|
|
|
|
uint64_t GetBufferPhys(int index) {
|
|
if (index == 0) return g_fbPhysBase;
|
|
if (index == 1 && g_flipSupported) return g_buf1Phys;
|
|
return 0;
|
|
}
|
|
|
|
uint64_t GetBufferPageCount(int index) {
|
|
if (index == 0) return (g_fbSize + 0xFFF) >> 12;
|
|
if (index == 1 && g_flipSupported) return g_buf1Pages;
|
|
return 0;
|
|
}
|
|
|
|
int GetFrontBuffer() {
|
|
g_flipLock.Acquire();
|
|
int live = ReadLiveBufferIndex();
|
|
if (live >= 0) g_frontBuffer = live;
|
|
g_flipLock.Release();
|
|
return live;
|
|
}
|
|
|
|
int AcquireFlip(int pid) {
|
|
if (!g_flipSupported || pid < 0) return -1;
|
|
|
|
g_flipLock.Acquire();
|
|
int oldOwner = g_flipOwnerPid;
|
|
g_flipOwnerPid = pid;
|
|
int live = ReadLiveBufferIndex();
|
|
if (live < 0) {
|
|
// Recover from an unrecognised/pending firmware surface by putting
|
|
// the legacy buffer back synchronously before userspace draws.
|
|
WriteReg(DSPASURF, (uint32_t)g_fbGttOffsetA);
|
|
(void)ReadReg(DSPASURF);
|
|
if (WaitForLiveBuffer(0)) live = 0;
|
|
}
|
|
if (live >= 0) g_frontBuffer = live;
|
|
g_flipLock.Release();
|
|
if (oldOwner != pid) {
|
|
KernelLogStream(DEBUG, "IntelGPU") << "Flip owner " << base::dec
|
|
<< (uint64_t)(oldOwner < 0 ? 0xFFFFFFFFu : (uint32_t)oldOwner)
|
|
<< " -> " << (uint64_t)pid;
|
|
}
|
|
KernelLogStream(DEBUG, "IntelGPU") << "Flip acquired by pid "
|
|
<< base::dec << (uint64_t)pid << " (live="
|
|
<< (uint64_t)(live < 0 ? 0xFFFFFFFFu : (uint32_t)live) << ")";
|
|
return live;
|
|
}
|
|
|
|
int64_t Flip(int index, bool waitVsync, int pid) {
|
|
if (!g_flipSupported || index < 0 || index > 1 || pid < 0) return -1;
|
|
|
|
g_flipLock.Acquire();
|
|
int oldOwner = g_flipOwnerPid;
|
|
g_flipOwnerPid = pid;
|
|
|
|
// Scanout memory is exposed through WC mappings. Drain userspace's
|
|
// completed frame before programming the display-plane surface.
|
|
asm volatile("sfence" ::: "memory");
|
|
uint64_t gttOff = (index == 0) ? g_fbGttOffsetA : g_buf1GttOffset;
|
|
WriteReg(DSPASURF, (uint32_t)gttOff);
|
|
(void)ReadReg(DSPASURF);
|
|
|
|
if (waitVsync) {
|
|
// DSPASURFLIVE is authoritative and avoids taking ownership of
|
|
// the GPU's shared interrupt domain merely to wait for vblank.
|
|
if (!WaitForLiveBuffer(index)) {
|
|
int live = ReadLiveBufferIndex();
|
|
if (live >= 0) g_frontBuffer = live;
|
|
uint32_t programmed = ReadReg(DSPASURF);
|
|
uint32_t liveSurface = ReadReg(DSPASURFLIVE);
|
|
g_flipLock.Release();
|
|
if (oldOwner != pid) {
|
|
KernelLogStream(DEBUG, "IntelGPU") << "Flip owner " << base::dec
|
|
<< (uint64_t)(oldOwner < 0 ? 0xFFFFFFFFu : (uint32_t)oldOwner)
|
|
<< " -> " << (uint64_t)pid;
|
|
}
|
|
KernelLogStream(WARNING, "IntelGPU") << "Flip latch timeout: requested="
|
|
<< base::dec << (uint64_t)index << " live="
|
|
<< (uint64_t)(live < 0 ? 0xFFFFFFFFu : (uint32_t)live)
|
|
<< " DSPASURF=" << base::hex << (uint64_t)programmed
|
|
<< " DSPASURFLIVE=" << (uint64_t)liveSurface;
|
|
return -1;
|
|
}
|
|
g_frontBuffer = index;
|
|
} else {
|
|
int live = ReadLiveBufferIndex();
|
|
if (live >= 0) g_frontBuffer = live;
|
|
}
|
|
g_flipLock.Release();
|
|
if (oldOwner != pid) {
|
|
KernelLogStream(DEBUG, "IntelGPU") << "Flip owner " << base::dec
|
|
<< (uint64_t)(oldOwner < 0 ? 0xFFFFFFFFu : (uint32_t)oldOwner)
|
|
<< " -> " << (uint64_t)pid;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
void OnProcessExit(int pid) {
|
|
if (!g_flipSupported) return;
|
|
g_flipLock.Acquire();
|
|
if (pid != g_flipOwnerPid) {
|
|
g_flipLock.Release();
|
|
return;
|
|
}
|
|
g_flipOwnerPid = -1;
|
|
|
|
uint32_t programmedBefore = ReadReg(DSPASURF);
|
|
uint32_t liveBefore = ReadReg(DSPASURFLIVE);
|
|
WriteReg(DSPASURF, (uint32_t)g_fbGttOffsetA);
|
|
(void)ReadReg(DSPASURF);
|
|
bool liveAtZero = WaitForLiveBuffer(0);
|
|
bool restored = liveAtZero;
|
|
int live = ReadLiveBufferIndex();
|
|
if (live >= 0) g_frontBuffer = live;
|
|
uint32_t programmedAfter = ReadReg(DSPASURF);
|
|
uint32_t liveAfter = ReadReg(DSPASURFLIVE);
|
|
g_flipLock.Release();
|
|
|
|
KernelLogStream(restored ? INFO : ERROR, "IntelGPU")
|
|
<< "Flip owner pid " << base::dec << (uint64_t)pid
|
|
<< " exited; restore buffer 0 " << (restored ? "latched" : "timed out")
|
|
<< " (before surf/live=" << base::hex << (uint64_t)programmedBefore
|
|
<< "/" << (uint64_t)liveBefore << ", after="
|
|
<< (uint64_t)programmedAfter << "/"
|
|
<< (uint64_t)liveAfter << ")";
|
|
}
|
|
|
|
void PanicRestoreScanout() {
|
|
// g_frontBuffer is only a software cache and can be stale precisely
|
|
// when the GPU or kernel is failing. Always request buffer 0 so the
|
|
// panic console cannot be left painting an invisible surface.
|
|
if (!g_flipSupported || !g_mmioBase) return;
|
|
WriteReg(DSPASURF, (uint32_t)g_fbGttOffsetA);
|
|
(void)ReadReg(DSPASURF);
|
|
g_frontBuffer = 0;
|
|
}
|
|
|
|
void Reinitialize() {
|
|
if (!g_initialized || !g_mmioBase) return;
|
|
|
|
KernelLogStream(INFO, "IntelGPU") << "Reinitializing display after S3 resume"
|
|
<< " (gen " << base::dec << (uint64_t)g_gpuGen << ")";
|
|
|
|
// 1. Re-enable PCI memory space and bus mastering. S3 resets the PCI
|
|
// command register, so MMIO writes to the GPU are silently dropped
|
|
// until we re-enable these bits.
|
|
Pci::EnableBusMaster(g_gpuInfo.pciBus, g_gpuInfo.pciDevice, g_gpuInfo.pciFunction);
|
|
DisableInterruptDelivery();
|
|
|
|
// Verify PCI memory space is accessible by reading back command register
|
|
uint16_t pciCmd = Pci::LegacyRead16(g_gpuInfo.pciBus, g_gpuInfo.pciDevice,
|
|
g_gpuInfo.pciFunction, (uint8_t)Pci::PCI_REG_COMMAND);
|
|
if (!(pciCmd & Pci::PCI_CMD_MEM_SPACE)) {
|
|
KernelLogStream(ERROR, "IntelGPU")
|
|
<< "PCI memory space not enabled after restore (cmd="
|
|
<< base::hex << (uint64_t)pciCmd << ")";
|
|
return;
|
|
}
|
|
|
|
// Sanity-check MMIO access: read a register and verify we don't get 0xFFFFFFFF
|
|
// (which indicates the device is not responding on the PCI bus)
|
|
uint32_t testRead = ReadReg(PIPEACONF);
|
|
if (testRead == 0xFFFFFFFF) {
|
|
KernelLogStream(ERROR, "IntelGPU")
|
|
<< "MMIO reads return 0xFFFFFFFF - GPU not responding";
|
|
return;
|
|
}
|
|
|
|
KernelLogStream(DEBUG, "IntelGPU") << "PCI memory space verified (cmd="
|
|
<< base::hex << (uint64_t)pciCmd << ")";
|
|
|
|
// 2. Disable VGA plane (firmware may have re-enabled it during POST)
|
|
DisableVga();
|
|
|
|
// 3. Restore legacy framebuffer mappings. Gen 8+ firmware scanout uses
|
|
// an aperture mapping whose backing physical pages are not described
|
|
// by the boot framebuffer address, so preserve the restored entries.
|
|
uint64_t pageCount = (g_fbSize + 0xFFF) / 0x1000;
|
|
if (g_gpuGen < 8) {
|
|
volatile uint32_t* gtt32 = (volatile uint32_t*)g_gttBase;
|
|
for (uint64_t i = 0; i < pageCount; i++) {
|
|
gtt32[i] = MakeGttPte32(g_fbPhysBase + i * 0x1000);
|
|
}
|
|
// Flush GTT writes by reading back the last entry
|
|
(void)gtt32[pageCount - 1];
|
|
}
|
|
|
|
KernelLogStream(DEBUG, "IntelGPU") << (g_gpuGen >= 8
|
|
? "Firmware GGTT scanout preserved"
|
|
: "Legacy GTT framebuffer mappings restored");
|
|
|
|
// 4. Re-enable the display pipe. After S3, the pipe may be off even
|
|
// though the firmware lit the backlight. Wait for it to become
|
|
// active before programming the display plane.
|
|
uint32_t pipeConf = ReadReg(PIPEACONF);
|
|
if (!(pipeConf & PIPECONF_ENABLE)) {
|
|
WriteReg(PIPEACONF, pipeConf | PIPECONF_ENABLE);
|
|
|
|
// Wait for pipe to become active (PIPECONF_STATE bit 30)
|
|
for (int i = 0; i < 100000; i++) {
|
|
if (ReadReg(PIPEACONF) & PIPECONF_STATE)
|
|
break;
|
|
asm volatile("pause");
|
|
}
|
|
|
|
if (!(ReadReg(PIPEACONF) & PIPECONF_STATE)) {
|
|
KernelLogStream(WARNING, "IntelGPU")
|
|
<< "Pipe A did not become active after enable";
|
|
}
|
|
}
|
|
|
|
// 5. Reprogram display plane to point at our GTT-mapped framebuffer
|
|
ProgramDisplayPlane();
|
|
|
|
// 6. Restore page-flip state: rewrite buffer 1's GGTT entries and
|
|
// re-show whichever buffer was front when we suspended.
|
|
if (g_flipSupported) {
|
|
uint64_t pages = (g_fbSize + 0xFFF) >> 12;
|
|
uint64_t baseIdx = g_buf1GttOffset >> 12;
|
|
for (uint64_t i = 0; i < pages; i++) {
|
|
WriteGgttPte(baseIdx + i, g_buf1Phys + i * 0x1000);
|
|
}
|
|
(void)ReadGgttPte(baseIdx + pages - 1);
|
|
|
|
if (g_frontBuffer != 0) {
|
|
WriteReg(DSPASURF, (uint32_t)g_buf1GttOffset);
|
|
(void)ReadReg(DSPASURF);
|
|
}
|
|
|
|
KernelLogStream(DEBUG, "IntelGPU") << "Page flip state restored (front="
|
|
<< 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";
|
|
}
|
|
|
|
};
|