blob: 77d4d66b2295e22a708ace2f6886c7bfa7340c34 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/graphics/display/drivers/intel-i915/igd.h"
#include <lib/device-protocol/pci.h>
#include <lib/zircon-internal/align.h>
#include <lib/zx/result.h>
#include <zircon/status.h>
#include <climits>
#include <hwreg/bitfields.h>
#include "src/graphics/display/drivers/intel-i915/acpi-memory-region.h"
#include "src/graphics/display/drivers/intel-i915/firmware-bridge.h"
namespace i915 {
namespace {
// Register definitions from IGD OpRegion/Software SCI documentation. Section
// numbers reference Skylake Sept 2016 rev 0.5.
// The number of eDP panel types supported by the IGD API
static const uint32_t kNumPanelTypes = 16;
// Entry half of Software SCI Entry/Exit Parameters - 3.3.1
class SciEntryParam : public hwreg::RegisterBase<SciEntryParam, uint32_t> {
public:
DEF_RSVDZ_FIELD(31, 16);
DEF_FIELD(15, 8, subfunction);
DEF_RSVDZ_FIELD(7, 5);
DEF_FIELD(4, 1, function);
DEF_BIT(0, swsci_indicator);
// Main function codes
static const uint32_t kFuncGetBiosData = 4;
// GetBiosData sub-function codes
static const uint32_t kGbdaSupportedCalls = 0;
static const uint32_t kGbdaPanelDetails = 5;
static auto Get() { return hwreg::RegisterAddr<SciEntryParam>(0); }
};
// Exit half of Software SCI Entry/Exit Parameters - 3.3.1
class SciExitParam : public hwreg::RegisterBase<SciExitParam, uint32_t> {
public:
DEF_RSVDZ_FIELD(31, 16);
DEF_FIELD(15, 8, exit_param);
DEF_FIELD(7, 5, exit_result);
DEF_RSVDZ_FIELD(4, 1);
DEF_BIT(0, swsci_indicator);
constexpr static uint32_t kResultOk = 1;
static auto Get() { return hwreg::RegisterAddr<SciExitParam>(0); }
};
// Additional param return value for GetBiosData supported calls function - 4.2.2
class GbdaSupportedCalls : public hwreg::RegisterBase<GbdaSupportedCalls, uint32_t> {
public:
DEF_RSVDZ_FIELD(31, 11);
DEF_BIT(10, get_aksv);
DEF_BIT(9, spread_spectrum_clocks);
DEF_RSVDZ_FIELD(8, 7);
DEF_BIT(6, internal_graphics);
DEF_BIT(5, tv_std_video_connector_info);
DEF_BIT(4, get_panel_details);
DEF_BIT(3, get_boot_display_preference);
DEF_RSVDZ_FIELD(2, 1);
DEF_BIT(0, requested_system_callbacks);
static auto Get() { return hwreg::RegisterAddr<GbdaSupportedCalls>(0); }
};
// Additional param return value for GetBiosData panel details function - 4.2.5
class GbdaPanelDetails : public hwreg::RegisterBase<GbdaPanelDetails, uint32_t> {
public:
DEF_RSVDZ_FIELD(31, 23);
DEF_FIELD(22, 20, bia_ctrl);
DEF_FIELD(19, 18, blc_support);
DEF_RSVDZ_BIT(17);
DEF_BIT(16, lid_state);
DEF_FIELD(15, 8, panel_type_plus1);
DEF_FIELD(7, 0, panel_scaling);
static auto Get() { return hwreg::RegisterAddr<GbdaPanelDetails>(0); }
};
static uint8_t iboost_idx_to_level(uint8_t iboost_idx) {
switch (iboost_idx) {
case 0:
return 1;
case 1:
return 3;
case 2:
return 7;
default:
zxlogf(INFO, "Invalid iboost override");
return 0;
}
}
bool IsPortHdmi(uint8_t dvo_port) {
switch (dvo_port) {
case 0: // DVO_PORT_HDMIA
case 1: // DVO_PORT_HDMIB
case 2: // DVO_PORT_HDMIC
case 3: // DVO_PORT_HDMID
case 12: // DVO_PORT_HDMIE
case 14: // DVO_PORT_HDMIF
case 16: // DVO_PORT_HDMIG
case 18: // DVO_PORT_HDMIH
case 20: // DVO_PORT_HDMII
return true;
default:
return false;
}
}
bool IsPortDisplayPort(uint8_t dvo_port) {
switch (dvo_port) {
case 10: // DVO_PORT_DPA
case 7: // DVO_PORT_DPB
case 8: // DVO_PORT_DPC
case 9: // DVO_PORT_DPD
case 11: // DVO_PORT_DPE
case 13: // DVO_PORT_DPF
case 15: // DVO_PORT_DPG
case 17: // DVO_PORT_DPH
case 19: // DVO_PORT_DPI
return true;
default:
return false;
}
}
std::optional<DdiId> PortToDdi(uint8_t dvo_port) {
switch (dvo_port) {
case 0: // DVO_PORT_HDMIA
case 10: // DVO_PORT_DPA
return DdiId::DDI_A;
case 1: // DVO_PORT_HDMIB
case 7: // DVO_PORT_DPB
return DdiId::DDI_B;
case 2: // DVO_PORT_HDMIC
case 8: // DVO_PORT_DPC
return DdiId::DDI_C;
case 3: // DVO_PORT_HDMID
case 9: // DVO_PORT_DPD
// i.e. DDI_TC_1
return DdiId::DDI_D;
case 12: // DVO_PORT_HDMIE
case 11: // DVO_PORT_DPE
// i.e. DDI_TC_2
return DdiId::DDI_E;
case 14: // DVO_PORT_HDMIF
case 13: // DVO_PORT_DPF
return DdiId::DDI_TC_3;
case 16: // DVO_PORT_HDMIG
case 15: // DVO_PORT_DPG
return DdiId::DDI_TC_4;
case 18: // DVO_PORT_HDMIH
case 17: // DVO_PORT_DPH
return DdiId::DDI_TC_5;
case 20: // DVO_PORT_HDMII
case 19: // DVO_PORT_DPI
return DdiId::DDI_TC_6;
default:
return std::nullopt;
}
}
} // namespace
IgdOpRegion::~IgdOpRegion() = default;
template <typename T>
T* IgdOpRegion::GetSection(uint16_t* size) {
return reinterpret_cast<T*>(GetSection(T::kBlockType, size));
}
uint8_t* IgdOpRegion::GetSection(uint8_t type, uint16_t* size) {
uint8_t* data = reinterpret_cast<uint8_t*>(bdb_);
uint16_t idx = bdb_->header_size;
while (idx < bdb_->bios_data_blocks_size - sizeof(block_header_t)) {
block_header_t* header = reinterpret_cast<block_header_t*>(data + idx);
uint16_t block_size = static_cast<uint16_t>(header->size_low | (header->size_high << 8));
if (block_size > bdb_->bios_data_blocks_size) {
return nullptr;
}
uint16_t new_idx = static_cast<uint16_t>(idx + block_size + sizeof(block_header_t));
if (new_idx <= bdb_->bios_data_blocks_size && header->type == type) {
*size = block_size;
return data + idx + sizeof(block_header_t);
}
idx = new_idx;
}
return nullptr;
}
bool IgdOpRegion::ProcessDdiConfigs() {
uint16_t size;
general_definitions_t* defs = GetSection<general_definitions_t>(&size);
if (defs == nullptr) {
zxlogf(ERROR, "Couldn't find vbt general definitions");
return false;
}
if (size < sizeof(general_definitions_t)) {
zxlogf(ERROR, "Bad size in vbt general definitions");
return false;
}
uint16_t num_configs =
static_cast<uint16_t>((size - sizeof(general_definitions_t)) / defs->ddi_config_size);
for (int i = 0; i < num_configs; i++) {
ddi_config_t* cfg = reinterpret_cast<ddi_config_t*>(defs->ddis + i * defs->ddi_config_size);
if (!cfg->ddi_flags) {
continue;
}
auto ddi_flags = DdiFlags::Get().FromValue(cfg->ddi_flags);
if (IsPortHdmi(cfg->port_type)) {
if (!ddi_flags.tmds()) {
zxlogf(WARNING, "Malformed hdmi config");
continue;
}
} else if (IsPortDisplayPort(cfg->port_type)) {
if (!ddi_flags.dp()) {
zxlogf(WARNING, "Malformed dp config");
continue;
}
} else {
zxlogf(WARNING, "The port %d is not supported, ignored.", cfg->port_type);
continue;
}
auto ddi = PortToDdi(cfg->port_type);
ZX_DEBUG_ASSERT(ddi.has_value());
if (ddi_features_.find(*ddi) != ddi_features_.end()) {
zxlogf(WARNING, "Duplicate ddi config");
continue;
}
ddi_features_[*ddi] = {
.supports_hdmi = ddi_flags.tmds() && !ddi_flags.not_hdmi(),
.supports_dvi = static_cast<bool>(ddi_flags.tmds()),
.supports_dp = static_cast<bool>(ddi_flags.dp()),
.is_edp = ddi_flags.dp() && ddi_flags.internal(),
.is_type_c = static_cast<bool>(cfg->is_usb_type_c()),
.is_thunderbolt = (bdb_->version >= 209) ? static_cast<bool>(cfg->is_thunderbolt()) : false,
.iboosts =
{
.hdmi_iboost = cfg->has_iboost_override()
? iboost_idx_to_level(cfg->dp_iboost_override())
: static_cast<uint8_t>(0),
.dp_iboost = cfg->has_iboost_override()
? iboost_idx_to_level(cfg->hdmi_iboost_override())
: static_cast<uint8_t>(0),
},
.hdmi_buffer_translation_idx = cfg->ddi_buf_trans_idx(),
};
}
return true;
}
bool IgdOpRegion::Swsci(ddk::Pci& pci, uint16_t function, uint16_t subfunction,
uint32_t additional_param, uint16_t* exit_param, uint32_t* additional_res) {
PciConfigOpRegion pci_op_region(pci);
zx::result<bool> in_use_result = pci_op_region.IsSystemControlInterruptInUse();
if (in_use_result.is_error()) {
zxlogf(WARNING, "Failed to read System Control Interrupt status from PCI OpRegion: %s",
in_use_result.status_string());
return false;
}
if (in_use_result.value()) {
zxlogf(WARNING, "OpRegion System Control Interrupt still in use after boot firmware handoff");
return false;
}
sci_interface_protocol_t* sci_interface =
reinterpret_cast<sci_interface_protocol_t*>(igd_opregion_->mailbox2);
auto sci_entry_param = SciEntryParam::Get().FromValue(0);
sci_entry_param.set_function(function);
sci_entry_param.set_subfunction(subfunction);
sci_entry_param.set_swsci_indicator(1);
sci_interface->entry_and_exit_params = sci_entry_param.reg_value();
sci_interface->additional_params = additional_param;
zx::result<> trigger_result = pci_op_region.TriggerSystemControlInterrupt();
if (trigger_result.is_error()) {
zxlogf(WARNING, "OpRegion System Control Interrupt triggering failed: %s",
trigger_result.status_string());
return false;
}
// The spec says to wait for 2ms if driver_sleep_timeout isn't set, but that's not
// long enough. I've seen delays as long as 10ms, so use 50ms to be safe.
int timeout_ms = sci_interface->driver_sleep_timeout ? sci_interface->driver_sleep_timeout : 50;
while (timeout_ms-- > 0) {
// TODO(costan): Polling should check both the "SWSCI in use" bit in SCIC in
// Mailbox 2 (currently checked) and the SWSCI trigger bit in the PCI config
// OpRegion.
auto sci_exit_param = SciExitParam::Get().FromValue(sci_interface->entry_and_exit_params);
if (!sci_exit_param.swsci_indicator()) {
if (sci_exit_param.exit_result() == SciExitParam::kResultOk) {
*exit_param = static_cast<uint16_t>(sci_exit_param.exit_param());
*additional_res = sci_interface->additional_params;
return true;
} else {
zxlogf(WARNING, "SWSCI failed (%x)", sci_exit_param.exit_result());
return false;
}
}
zx_nanosleep(zx_deadline_after(ZX_MSEC(1)));
}
zxlogf(WARNING, "SWSCI timeout");
return false;
}
bool IgdOpRegion::GetPanelType(ddk::Pci& pci, uint8_t* type) {
uint16_t exit_param;
uint32_t additional_res;
// TODO(stevensd): cache the supported calls when we need to use Swsci more than once
if (Swsci(pci, SciEntryParam::kFuncGetBiosData, SciEntryParam::kGbdaSupportedCalls,
0 /* unused additional_param */, &exit_param, &additional_res)) {
auto support = GbdaSupportedCalls::Get().FromValue(additional_res);
if (support.get_panel_details()) {
// TODO(stevensd): Support the case where there is >1 eDP panel
uint32_t panel_number = 0;
if (Swsci(pci, SciEntryParam::kFuncGetBiosData, SciEntryParam::kGbdaPanelDetails,
panel_number, &exit_param, &additional_res)) {
auto details = GbdaPanelDetails::Get().FromValue(additional_res);
if (details.panel_type_plus1() && details.panel_type_plus1() < (kNumPanelTypes + 1)) {
*type = static_cast<uint8_t>(details.panel_type_plus1() - 1);
zxlogf(DEBUG, "SWSCI panel type %d", *type);
return true;
}
}
}
}
uint16_t size;
lvds_config_t* cfg = GetSection<lvds_config_t>(&size);
if (!cfg || cfg->panel_type >= kNumPanelTypes) {
return false;
}
*type = cfg->panel_type;
return true;
}
bool IgdOpRegion::CheckForLowVoltageEdp(ddk::Pci& pci) {
bool has_edp = true;
for (const auto& kv : ddi_features_) {
has_edp |= kv.second.is_edp;
}
if (!has_edp) {
zxlogf(DEBUG, "No edp found");
return true;
}
uint16_t size;
edp_config_t* edp = GetSection<edp_config_t>(&size);
if (edp == nullptr) {
zxlogf(WARNING, "Couldn't find edp general definitions");
return false;
}
if (!GetPanelType(pci, &panel_type_)) {
zxlogf(TRACE, "No panel type");
return false;
}
edp_is_low_voltage_ =
!((edp->vswing_preemphasis[panel_type_ / 2] >> (4 * panel_type_ % 2)) & 0xf);
zxlogf(TRACE, "Is low voltage edp? %d", edp_is_low_voltage_);
return true;
}
void IgdOpRegion::ProcessBacklightData() {
uint16_t size;
lfp_backlight_t* data = GetSection<lfp_backlight_t>(&size);
if (data) {
lfp_backlight_entry_t* e = &data->entries[panel_type_];
min_backlight_brightness_ = e->min_brightness / 255.0;
}
}
zx_status_t IgdOpRegion::Init(zx_device_t* parent, ddk::Pci& pci) {
PciConfigOpRegion pci_op_region(pci);
zx::result<zx_paddr_t> memory_op_region_address = pci_op_region.ReadMemoryOpRegionAddress();
if (memory_op_region_address.is_error()) {
// Not logging at the ERROR level because this is an entirely plausible
// situation (since OpRegion support is optional for workstation systems),
// and we can do display initialization in many cases without OpRegion
// information.
zxlogf(WARNING, "Failed to get Memory OpRegion address: %s",
memory_op_region_address.status_string());
return memory_op_region_address.error_value();
}
zxlogf(TRACE, "Memory OpRegion start: %08" PRIx64, memory_op_region_address.value());
{
zx::result<AcpiMemoryRegion> memory_op_region =
AcpiMemoryRegion::Create(parent, memory_op_region_address.value(), kIgdOpRegionLen);
if (memory_op_region.is_error()) {
zxlogf(ERROR, "Failed to map IGD Memory OpRegion: %s",
zx_status_get_string(memory_op_region.error_value()));
return memory_op_region.error_value();
}
memory_op_region_ = std::move(memory_op_region).value();
igd_opregion_ = reinterpret_cast<igd_opregion_t*>(memory_op_region_.data().data());
if (!igd_opregion_->validate()) {
zxlogf(ERROR, "Failed to validate IGD Memory OpRegion");
return ZX_ERR_INTERNAL;
}
}
vbt_header_t* vbt_header = nullptr;
if (igd_opregion_->major_version() == 2 && igd_opregion_->minor_version() == 1 &&
igd_opregion_->asle_supported()) {
auto [rvda, rvds] = igd_opregion_->vbt_region();
zx::result<AcpiMemoryRegion> extended_vbt_region =
AcpiMemoryRegion::Create(parent, memory_op_region_address.value() + rvda, rvds);
if (extended_vbt_region.is_error()) {
zxlogf(ERROR, "Failed to map extended VBT: %s",
zx_status_get_string(extended_vbt_region.error_value()));
return extended_vbt_region.error_value();
}
extended_vbt_region_ = std::move(extended_vbt_region).value();
vbt_header = reinterpret_cast<vbt_header_t*>(extended_vbt_region_.data().data());
} else {
vbt_header = reinterpret_cast<vbt_header_t*>(&igd_opregion_->mailbox4);
}
if (!vbt_header->validate()) {
zxlogf(ERROR, "Failed to validate vbt header");
return ZX_ERR_INTERNAL;
}
bdb_ = reinterpret_cast<bios_data_blocks_header_t*>(reinterpret_cast<uintptr_t>(vbt_header) +
vbt_header->bios_data_blocks_offset);
uint16_t vbt_size = vbt_header->vbt_size;
if (!bdb_->validate() || bdb_->bios_data_blocks_size > vbt_size ||
vbt_header->bios_data_blocks_offset + bdb_->bios_data_blocks_size > vbt_size) {
zxlogf(ERROR, "Failed to validate bdb header");
return ZX_ERR_INTERNAL;
}
// TODO(stevensd): 196 seems old enough that all gen9 processors will have it. If we want to
// support older hardware, we'll need to handle missing data.
if (bdb_->version < 196) {
zxlogf(ERROR, "Out of date vbt (%d)", bdb_->version);
return ZX_ERR_INTERNAL;
}
if (!ProcessDdiConfigs() || !CheckForLowVoltageEdp(pci)) {
return ZX_ERR_INTERNAL;
}
ProcessBacklightData();
return ZX_OK;
}
} // namespace i915