blob: dec85ebd0083d72991b3620be51300fa26628dbd [file] [log] [blame]
// Copyright 2017 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/lib/edid/edid.h"
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <iterator>
#include <memory>
#include <fbl/algorithm.h>
#include <fbl/alloc_checker.h>
#include "src/graphics/display/lib/api-types-cpp/display-timing.h"
#include "src/graphics/display/lib/driver-framework-migration-utils/logging/zxlogf.h"
#include "src/graphics/display/lib/edid/eisa_vid_lut.h"
#include "src/graphics/display/lib/edid/timings.h"
namespace {
template <typename T>
bool base_validate(const T* block) {
static_assert(sizeof(T) == edid::kBlockSize, "Size check for Edid struct");
const uint8_t* edid_bytes = reinterpret_cast<const uint8_t*>(block);
if (edid_bytes[0] != T::kTag) {
return false;
}
// The last byte of the 128-byte EDID data is a checksum byte which
// should make the 128 bytes sum to zero.
uint8_t sum = 0;
for (uint32_t i = 0; i < edid::kBlockSize; ++i) {
sum = static_cast<uint8_t>(sum + edid_bytes[i]);
}
return sum == 0;
}
} // namespace
namespace edid {
const char* GetEisaVendorName(uint16_t manufacturer_name_code) {
uint8_t c1 = static_cast<uint8_t>((((manufacturer_name_code >> 8) & 0x7c) >> 2) + 'A' - 1);
uint8_t c2 = static_cast<uint8_t>(
((((manufacturer_name_code >> 8) & 0x03) << 3) | (manufacturer_name_code & 0xe0) >> 5) + 'A' -
1);
uint8_t c3 = static_cast<uint8_t>(((manufacturer_name_code & 0x1f)) + 'A' - 1);
return lookup_eisa_vid(EISA_ID(c1, c2, c3));
}
bool BaseEdid::validate() const {
static const uint8_t kEdidHeader[8] = {0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0};
return base_validate<BaseEdid>(this) && memcmp(header, kEdidHeader, sizeof(kEdidHeader)) == 0;
}
bool CeaEdidTimingExtension::validate() const {
if (!(dtd_start_idx <= sizeof(payload) && base_validate<CeaEdidTimingExtension>(this))) {
return false;
}
// If this is zero, there is no DTDs present and no non-DTD data.
if (dtd_start_idx == 0) {
return true;
}
if (dtd_start_idx > 0 && dtd_start_idx < offsetof(CeaEdidTimingExtension, payload)) {
return false;
}
size_t offset = 0;
size_t dbc_end = dtd_start_idx - offsetof(CeaEdidTimingExtension, payload);
while (offset < dbc_end) {
const DataBlock* data_block = reinterpret_cast<const DataBlock*>(payload + offset);
offset += (1 + data_block->length()); // Length doesn't include the header
// Check that the block doesn't run past the end if the dbc
if (offset > dbc_end) {
return false;
}
}
return true;
}
ReadEdidResult ReadEdidFromDdcForTesting(void* ctx, ddc_i2c_transact transact) {
uint8_t segment_address = 0;
uint8_t segment_offset = 0;
ddc_i2c_msg_t msgs[3] = {
{.is_read = false, .addr = kDdcSegmentI2cAddress, .buf = &segment_address, .length = 1},
{.is_read = false, .addr = kDdcDataI2cAddress, .buf = &segment_offset, .length = 1},
{.is_read = true, .addr = kDdcDataI2cAddress, .buf = nullptr, .length = kBlockSize},
};
BaseEdid base_edid;
msgs[2].buf = reinterpret_cast<uint8_t*>(&base_edid);
// The VESA E-DDC standard claims that the segment pointer is reset to its
// default value (00h) at the completion of each command sequence.
// (Section 2.2.5 "Segment Pointer", Page 18, VESA E-DDC Standard,
// Version 1.3)
//
// Note that we are not following the recommended reading patterns in the
// E-DDC standard, which requires drivers always issue segment writes for
// each read and ignore the NACKs (Section 5.1.1 "Basic Operation for E-DDC
// Access of EDID", S 6.5 "E-DDC Sequential Read", VESA E-DDC Standard,
// Version 1.3). Instead, when reading the first block (base EDID), we skip
// writing the segment address register and rely on display devices' reset
// mechanism.
//
// This makes the following EDID read procedure compatible with display
// devices that don't support Enhanced DDC standard; otherwise these devices
// will issue NACKs and some display controllers (e.g. Intel HD Display)
// might not be able to handle it correctly. It is possible that drivers may
// fail connecting to monitors that only recognizes some fixed structures or
// I2C command patterns (segment write always precedes data read), though the
// chance is rare.
if (!transact(ctx, msgs + 1, 2)) {
return ReadEdidResult::MakeError("Failed to read base edid");
}
if (!base_edid.validate()) {
return ReadEdidResult::MakeError("Failed to validate base edid");
}
uint16_t edid_length = static_cast<uint16_t>((base_edid.num_extensions + 1) * kBlockSize);
fbl::AllocChecker ac;
fbl::Vector<uint8_t> edid;
edid.resize(edid_length, &ac);
if (!ac.check()) {
return ReadEdidResult::MakeError("Failed to allocate edid storage");
}
memcpy(edid.data(), reinterpret_cast<void*>(&base_edid), kBlockSize);
for (uint8_t i = 1; i && i <= base_edid.num_extensions; i++) {
segment_address = i / 2;
segment_offset = i % 2 ? kBlockSize : 0;
msgs[2].buf = edid.data() + i * kBlockSize;
// The segment pointer is reset to zero every time after a command sequence.
// As long as the segment number is not zero, we should issue a DDC segment
// read before we read / write a piece of data.
bool include_segment = segment_address != 0;
bool transact_success = include_segment ? transact(ctx, msgs, 3) : transact(ctx, msgs + 1, 2);
if (!transact_success) {
return ReadEdidResult::MakeError("Failed to read full edid");
}
}
return ReadEdidResult::MakeEdidBytes(std::move(edid));
}
bool Edid::Init(void* ctx, ddc_i2c_transact transact, const char** err_msg) {
auto read_edid_result = ReadEdidFromDdcForTesting(ctx, transact);
if (read_edid_result.is_error) {
ZX_DEBUG_ASSERT(read_edid_result.error_message != nullptr);
*err_msg = read_edid_result.error_message;
return false;
}
edid_bytes_ = std::move(read_edid_result.edid_bytes);
return Init(edid_bytes_.data(), static_cast<uint16_t>(edid_bytes_.size()), err_msg);
}
bool Edid::Init(const uint8_t* bytes, uint16_t len, const char** err_msg) {
// The maximum size of an edid is 255 * 128 bytes, so any 16 bit multiple is fine.
if (len == 0 || len % kBlockSize != 0) {
*err_msg = "Invalid edid length";
return false;
}
bytes_ = bytes;
len_ = len;
if (!(base_edid_ = GetBlock<BaseEdid>(0)) || !base_edid_->validate()) {
*err_msg = "Failed to validate base edid";
return false;
}
if (((base_edid_->num_extensions + 1) * kBlockSize) != len) {
*err_msg = "Bad extension count";
return false;
}
if (!base_edid_->digital()) {
*err_msg = "Analog displays not supported";
return false;
}
for (uint8_t i = 1; i < len / kBlockSize; i++) {
if (bytes_[i * kBlockSize] == CeaEdidTimingExtension::kTag) {
if (!GetBlock<CeaEdidTimingExtension>(i)->validate()) {
*err_msg = "Failed to validate extensions";
return false;
}
}
}
monitor_serial_[0] = monitor_name_[0] = '\0';
for (auto it = descriptor_iterator(this); it.is_valid(); ++it) {
char* dest;
if (it->timing.pixel_clock_10khz != 0) {
continue;
} else if (it->monitor.type == Descriptor::Monitor::kName) {
dest = monitor_name_;
} else if (it->monitor.type == Descriptor::Monitor::kSerial) {
dest = monitor_serial_;
} else {
continue;
}
// Look for '\n' if it exists, otherwise take the whole string.
uint32_t len;
for (len = 0; len < sizeof(Descriptor::Monitor::data) && it->monitor.data[len] != 0x0A; ++len) {
// Empty body
}
// Copy the string and remember to null-terminate.
memcpy(dest, it->monitor.data, len);
dest[len] = '\0';
}
// If we didn't find a valid serial descriptor, use the base serial number
if (monitor_serial_[0] == '\0') {
sprintf(monitor_serial_, "%d", base_edid_->serial_number);
}
uint8_t c1 = static_cast<uint8_t>(((base_edid_->manufacturer_id1 & 0x7c) >> 2) + 'A' - 1);
uint8_t c2 = static_cast<uint8_t>(
(((base_edid_->manufacturer_id1 & 0x03) << 3) | (base_edid_->manufacturer_id2 & 0xe0) >> 5) +
'A' - 1);
uint8_t c3 = static_cast<uint8_t>(((base_edid_->manufacturer_id2 & 0x1f)) + 'A' - 1);
manufacturer_id_[0] = c1;
manufacturer_id_[1] = c2;
manufacturer_id_[2] = c3;
manufacturer_id_[3] = '\0';
manufacturer_name_ = lookup_eisa_vid(EISA_ID(c1, c2, c3));
return true;
}
template <typename T>
const T* Edid::GetBlock(uint8_t block_num) const {
const uint8_t* bytes = bytes_ + block_num * kBlockSize;
return bytes[0] == T::kTag ? reinterpret_cast<const T*>(bytes) : nullptr;
}
bool Edid::is_hdmi() const {
data_block_iterator dbs(this);
if (!dbs.is_valid() || dbs.cea_revision() < 0x03) {
return false;
}
do {
if (dbs->type() == VendorSpecificBlock::kType) {
// HDMI's 24-bit IEEE registration is 0x000c03 - vendor_number is little endian
if (dbs->payload.vendor.vendor_number[0] == 0x03 &&
dbs->payload.vendor.vendor_number[1] == 0x0c &&
dbs->payload.vendor.vendor_number[2] == 0x00) {
return true;
}
}
} while ((++dbs).is_valid());
return false;
}
display::DisplayTiming DetailedTimingDescriptorToDisplayTiming(
const DetailedTimingDescriptor& dtd) {
// A valid DetailedTimingDescriptor guarantees that
// horizontal_blanking >= horizontal_front_porch + horizontal_sync_pulse, and
// all of them are non-negative and fit in [0, kMaxTimingValue].
//
// This constraint guarantees that
// horizontal_blanking - (horizontal_front_porch + horizontal_sync_pulse)
// will also fit in [0, kMaxTimingValue]; the calculation won't overflow,
// causing undefined behaviors.
int32_t horizontal_back_porch_px =
static_cast<int32_t>(dtd.horizontal_blanking() -
(dtd.horizontal_front_porch() + dtd.horizontal_sync_pulse_width()));
// A similar argument holds for the vertical back porch.
int32_t vertical_back_porch_lines = static_cast<int32_t>(
dtd.vertical_blanking() - (dtd.vertical_front_porch() + dtd.vertical_sync_pulse_width()));
if (dtd.type() != TYPE_DIGITAL_SEPARATE) {
// TODO(https://fxbug.dev/42086615): Displays using composite syncs are not
// supported. We treat them as if they were using separate sync signals.
zxlogf(WARNING, "The detailed timing descriptor uses composite sync; this is not supported.");
}
return display::DisplayTiming{
.horizontal_active_px = static_cast<int32_t>(dtd.horizontal_addressable()),
.horizontal_front_porch_px = static_cast<int32_t>(dtd.horizontal_front_porch()),
.horizontal_sync_width_px = static_cast<int32_t>(dtd.horizontal_sync_pulse_width()),
.horizontal_back_porch_px = horizontal_back_porch_px,
.vertical_active_lines = static_cast<int32_t>(dtd.vertical_addressable()),
.vertical_front_porch_lines = static_cast<int32_t>(dtd.vertical_front_porch()),
.vertical_sync_width_lines = static_cast<int32_t>(dtd.vertical_sync_pulse_width()),
.vertical_back_porch_lines = vertical_back_porch_lines,
.pixel_clock_frequency_hz = int64_t{dtd.pixel_clock_10khz} * 10'000,
.fields_per_frame = dtd.interlaced() ? display::FieldsPerFrame::kInterlaced
: display::FieldsPerFrame::kProgressive,
.hsync_polarity = dtd.hsync_polarity() ? display::SyncPolarity::kPositive
: display::SyncPolarity::kNegative,
.vsync_polarity = dtd.vsync_polarity() ? display::SyncPolarity::kPositive
: display::SyncPolarity::kNegative,
.vblank_alternates = false,
.pixel_repetition = 0,
};
}
// Returns std::nullopt if the standard timing descriptor `std` is invalid or
// unsupported.
// Otherwise, returns the DisplayTiming converted from the descriptor.
std::optional<display::DisplayTiming> StandardTimingDescriptorToDisplayTiming(
const BaseEdid& edid, const StandardTimingDescriptor& std) {
// Pick the largest resolution advertised by the display and then use the
// generalized timing formula to compute the timing parameters.
// TODO(stevensd): Support interlaced modes and margins
int32_t width = static_cast<int32_t>(std.horizontal_resolution());
int32_t height =
static_cast<int32_t>(std.vertical_resolution(edid.edid_version, edid.edid_revision));
int32_t v_rate = static_cast<int32_t>(std.vertical_freq()) + 60;
if (!width || !height || !v_rate) {
zxlogf(WARNING, "Invalid standard timing descriptor: %" PRId32 " x %" PRId32 "@ %" PRId32 " Hz",
width, height, v_rate);
return std::nullopt;
}
for (const display::DisplayTiming& dmt_timing : internal::kDmtDisplayTimings) {
if (dmt_timing.horizontal_active_px == width && dmt_timing.vertical_active_lines == height &&
((dmt_timing.vertical_field_refresh_rate_millihertz() + 500) / 1000) == v_rate) {
return dmt_timing;
}
}
zxlogf(WARNING,
"This EDID contains a non-DMT standard timing (%" PRIu32 "x%" PRIu32 " @%" PRIu32
"Hz). The timing is not supported and will be ignored. See https://fxbug.dev/42085380 for "
"details.",
width, height, v_rate);
return std::nullopt;
}
timing_iterator& timing_iterator::operator++() {
while (state_ != kDone) {
Advance();
// If either of these are 0, then the timing value is definitely wrong
if (display_timing_.vertical_active_lines != 0 && display_timing_.horizontal_active_px != 0) {
break;
}
}
return *this;
}
void timing_iterator::Advance() {
if (state_ == kDtds) {
while (descriptors_.is_valid()) {
if (descriptors_->timing.pixel_clock_10khz != 0) {
display_timing_ = DetailedTimingDescriptorToDisplayTiming(descriptors_->timing);
++descriptors_;
return;
}
++descriptors_;
}
state_ = kSvds;
state_index_ = UINT16_MAX;
}
if (state_ == kSvds) {
while (dbs_.is_valid()) {
if (dbs_->type() == ShortVideoDescriptor::kType) {
state_index_++;
uint32_t modes_to_skip = state_index_;
for (unsigned i = 0; i < dbs_->length(); i++) {
uint32_t idx = dbs_->payload.video[i].standard_mode_idx() - 1;
if (idx >= internal::kCtaDisplayTimings.size()) {
continue;
}
if (modes_to_skip == 0) {
display_timing_ = internal::kCtaDisplayTimings[idx];
return;
}
// For timings with refresh rates that are multiples of 6, there are
// corresponding timings adjusted by a factor of 1000/1001.
//
// TODO(https://fxbug.dev/42086617): Revise the refresh rate adjustment
// logic to make sure that it complies with the CTA-861 standards.
uint32_t rounded_refresh =
(internal::kCtaDisplayTimings[idx].vertical_field_refresh_rate_millihertz() + 999) /
1000;
if (rounded_refresh % 6 == 0) {
if (modes_to_skip == 1) {
display_timing_ = internal::kCtaDisplayTimings[idx];
// `pixel_clock_frequency_hz` is less than 2^41, and `double` has
// 51 fractional bits, so `pixel_clock_frequency_hz` can be
// converted to a double value without losing precision.
double clock = static_cast<double>(display_timing_.pixel_clock_frequency_hz);
// 240/480 height entries are already multiplied by 1000/1001
double mult = display_timing_.vertical_active_lines == 240 ||
display_timing_.vertical_active_lines == 480
? 1.001
: (1000. / 1001.);
display_timing_.pixel_clock_frequency_hz = static_cast<int64_t>(round(clock * mult));
return;
}
modes_to_skip -= 2;
} else {
modes_to_skip--;
}
}
}
++dbs_;
// Reset the index for either the next SVD block or the STDs.
state_index_ = UINT16_MAX;
}
state_ = kStds;
}
if (state_ == kStds) {
while (++state_index_ < std::size(edid_->base_edid_->standard_timings)) {
const StandardTimingDescriptor* desc = edid_->base_edid_->standard_timings + state_index_;
if (desc->byte1 == 0x01 && desc->byte2 == 0x01) {
continue;
}
std::optional<display::DisplayTiming> display_timing =
StandardTimingDescriptorToDisplayTiming(*edid_->base_edid_, *desc);
if (display_timing.has_value()) {
display_timing_ = *display_timing;
}
return;
}
state_ = kDone;
}
}
audio_data_block_iterator& audio_data_block_iterator::operator++() {
while (dbs_.is_valid()) {
uint32_t num_sads = static_cast<uint32_t>(dbs_->length() / sizeof(ShortAudioDescriptor));
if (dbs_->type() != ShortAudioDescriptor::kType || ++sad_idx_ > num_sads) {
++dbs_;
sad_idx_ = UINT8_MAX;
continue;
}
descriptor_ = dbs_->payload.audio[sad_idx_];
return *this;
}
edid_ = nullptr;
return *this;
}
Edid::descriptor_iterator& Edid::descriptor_iterator::operator++() {
if (!edid_) {
return *this;
}
if (block_idx_ == 0) {
descriptor_idx_++;
if (descriptor_idx_ < std::size(edid_->base_edid_->detailed_descriptors)) {
descriptor_ = edid_->base_edid_->detailed_descriptors + descriptor_idx_;
if (descriptor_->timing.pixel_clock_10khz != 0 || descriptor_->monitor.type != 0x10) {
return *this;
}
}
block_idx_++;
descriptor_idx_ = UINT32_MAX;
}
while (block_idx_ < (edid_->len_ / kBlockSize)) {
auto cea_extn_block = edid_->GetBlock<CeaEdidTimingExtension>(block_idx_);
size_t offset = sizeof(CeaEdidTimingExtension::payload);
if (cea_extn_block &&
cea_extn_block->dtd_start_idx > offsetof(CeaEdidTimingExtension, payload)) {
offset = cea_extn_block->dtd_start_idx - offsetof(CeaEdidTimingExtension, payload);
}
descriptor_idx_++;
offset += sizeof(Descriptor) * descriptor_idx_;
// Return if the descriptor is within bounds and either a timing descriptor or not
// a dummy monitor descriptor, otherwise advance to the next block
if (offset + sizeof(DetailedTimingDescriptor) <= sizeof(CeaEdidTimingExtension::payload)) {
descriptor_ = reinterpret_cast<const Descriptor*>(cea_extn_block->payload + offset);
if (descriptor_->timing.pixel_clock_10khz != 0 ||
descriptor_->monitor.type != Descriptor::Monitor::kDummyType) {
return *this;
}
}
block_idx_++;
descriptor_idx_ = UINT32_MAX;
}
edid_ = nullptr;
return *this;
}
Edid::data_block_iterator::data_block_iterator(const Edid* edid) : edid_(edid) {
++(*this);
if (is_valid()) {
cea_revision_ = edid_->GetBlock<CeaEdidTimingExtension>(block_idx_)->revision_number;
}
}
Edid::data_block_iterator& Edid::data_block_iterator::operator++() {
if (!edid_) {
return *this;
}
while (block_idx_ < (edid_->len_ / kBlockSize)) {
auto cea_extn_block = edid_->GetBlock<CeaEdidTimingExtension>(block_idx_);
size_t dbc_end = 0;
if (cea_extn_block &&
cea_extn_block->dtd_start_idx > offsetof(CeaEdidTimingExtension, payload)) {
dbc_end = cea_extn_block->dtd_start_idx - offsetof(CeaEdidTimingExtension, payload);
}
db_idx_++;
uint32_t db_to_skip = db_idx_;
uint32_t offset = 0;
while (offset < dbc_end) {
auto* dblk = reinterpret_cast<const DataBlock*>(cea_extn_block->payload + offset);
if (db_to_skip == 0) {
db_ = dblk;
return *this;
}
db_to_skip--;
offset += (dblk->length() + 1); // length doesn't include the data block header byte
}
block_idx_++;
db_idx_ = UINT32_MAX;
}
edid_ = nullptr;
return *this;
}
void Edid::Print(void (*print_fn)(const char* str)) const {
char str_buf[128];
print_fn("Raw edid:\n");
for (auto i = 0; i < edid_length(); i++) {
constexpr int kBytesPerLine = 16;
char* b = str_buf;
if (i % kBytesPerLine == 0) {
b += sprintf(b, "%04x: ", i);
}
sprintf(b, "%02x%s", edid_bytes()[i], i % kBytesPerLine == kBytesPerLine - 1 ? "\n" : " ");
print_fn(str_buf);
}
}
bool Edid::supports_basic_audio() const {
uint8_t block_idx = 1; // Skip block 1, since it can't be a CEA block
while (block_idx < (len_ / kBlockSize)) {
auto cea_extn_block = GetBlock<CeaEdidTimingExtension>(block_idx);
if (cea_extn_block && cea_extn_block->revision_number >= 2) {
return cea_extn_block->basic_audio();
}
block_idx++;
}
return false;
}
} // namespace edid