From 8184870f93522e53e719a34861cad6337ae35cf0 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 26 Dec 2021 22:19:55 -0700 Subject: [PATCH] LibEDID: Add a library to parse EDID blobs This library can be used (for the most part) by kernel drivers as well as user mode. For this reason FixedPoint is used rather than floating point, but kept to a minimum. --- Tests/CMakeLists.txt | 1 + Tests/LibEDID/CMakeLists.txt | 7 + Tests/LibEDID/TestEDID.cpp | 408 +++++++ Userland/Libraries/CMakeLists.txt | 1 + Userland/Libraries/LibEDID/CMakeLists.txt | 8 + Userland/Libraries/LibEDID/DMT.cpp | 163 +++ Userland/Libraries/LibEDID/DMT.h | 64 ++ Userland/Libraries/LibEDID/EDID.cpp | 1190 +++++++++++++++++++++ Userland/Libraries/LibEDID/EDID.h | 436 ++++++++ Userland/Libraries/LibEDID/VIC.cpp | 199 ++++ Userland/Libraries/LibEDID/VIC.h | 42 + 11 files changed, 2519 insertions(+) create mode 100644 Tests/LibEDID/CMakeLists.txt create mode 100644 Tests/LibEDID/TestEDID.cpp create mode 100644 Userland/Libraries/LibEDID/CMakeLists.txt create mode 100644 Userland/Libraries/LibEDID/DMT.cpp create mode 100644 Userland/Libraries/LibEDID/DMT.h create mode 100644 Userland/Libraries/LibEDID/EDID.cpp create mode 100644 Userland/Libraries/LibEDID/EDID.h create mode 100644 Userland/Libraries/LibEDID/VIC.cpp create mode 100644 Userland/Libraries/LibEDID/VIC.h diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index b7b2951385..17e076de14 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory(LibC) add_subdirectory(LibCompress) add_subdirectory(LibCore) add_subdirectory(LibCpp) +add_subdirectory(LibEDID) add_subdirectory(LibELF) add_subdirectory(LibGfx) add_subdirectory(LibGL) diff --git a/Tests/LibEDID/CMakeLists.txt b/Tests/LibEDID/CMakeLists.txt new file mode 100644 index 0000000000..3349120f98 --- /dev/null +++ b/Tests/LibEDID/CMakeLists.txt @@ -0,0 +1,7 @@ +set(TEST_SOURCES + TestEDID.cpp +) + +foreach(source IN LISTS TEST_SOURCES) + serenity_test("${source}" LibEDID LIBS LibEDID) +endforeach() diff --git a/Tests/LibEDID/TestEDID.cpp b/Tests/LibEDID/TestEDID.cpp new file mode 100644 index 0000000000..aebde9f68f --- /dev/null +++ b/Tests/LibEDID/TestEDID.cpp @@ -0,0 +1,408 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +static const u8 edid1_bin[] = { + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x49, 0x14, 0x34, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x2a, 0x18, 0x01, 0x04, 0xa5, 0x1a, 0x13, 0x78, + 0x06, 0xee, 0x91, 0xa3, 0x54, 0x4c, 0x99, 0x26, 0x0f, 0x50, 0x54, 0x21, + 0x08, 0x00, 0xe1, 0xc0, 0xd1, 0xc0, 0xd1, 0x00, 0xa9, 0x40, 0xb3, 0x00, + 0x95, 0x00, 0x81, 0x80, 0x81, 0x40, 0x25, 0x20, 0x00, 0x66, 0x41, 0x00, + 0x1a, 0x30, 0x00, 0x1e, 0x33, 0x40, 0x04, 0xc3, 0x10, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0xfd, 0x00, 0x32, 0x7d, 0x1e, 0xa0, 0x78, 0x01, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x51, + 0x45, 0x4d, 0x55, 0x20, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x0a, + 0x00, 0x00, 0x00, 0xf7, 0x00, 0x0a, 0x00, 0x40, 0x82, 0x00, 0x28, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc4, 0x02, 0x03, 0x0a, 0x00, + 0x45, 0x7d, 0x65, 0x60, 0x59, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xf2 +}; + +TEST_CASE(edid1) +{ + auto edid_load_result = EDID::Parser::from_bytes({ edid1_bin, sizeof(edid1_bin) }); + EXPECT(!edid_load_result.is_error()); + auto edid = edid_load_result.release_value(); + EXPECT(edid.legacy_manufacturer_id() == "RHT"); + EXPECT(!edid.aspect_ratio().has_value()); + auto screen_size = edid.screen_size(); + EXPECT(screen_size.has_value()); + EXPECT(screen_size.value().horizontal_cm() == 26); + EXPECT(screen_size.value().vertical_cm() == 19); + auto gamma = edid.gamma(); + EXPECT(gamma.has_value()); + EXPECT(gamma.value() >= 2.19f && gamma.value() <= 2.21f); + EXPECT(edid.display_product_name() == "QEMU Monitor"); + + { + static constexpr struct { + unsigned width; + unsigned height; + unsigned refresh_rate; + EDID::Parser::EstablishedTiming::Source source; + u8 dmt_id { 0 }; + } expected_established_timings[] = { + { 640, 480, 60, EDID::Parser::EstablishedTiming::Source::IBM, 0x4 }, + { 800, 600, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x9 }, + { 1024, 768, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x10 }, + { 1280, 768, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x17 }, + { 1360, 768, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x27 }, + { 1400, 1050, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x2a }, + { 1792, 1344, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x3e }, + { 1856, 1392, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x41 }, + { 1920, 1440, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x49 } + }; + static constexpr size_t expected_established_timings_count = sizeof(expected_established_timings) / sizeof(expected_established_timings[0]); + size_t established_timings_found = 0; + auto result = edid.for_each_established_timing([&](auto& established_timings) { + EXPECT(established_timings_found < expected_established_timings_count); + auto& expected_timings = expected_established_timings[established_timings_found]; + EXPECT(established_timings.width() == expected_timings.width); + EXPECT(established_timings.height() == expected_timings.height); + EXPECT(established_timings.refresh_rate() == expected_timings.refresh_rate); + EXPECT(established_timings.source() == expected_timings.source); + EXPECT(established_timings.dmt_id() == expected_timings.dmt_id); + established_timings_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(established_timings_found == expected_established_timings_count); + } + + { + static constexpr struct { + unsigned width; + unsigned height; + unsigned refresh_rate; + u8 dmt_id { 0 }; + } expected_standard_established_timings[] = { + { 2048, 1152, 60, 0x54 }, + { 1920, 1080, 60, 0x52 }, + { 1920, 1200, 60, 0x45 }, + { 1600, 1200, 60, 0x33 }, + { 1680, 1050, 60, 0x3a }, + { 1440, 900, 60, 0x2f }, + { 1280, 1024, 60, 0x23 }, + { 1280, 960, 60, 0x20 } + }; + static constexpr size_t expected_standard_timings_count = sizeof(expected_standard_established_timings) / sizeof(expected_standard_established_timings[0]); + size_t standard_timings_found = 0; + auto result = edid.for_each_standard_timing([&](auto& standard_timings) { + EXPECT(standard_timings_found < expected_standard_timings_count); + auto& expected_timings = expected_standard_established_timings[standard_timings_found]; + EXPECT(standard_timings.dmt_id() == expected_timings.dmt_id); + EXPECT(standard_timings.width() == expected_timings.width); + EXPECT(standard_timings.height() == expected_timings.height); + EXPECT(standard_timings.refresh_rate() == expected_timings.refresh_rate); + standard_timings_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(standard_timings_found == expected_standard_timings_count); + } + + { + static constexpr struct { + unsigned block_id; + unsigned width; + unsigned height; + unsigned refresh_rate; + } expected_detailed_timings[] = { + { 0, 1024, 768, 75 } + }; + static constexpr size_t expected_detailed_timings_count = sizeof(expected_detailed_timings) / sizeof(expected_detailed_timings[0]); + size_t detailed_timings_found = 0; + auto result = edid.for_each_detailed_timing([&](auto& detailed_timing, unsigned block_id) { + EXPECT(detailed_timings_found < expected_detailed_timings_count); + auto& expected_timings = expected_detailed_timings[detailed_timings_found]; + EXPECT(block_id == expected_timings.block_id); + EXPECT(detailed_timing.horizontal_addressable_pixels() == expected_timings.width); + EXPECT(detailed_timing.vertical_addressable_lines() == expected_timings.height); + EXPECT(detailed_timing.refresh_rate().lround() == expected_timings.refresh_rate); + detailed_timings_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(detailed_timings_found == expected_detailed_timings_count); + } + + { + static constexpr u8 expected_vic_ids[] = { 125, 101, 96, 89, 31 }; + static constexpr size_t expected_vic_ids_count = sizeof(expected_vic_ids) / sizeof(expected_vic_ids[0]); + size_t vic_ids_found = 0; + auto result = edid.for_each_short_video_descriptor([&](unsigned block_id, bool is_native, EDID::VIC::Details const& vic) { + EXPECT(vic_ids_found < expected_vic_ids_count); + EXPECT(block_id == 1); + EXPECT(!is_native); // none are marked as native + EXPECT(vic.vic_id == expected_vic_ids[vic_ids_found]); + vic_ids_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(vic_ids_found == expected_vic_ids_count); + } + + { + // This edid has one CEA861 extension block only + size_t extension_blocks_found = 0; + auto result = edid.for_each_extension_block([&](unsigned block_id, u8 tag, u8 revision, ReadonlyBytes) { + EXPECT(block_id == 1); + EXPECT(tag == 0x2); + EXPECT(revision == 3); + extension_blocks_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(extension_blocks_found == 1); + } +} + +static const u8 edid2_bin[] = { + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x04, 0x72, 0x1d, 0x08, + 0xd2, 0x02, 0x96, 0x49, 0x20, 0x1e, 0x01, 0x04, 0xb5, 0x3c, 0x22, 0x78, + 0x3b, 0xff, 0x15, 0xa6, 0x53, 0x4a, 0x98, 0x26, 0x0f, 0x50, 0x54, 0xbf, + 0xef, 0x80, 0xd1, 0xc0, 0xb3, 0x00, 0x95, 0x00, 0x81, 0x80, 0x81, 0x40, + 0x81, 0xc0, 0x01, 0x01, 0x01, 0x01, 0x86, 0x6f, 0x00, 0x3c, 0xa0, 0xa0, + 0x0f, 0x50, 0x08, 0x20, 0x35, 0x00, 0x55, 0x50, 0x21, 0x00, 0x00, 0x1e, + 0x56, 0x5e, 0x00, 0xa0, 0xa0, 0xa0, 0x29, 0x50, 0x30, 0x20, 0x35, 0x00, + 0x55, 0x50, 0x21, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x30, + 0x4b, 0x78, 0x78, 0x1e, 0x01, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x00, 0x00, 0x00, 0xfc, 0x00, 0x43, 0x42, 0x32, 0x37, 0x32, 0x55, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x01, 0xc5, 0x02, 0x03, 0x33, 0x71, + 0x4c, 0x12, 0x13, 0x04, 0x1f, 0x90, 0x14, 0x05, 0x01, 0x11, 0x02, 0x03, + 0x4a, 0x23, 0x09, 0x07, 0x07, 0x83, 0x01, 0x00, 0x00, 0xe2, 0x00, 0xc0, + 0x67, 0x03, 0x0c, 0x00, 0x10, 0x00, 0x38, 0x3c, 0xe3, 0x05, 0xe3, 0x01, + 0xe3, 0x0f, 0x00, 0x00, 0xe6, 0x06, 0x07, 0x01, 0x60, 0x60, 0x45, 0x01, + 0x1d, 0x00, 0x72, 0x51, 0xd0, 0x1e, 0x20, 0x6e, 0x28, 0x55, 0x00, 0x55, + 0x50, 0x21, 0x00, 0x00, 0x1e, 0x01, 0x1d, 0x00, 0xbc, 0x52, 0xd0, 0x1e, + 0x20, 0xb8, 0x28, 0x55, 0x40, 0x55, 0x50, 0x21, 0x00, 0x00, 0x1e, 0x56, + 0x5e, 0x00, 0xa0, 0xa0, 0xa0, 0x29, 0x50, 0x30, 0x20, 0x35, 0x00, 0x55, + 0x50, 0x21, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xe1 +}; + +TEST_CASE(edid2) +{ + auto edid_load_result = EDID::Parser::from_bytes({ edid2_bin, sizeof(edid2_bin) }); + EXPECT(!edid_load_result.is_error()); + auto edid = edid_load_result.release_value(); + EXPECT(edid.legacy_manufacturer_id() == "ACR"); + EXPECT(edid.serial_number() == 1234567890); + auto digital_interface = edid.digital_display(); + EXPECT(digital_interface.has_value()); + EXPECT(digital_interface.value().color_bit_depth() == EDID::Parser::DigitalDisplay::ColorBitDepth::BPP_10); + EXPECT(digital_interface.value().supported_interface() == EDID::Parser::DigitalDisplay::SupportedInterface::DisplayPort); + EXPECT(!digital_interface.value().features().supports_standby()); + EXPECT(!digital_interface.value().features().supports_suspend()); + EXPECT(digital_interface.value().features().supports_off()); + EXPECT(digital_interface.value().features().preferred_timing_mode_includes_pixel_format_and_refresh_rate()); + EXPECT(!digital_interface.value().features().srgb_is_default_color_space()); + EXPECT(digital_interface.value().features().frequency() == EDID::Parser::DigitalDisplayFeatures::Frequency::Continuous); + EXPECT(digital_interface.value().features().supported_color_encodings() == EDID::Parser::DigitalDisplayFeatures::SupportedColorEncodings::RGB444_YCrCb444_YCrCb422); + EXPECT(!edid.aspect_ratio().has_value()); + auto screen_size = edid.screen_size(); + EXPECT(screen_size.has_value()); + EXPECT(screen_size.value().horizontal_cm() == 60); + EXPECT(screen_size.value().vertical_cm() == 34); + auto gamma = edid.gamma(); + EXPECT(gamma.has_value()); + EXPECT(gamma.value() >= 2.19f && gamma.value() <= 2.21f); + EXPECT(edid.display_product_name() == "CB272U"); + + { + static constexpr struct { + unsigned width; + unsigned height; + unsigned refresh_rate; + EDID::Parser::EstablishedTiming::Source source; + u8 dmt_id { 0 }; + } expected_established_timings[] = { + { 720, 400, 70, EDID::Parser::EstablishedTiming::Source::IBM }, + { 640, 480, 60, EDID::Parser::EstablishedTiming::Source::IBM, 0x4 }, + { 640, 480, 67, EDID::Parser::EstablishedTiming::Source::Apple }, + { 640, 480, 73, EDID::Parser::EstablishedTiming::Source::VESA, 0x5 }, + { 640, 480, 75, EDID::Parser::EstablishedTiming::Source::VESA, 0x6 }, + { 800, 600, 56, EDID::Parser::EstablishedTiming::Source::VESA, 0x8 }, + { 800, 600, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x9 }, + { 800, 600, 72, EDID::Parser::EstablishedTiming::Source::VESA, 0xa }, + { 800, 600, 75, EDID::Parser::EstablishedTiming::Source::VESA, 0xb }, + { 832, 624, 75, EDID::Parser::EstablishedTiming::Source::Apple }, + { 1024, 768, 60, EDID::Parser::EstablishedTiming::Source::VESA, 0x10 }, + { 1024, 768, 70, EDID::Parser::EstablishedTiming::Source::VESA, 0x11 }, + { 1024, 768, 75, EDID::Parser::EstablishedTiming::Source::VESA, 0x12 }, + { 1280, 1024, 75, EDID::Parser::EstablishedTiming::Source::VESA, 0x24 }, + { 1152, 870, 75, EDID::Parser::EstablishedTiming::Source::Apple } + }; + static constexpr size_t expected_established_timings_count = sizeof(expected_established_timings) / sizeof(expected_established_timings[0]); + size_t established_timings_found = 0; + auto result = edid.for_each_established_timing([&](auto& established_timings) { + EXPECT(established_timings_found < expected_established_timings_count); + auto& expected_timings = expected_established_timings[established_timings_found]; + EXPECT(established_timings.width() == expected_timings.width); + EXPECT(established_timings.height() == expected_timings.height); + EXPECT(established_timings.refresh_rate() == expected_timings.refresh_rate); + EXPECT(established_timings.source() == expected_timings.source); + EXPECT(established_timings.dmt_id() == expected_timings.dmt_id); + established_timings_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(established_timings_found == expected_established_timings_count); + } + + { + static constexpr struct { + unsigned width; + unsigned height; + unsigned refresh_rate; + u8 dmt_id { 0 }; + } expected_standard_established_timings[] = { + { 1920, 1080, 60, 0x52 }, + { 1680, 1050, 60, 0x3a }, + { 1440, 900, 60, 0x2f }, + { 1280, 1024, 60, 0x23 }, + { 1280, 960, 60, 0x20 }, + { 1280, 720, 60, 0x55 }, + }; + static constexpr size_t expected_standard_timings_count = sizeof(expected_standard_established_timings) / sizeof(expected_standard_established_timings[0]); + size_t standard_timings_found = 0; + auto result = edid.for_each_standard_timing([&](auto& standard_timings) { + EXPECT(standard_timings_found < expected_standard_timings_count); + auto& expected_timings = expected_standard_established_timings[standard_timings_found]; + EXPECT(standard_timings.dmt_id() == expected_timings.dmt_id); + EXPECT(standard_timings.width() == expected_timings.width); + EXPECT(standard_timings.height() == expected_timings.height); + EXPECT(standard_timings.refresh_rate() == expected_timings.refresh_rate); + standard_timings_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(standard_timings_found == expected_standard_timings_count); + } + + { + static constexpr struct { + unsigned block_id; + unsigned width; + unsigned height; + unsigned refresh_rate; + } expected_detailed_timings[] = { + { 0, 2560, 1440, 75 }, + { 0, 2560, 1440, 60 }, + { 1, 1280, 720, 60 }, + { 1, 1280, 720, 50 }, + { 1, 2560, 1440, 60 } + }; + static constexpr size_t expected_detailed_timings_count = sizeof(expected_detailed_timings) / sizeof(expected_detailed_timings[0]); + size_t detailed_timings_found = 0; + auto result = edid.for_each_detailed_timing([&](auto& detailed_timing, unsigned block_id) { + EXPECT(detailed_timings_found < expected_detailed_timings_count); + auto& expected_timings = expected_detailed_timings[detailed_timings_found]; + EXPECT(block_id == expected_timings.block_id); + EXPECT(detailed_timing.horizontal_addressable_pixels() == expected_timings.width); + EXPECT(detailed_timing.vertical_addressable_lines() == expected_timings.height); + EXPECT(detailed_timing.refresh_rate().lround() == expected_timings.refresh_rate); + detailed_timings_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(detailed_timings_found == expected_detailed_timings_count); + } + + { + static constexpr u8 expected_vic_ids[] = { 18, 19, 4, 31, 16, 20, 5, 1, 17, 2, 3, 74 }; + static constexpr size_t expected_vic_ids_count = sizeof(expected_vic_ids) / sizeof(expected_vic_ids[0]); + size_t vic_ids_found = 0; + auto result = edid.for_each_short_video_descriptor([&](unsigned block_id, bool is_native, EDID::VIC::Details const& vic) { + EXPECT(vic_ids_found < expected_vic_ids_count); + EXPECT(block_id == 1); + EXPECT(is_native == (vic_ids_found == 4)); // the 5th value is marked native + EXPECT(vic.vic_id == expected_vic_ids[vic_ids_found]); + vic_ids_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(vic_ids_found == expected_vic_ids_count); + } + + { + // This edid has one CEA861 extension block only + size_t extension_blocks_found = 0; + auto result = edid.for_each_extension_block([&](unsigned block_id, u8 tag, u8 revision, ReadonlyBytes) { + EXPECT(block_id == 1); + EXPECT(tag == 0x2); + EXPECT(revision == 3); + extension_blocks_found++; + return IterationDecision::Continue; + }); + EXPECT(!result.is_error()); + EXPECT(result.value() == IterationDecision::Continue); + EXPECT(extension_blocks_found == 1); + } +} + +TEST_CASE(dmt_find_std_id) +{ + auto* dmt = EDID::DMT::find_timing_by_std_id(0xd1, 0xf); + EXPECT(dmt); + EXPECT(dmt->dmt_id == 0x46); + EXPECT(dmt->horizontal_pixels == 1920 && dmt->vertical_lines == 1200); +} + +TEST_CASE(dmt_frequency) +{ + auto* dmt = EDID::DMT::find_timing_by_dmt_id(0x4); + EXPECT(dmt); + static constexpr FixedPoint<16, u32> expected_vertical_frequency(59.940); + EXPECT(dmt->vertical_frequency_hz() == expected_vertical_frequency); + static constexpr FixedPoint<16, u32> expected_horizontal_frequency(31.469); + EXPECT(dmt->horizontal_frequency_khz() == expected_horizontal_frequency); +} + +TEST_CASE(vic) +{ + EXPECT(!EDID::VIC::find_details_by_vic_id(0)); // invalid + EXPECT(!EDID::VIC::find_details_by_vic_id(160)); // forbidden range + EXPECT(!EDID::VIC::find_details_by_vic_id(250)); // reserved + auto* vic_def_32 = EDID::VIC::find_details_by_vic_id(32); + EXPECT(vic_def_32); + EXPECT(vic_def_32->vic_id == 32); + auto* vic_def_200 = EDID::VIC::find_details_by_vic_id(200); + EXPECT(vic_def_200); + EXPECT(vic_def_200->vic_id == 200); + + for (unsigned vic_id = 0; vic_id <= 0xff; vic_id++) { + auto* vic_def = EDID::VIC::find_details_by_vic_id((u8)vic_id); + if (vic_def) { + EXPECT((vic_id >= 1 && vic_id <= 127) || (vic_id >= 193 && vic_id <= 219)); + EXPECT(vic_def->vic_id == vic_id); + } else { + EXPECT(vic_id == 0 || (vic_id >= 128 && vic_id <= 192) || (vic_id >= 220)); + } + } +} diff --git a/Userland/Libraries/CMakeLists.txt b/Userland/Libraries/CMakeLists.txt index 6c36935270..3c1faf0986 100644 --- a/Userland/Libraries/CMakeLists.txt +++ b/Userland/Libraries/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory(LibDeviceTree) add_subdirectory(LibDiff) add_subdirectory(LibDl) add_subdirectory(LibDSP) +add_subdirectory(LibEDID) add_subdirectory(LibELF) add_subdirectory(LibFileSystemAccessClient) add_subdirectory(LibGemini) diff --git a/Userland/Libraries/LibEDID/CMakeLists.txt b/Userland/Libraries/LibEDID/CMakeLists.txt new file mode 100644 index 0000000000..a3f981385f --- /dev/null +++ b/Userland/Libraries/LibEDID/CMakeLists.txt @@ -0,0 +1,8 @@ +set(SOURCES + DMT.cpp + EDID.cpp + VIC.cpp +) + +serenity_lib(LibEDID edid) +target_link_libraries(LibEDID LibC) diff --git a/Userland/Libraries/LibEDID/DMT.cpp b/Userland/Libraries/LibEDID/DMT.cpp new file mode 100644 index 0000000000..6884420ef7 --- /dev/null +++ b/Userland/Libraries/LibEDID/DMT.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace EDID { + +// Monitor timings as per Display Monitor Timing Standard (DMT) 1.0 rev 13 +static constexpr DMT::MonitorTiming s_monitor_timings[] = { + { 0x1, {}, 8, 640, 350, 37861, 85080, 31500, 32, 32, 192, 95, 64, 3 }, + { 0x2, { 0x31, 0x19 }, 8, 640, 400, 37861, 85080, 31500, 32, 1, 192, 45, 64, 3 }, + { 0x3, {}, 9, 720, 400, 37927, 85039, 35500, 36, 1, 216, 46, 72, 3 }, + { 0x4, { 0x31, 0x40 }, 8, 640, 480, 31469, 59940, 25175, 8, 2, 144, 29, 96, 2 }, + { 0x5, { 0x31, 0x4c }, 8, 640, 480, 37861, 72809, 31500, 16, 1, 176, 24, 40, 3 }, + { 0x6, { 0x31, 0x4f }, 8, 640, 480, 37500, 75000, 31500, 16, 1, 200, 20, 64, 3 }, + { 0x7, { 0x31, 0x59 }, 8, 640, 480, 43269, 85008, 36000, 56, 1, 192, 29, 56, 3 }, + { 0x8, {}, 8, 800, 600, 35156, 56250, 36000, 24, 1, 224, 25, 72, 2 }, + { 0x9, { 0x45, 0x40 }, 8, 800, 600, 37879, 60317, 40000, 40, 1, 256, 28, 128, 4 }, + { 0xa, { 0x45, 0x4c }, 8, 800, 600, 48077, 72188, 50000, 56, 37, 240, 66, 120, 6 }, + { 0xb, { 0x45, 0x4f }, 8, 800, 600, 46875, 75000, 49500, 16, 1, 256, 25, 80, 3 }, + { 0xc, { 0x45, 0x59 }, 8, 800, 600, 53674, 85061, 56250, 32, 1, 248, 31, 64, 3 }, + { 0xd, {}, 8, 800, 600, 76302, 119972, 73250, 48, 3, 160, 36, 32, 4, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0xe, {}, 8, 848, 480, 31020, 60000, 33750, 16, 6, 240, 37, 112, 8 }, + { 0xf, {}, 8, 1024, 768, 35522, 86957, 44900, 8, 0, 240, 24, 176, 4, DMT::MonitorTiming::CVTCompliance::NotCompliant, {}, DMT::MonitorTiming::ScanType::Interlaced }, + { 0x10, { 0x61, 0x40 }, 8, 1024, 768, 48363, 60004, 65000, 24, 3, 320, 38, 136, 6 }, + { 0x11, { 0x61, 0x4a }, 8, 1024, 768, 56476, 70069, 75000, 24, 3, 304, 38, 136, 6 }, + { 0x12, { 0x61, 0x4f }, 8, 1024, 768, 60023, 75029, 78750, 16, 1, 288, 32, 96, 3 }, + { 0x13, { 0x61, 0x59 }, 8, 1024, 768, 68677, 84997, 94500, 48, 1, 352, 40, 96, 3 }, + { 0x14, {}, 8, 1024, 768, 97551, 119989, 115500, 48, 3, 160, 45, 32, 4, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x15, { 0x71, 0x4f }, 8, 1152, 864, 67500, 75000, 108000, 64, 1, 448, 36, 128, 3 }, + { 0x55, { 0x81, 0xc0 }, 1, 1280, 720, 45000, 60000, 74250, 110, 5, 370, 30, 40, 5 }, + { 0x16, {}, 8, 1280, 768, 47396, 59995, 68250, 48, 3, 160, 22, 32, 7, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking, { 0x7f, 0x1c, 0x22 } }, + { 0x17, {}, 8, 1280, 768, 47776, 59870, 79500, 64, 3, 384, 30, 128, 7, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x7f, 0x1c, 0x28 } }, + { 0x18, {}, 8, 1280, 768, 60289, 74893, 102250, 80, 3, 416, 37, 128, 7, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x7f, 0x1c, 0x44 } }, + { 0x19, {}, 8, 1280, 768, 68633, 84837, 117500, 80, 3, 432, 41, 136, 7, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x7f, 0x1c, 0x62 } }, + { 0x1a, {}, 8, 1280, 768, 97396, 119798, 140250, 48, 3, 160, 45, 32, 7, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x1b, {}, 8, 1280, 800, 49306, 59910, 71000, 48, 3, 160, 23, 32, 6, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking, { 0x8f, 0x18, 0x21 } }, + { 0x1c, { 0x81, 0x0 }, 8, 1280, 800, 49702, 59810, 83500, 72, 3, 400, 31, 128, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x8f, 0x18, 0x28 } }, + { 0x1d, { 0x81, 0xf }, 8, 1280, 800, 62795, 74934, 106500, 80, 3, 416, 38, 128, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x8f, 0x18, 0x44 } }, + { 0x1e, { 0x81, 0x19 }, 8, 1280, 800, 71554, 84880, 122500, 80, 3, 432, 43, 136, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x8f, 0x18, 0x62 } }, + { 0x1f, {}, 8, 1280, 800, 101563, 119909, 146250, 48, 3, 160, 47, 32, 6, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x20, { 0x81, 0x40 }, 8, 1280, 960, 60000, 60000, 108000, 96, 1, 520, 40, 112, 3 }, + { 0x21, { 0x81, 0x59 }, 8, 1280, 960, 85938, 85002, 148500, 64, 1, 448, 3, 160, 3 }, + { 0x22, {}, 8, 1280, 960, 121875, 119838, 175500, 48, 3, 160, 57, 32, 4, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x23, { 0x81, 0x80 }, 8, 1280, 1024, 63981, 60020, 108000, 48, 1, 408, 42, 112, 3 }, + { 0x24, { 0x81, 0x8f }, 8, 1280, 1024, 79976, 75025, 135000, 16, 1, 408, 42, 144, 3 }, + { 0x25, { 0x81, 0x99 }, 8, 1280, 1024, 91146, 85024, 157500, 64, 1, 448, 48, 160, 3 }, + { 0x26, {}, 8, 1280, 1024, 130035, 119958, 187250, 48, 3, 160, 60, 32, 7, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x27, {}, 8, 1360, 768, 47712, 60015, 85500, 64, 3, 432, 27, 112, 6 }, + { 0x28, {}, 8, 1360, 768, 97533, 119967, 148250, 48, 5, 160, 45, 32, 5, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x51, {}, 1, 1366, 768, 57712, 59790, 85500, 70, 3, 426, 30, 143, 3 }, + { 0x56, {}, 1, 1366, 768, 48000, 60000, 72000, 14, 1, 134, 32, 56, 3 }, + { 0x29, {}, 8, 1400, 1050, 64744, 59948, 101000, 48, 3, 160, 30, 32, 4, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking, { 0x0c, 0x20, 0x21 } }, + { 0x2a, { 0x90, 0x40 }, 8, 1400, 1050, 65317, 59978, 121750, 88, 3, 464, 39, 144, 4, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x0c, 0x20, 0x28 } }, + { 0x2b, { 0x90, 0x4f }, 8, 1400, 1050, 82278, 74867, 156000, 104, 3, 496, 49, 144, 4, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x0c, 0x20, 0x44 } }, + { 0x2c, { 0x90, 0x59 }, 8, 1400, 1050, 93881, 84960, 179500, 104, 3, 512, 55, 152, 4, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x0c, 0x20, 0x62 } }, + { 0x2d, {}, 8, 1400, 1050, 133333, 119904, 208000, 48, 3, 160, 62, 32, 4, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x2e, {}, 8, 1440, 900, 55469, 59901, 88750, 48, 3, 160, 26, 32, 6, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking, { 0xc1, 0x18, 0x21 } }, + { 0x2f, { 0x95, 0x0 }, 8, 1440, 900, 55935, 59887, 106500, 80, 3, 464, 34, 152, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0xc1, 0x18, 0x28 } }, + { 0x30, { 0x95, 0xf }, 8, 1440, 900, 70635, 74984, 136750, 96, 3, 496, 42, 152, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0xc1, 0x18, 0x44 } }, + { 0x31, { 0x95, 0x19 }, 8, 1440, 900, 80430, 84842, 157000, 104, 3, 512, 48, 152, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0xc1, 0x18, 0x68 } }, + { 0x32, {}, 8, 1440, 900, 114219, 119852, 182750, 48, 3, 160, 53, 32, 6, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x53, { 0xa9, 0xc0 }, 8, 1600, 900, 60000, 60000, 108000, 24, 1, 200, 100, 80, 3 }, + { 0x33, { 0xa9, 0x40 }, 8, 1600, 1200, 75000, 60000, 162000, 64, 1, 560, 50, 192, 3 }, + { 0x34, { 0xa9, 0x45 }, 8, 1600, 1200, 81250, 65000, 175500, 64, 1, 560, 50, 192, 3 }, + { 0x35, { 0xa9, 0x4a }, 8, 1600, 1200, 87500, 70000, 189000, 64, 1, 560, 50, 192, 3 }, + { 0x36, { 0xa9, 0x4f }, 8, 1600, 1200, 93750, 75000, 202500, 64, 1, 560, 50, 192, 3 }, + { 0x37, { 0xa9, 0x59 }, 8, 1600, 1200, 106250, 85000, 229500, 64, 1, 560, 50, 192, 3 }, + { 0x38, {}, 8, 1600, 1200, 152415, 119917, 268250, 48, 3, 160, 71, 32, 4, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x39, {}, 8, 1680, 1050, 64674, 59883, 119000, 48, 3, 160, 30, 32, 6, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking, { 0x0c, 0x28, 0x21 } }, + { 0x3a, { 0xb3, 0x0 }, 8, 1680, 1050, 65290, 59954, 146250, 104, 3, 560, 39, 176, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x0c, 0x28, 0x28 } }, + { 0x3b, { 0xb3, 0xf }, 8, 1680, 1050, 82306, 74892, 187000, 120, 3, 592, 49, 176, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x0c, 0x28, 0x44 } }, + { 0x3c, { 0xb3, 0x19 }, 8, 1680, 1050, 93859, 84941, 214750, 128, 3, 608, 55, 176, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x0c, 0x28, 0x68 } }, + { 0x3d, {}, 8, 1680, 1050, 133424, 119986, 245500, 48, 3, 160, 62, 32, 6, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x3e, { 0xc1, 0x40 }, 8, 1792, 1344, 83640, 60000, 204750, 128, 1, 656, 50, 200, 3 }, + { 0x3f, { 0xc1, 0x4f }, 8, 1792, 1344, 106270, 74997, 261000, 96, 1, 664, 73, 216, 3 }, + { 0x40, {}, 8, 1792, 1344, 170722, 119974, 333250, 48, 3, 160, 79, 32, 4, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x41, { 0xc9, 0x40 }, 8, 1856, 1392, 86333, 59995, 218250, 96, 1, 672, 47, 224, 3 }, + { 0x42, { 0xc9, 0x4f }, 8, 1856, 1392, 112500, 75000, 288000, 128, 1, 704, 108, 224, 3 }, + { 0x43, {}, 8, 1856, 1392, 176835, 119970, 356500, 48, 3, 160, 82, 32, 4, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x52, { 0xd1, 0xc0 }, 4, 1920, 1080, 67500, 60000, 148500, 88, 4, 280, 45, 44, 5 }, + { 0x44, {}, 8, 1920, 1200, 74038, 59950, 154000, 48, 3, 160, 35, 32, 6, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking, { 0x57, 0x28, 0x21 } }, + { 0x45, { 0xd1, 0x0 }, 8, 1920, 1200, 74556, 59885, 193250, 136, 3, 672, 45, 200, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x57, 0x28, 0x28 } }, + { 0x46, { 0xd1, 0xf }, 8, 1920, 1200, 94038, 74930, 245250, 136, 3, 688, 55, 208, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x57, 0x28, 0x44 } }, + { 0x47, { 0xd1, 0x19 }, 8, 1920, 1200, 107184, 84932, 281250, 144, 3, 704, 62, 208, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x57, 0x28, 0x62 } }, + { 0x48, {}, 8, 1920, 1200, 152404, 119909, 317000, 48, 3, 160, 71, 32, 6, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x49, { 0xd1, 0x40 }, 8, 1920, 1440, 90000, 60000, 234000, 128, 1, 680, 60, 208, 3 }, + { 0x4a, { 0xd1, 0x4f }, 8, 1920, 1440, 112500, 75000, 297000, 144, 1, 720, 60, 224, 3 }, + { 0x4b, {}, 8, 1920, 1440, 182933, 119956, 380500, 48, 3, 160, 85, 32, 4, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x54, { 0xe1, 0xc0 }, 1, 2048, 1152, 72000, 60000, 162000, 26, 1, 202, 48, 80, 3 }, + { 0x4c, {}, 8, 2560, 1600, 98713, 59972, 268500, 48, 3, 160, 46, 32, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x1f, 0x38, 0x21 } }, + { 0x4d, {}, 8, 2650, 1600, 99458, 59987, 348500, 192, 3, 944, 58, 280, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x1f, 0x38, 0x28 } }, + { 0x4e, {}, 8, 2560, 1600, 125354, 74972, 443250, 208, 3, 976, 72, 280, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x1f, 0x38, 0x44 } }, + { 0x4f, {}, 8, 2560, 1600, 142887, 84951, 505250, 208, 3, 976, 82, 280, 6, DMT::MonitorTiming::CVTCompliance::Compliant, { 0x1f, 0x38, 0x62 } }, + { 0x50, {}, 8, 2560, 1600, 203217, 119963, 552750, 48, 3, 160, 94, 32, 6, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlanking }, + { 0x57, {}, 1, 4096, 2160, 133320, 60000, 556744, 8, 48, 80, 62, 32, 8, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlankingV2 }, + { 0x58, {}, 1, 4096, 2160, 133187, 59940, 556188, 8, 48, 80, 62, 32, 8, DMT::MonitorTiming::CVTCompliance::CompliantReducedBlankingV2 }, +}; + +FixedPoint<16, u32> DMT::MonitorTiming::horizontal_frequency_khz() const +{ + return FixedPoint<16, u32>(horizontal_frequency_hz) / 1000; +} + +FixedPoint<16, u32> DMT::MonitorTiming::vertical_frequency_hz() const +{ + return FixedPoint<16, u32>(vertical_frequency_millihz) / 1000; +} + +u32 DMT::MonitorTiming::refresh_rate_hz() const +{ + return vertical_frequency_hz().ltrunk(); +} + +String DMT::MonitorTiming::name() const +{ + if (scan_type == ScanType::Interlaced) + return String::formatted("{} x {} @ {}Hz (Interlaced)", horizontal_pixels, vertical_lines, refresh_rate_hz()); + return String::formatted("{} x {} @ {}Hz", horizontal_pixels, vertical_lines, refresh_rate_hz()); +} + +auto DMT::find_timing_by_dmt_id(u8 dmt_id) -> MonitorTiming const* +{ + if (dmt_id == 0) + return nullptr; + + for (auto& monitor_timing : s_monitor_timings) { + if (monitor_timing.dmt_id == dmt_id) + return &monitor_timing; + } + + return nullptr; +} + +auto DMT::find_timing_by_std_id(u8 std_id_byte1, u8 std_id_byte2) -> MonitorTiming const* +{ + for (auto& monitor_timing : s_monitor_timings) { + if (!monitor_timing.has_std()) + continue; + if (monitor_timing.std_bytes[0] == std_id_byte1 && monitor_timing.std_bytes[1] == std_id_byte2) + return &monitor_timing; + } + + return nullptr; +} + +auto DMT::find_timing_by_cvt(CVT cvt) -> MonitorTiming const* +{ + for (auto& monitor_timing : s_monitor_timings) { + if (!monitor_timing.has_cvt()) + continue; + if (monitor_timing.cvt_bytes[0] == cvt.bytes[0] && monitor_timing.cvt_bytes[1] == cvt.bytes[1] && monitor_timing.cvt_bytes[2] == cvt.bytes[2]) + return &monitor_timing; + } + + return nullptr; +} + +} diff --git a/Userland/Libraries/LibEDID/DMT.h b/Userland/Libraries/LibEDID/DMT.h new file mode 100644 index 0000000000..2774b09315 --- /dev/null +++ b/Userland/Libraries/LibEDID/DMT.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace EDID { + +class DMT final { +public: + struct CVT { + u8 bytes[3]; + }; + struct MonitorTiming { + enum class ScanType : u8 { + NonInterlaced, + Interlaced + }; + enum class CVTCompliance : u8 { + NotCompliant, + Compliant, + CompliantReducedBlanking, + CompliantReducedBlankingV2, + }; + + u8 dmt_id; + u8 std_bytes[2]; + u8 char_width_pixels; + u16 horizontal_pixels; + u16 vertical_lines; + u32 horizontal_frequency_hz; + u32 vertical_frequency_millihz; + u32 pixel_clock_khz; + u8 horizontal_front_porch_pixels; + u8 vertical_front_porch_lines; + u16 horizontal_blank_pixels; + u16 vertical_blank_lines; + u16 horizontal_sync_time_pixels; + u8 vertical_sync_time_lines; + CVTCompliance cvt_compliance { CVTCompliance::NotCompliant }; + u8 cvt_bytes[3] {}; + ScanType scan_type { ScanType::NonInterlaced }; + + ALWAYS_INLINE bool has_std() const { return std_bytes[0] != 0; } + ALWAYS_INLINE bool has_cvt() const { return cvt_bytes[0] != 0; } + + FixedPoint<16, u32> horizontal_frequency_khz() const; + FixedPoint<16, u32> vertical_frequency_hz() const; + u32 refresh_rate_hz() const; + String name() const; + }; + + static MonitorTiming const* find_timing_by_dmt_id(u8); + static MonitorTiming const* find_timing_by_std_id(u8, u8); + static MonitorTiming const* find_timing_by_cvt(CVT); +}; + +} diff --git a/Userland/Libraries/LibEDID/EDID.cpp b/Userland/Libraries/LibEDID/EDID.cpp new file mode 100644 index 0000000000..c4a36be73a --- /dev/null +++ b/Userland/Libraries/LibEDID/EDID.cpp @@ -0,0 +1,1190 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace EDID { + +// clang doesn't like passing around pointers to members in packed structures, +// even though we're only using them for arithmetic purposes +#ifdef __clang__ +# pragma clang diagnostic ignored "-Waddress-of-packed-member" +#endif + +namespace Definitions { + +struct [[gnu::packed]] StandardTimings { + u8 horizontal_8_pixels; + u8 ratio_and_refresh_rate; +}; + +struct [[gnu::packed]] DetailedTiming { + u16 pixel_clock; + u8 horizontal_addressable_pixels_low; + u8 horizontal_blanking_pixels_low; + u8 horizontal_addressable_and_blanking_pixels_high; + u8 vertical_addressable_lines_low; + u8 vertical_blanking_lines_low; + u8 vertical_addressable_and_blanking_lines_high; + u8 horizontal_front_porch_pixels_low; + u8 horizontal_sync_pulse_width_pixels_low; + u8 vertical_front_porch_and_sync_pulse_width_lines_low; + u8 horizontal_and_vertical_front_porch_sync_pulse_width_high; + u8 horizontal_addressable_image_size_mm_low; + u8 vertical_addressable_image_size_mm_low; + u8 horizontal_vertical_addressable_image_size_mm_high; + u8 right_or_left_horizontal_border_pixels; + u8 top_or_bottom_vertical_border_lines; + u8 features; +}; + +enum class DisplayDescriptorTag : u8 { + ManufacturerSpecified_First = 0x0, + ManufacturerSpecified_Last = 0xf, + Dummy = 0x10, + EstablishedTimings3 = 0xf7, + CVTTimingCodes = 0xf8, + DisplayColorManagementData = 0xf9, + StandardTimingIdentifications = 0xfa, + ColorPointData = 0xfb, + DisplayProductName = 0xfc, + DisplayRangeLimits = 0xfd, + AlphanumericDataString = 0xfe, + DisplayProductSerialNumber = 0xff +}; + +struct [[gnu::packed]] DisplayDescriptor { + u16 zero; + u8 reserved1; + u8 tag; + u8 reserved2; + union { + struct [[gnu::packed]] { + u8 ascii_name[13]; + } display_product_name; + struct [[gnu::packed]] { + u8 ascii_str[13]; + } display_product_serial_number; + struct [[gnu::packed]] { + u8 revision; + u8 dmt_bits[6]; + u8 reserved[6]; + } established_timings3; + struct [[gnu::packed]] { + u8 version; + u8 cvt[4][3]; + } coordinated_video_timings; + }; +}; + +static_assert(sizeof(DetailedTiming) == sizeof(DisplayDescriptor)); + +struct [[gnu::packed]] EDID { + u64 header; + struct [[gnu::packed]] { + u16 manufacturer_id; + u16 product_code; + u32 serial_number; + u8 week_of_manufacture; + u8 year_of_manufacture; + } vendor; + struct [[gnu::packed]] { + u8 version; + u8 revision; + } version; + struct [[gnu::packed]] { + u8 video_input_definition; + u8 horizontal_size_or_aspect_ratio; + u8 vertical_size_or_aspect_ratio; + u8 display_transfer_characteristics; + u8 feature_support; + } basic_display_parameters; + struct [[gnu::packed]] { + u8 red_green_low_order_bits; + u8 blue_white_low_order_bits; + u8 red_x_high_order_bits; + u8 red_y_high_order_bits; + u8 green_x_high_order_bits; + u8 green_y_high_order_bits; + u8 blue_x_high_order_bits; + u8 blue_y_high_order_bits; + u8 white_x_high_order_bits; + u8 white_y_high_order_bits; + } color_characteristics; + struct [[gnu::packed]] { + u8 timings_1; + u8 timings_2; + u8 manufacturer_reserved; + } established_timings; + StandardTimings standard_timings[8]; + union { + DetailedTiming detailed_timing; + DisplayDescriptor display_descriptor; + } detailed_timing_or_display_descriptors[4]; + u8 extension_block_count; + u8 checksum; +}; + +enum ExtensionBlockTag : u8 { + CEA_861 = 0x2, + VideoTimingBlock = 0x10, + DisplayInformation = 0x40, + LocalizedString = 0x50, + DigitalPacketVideoLink = 0x60, + ExtensionBlockMap = 0xf0, + ManufacturerDefined = 0xff +}; + +struct [[gnu::packed]] ExtensionBlock { + u8 tag; + union { + struct [[gnu::packed]] { + u8 block_tags[126]; + } map; + struct [[gnu::packed]] { + u8 revision; + u8 bytes[125]; + } block; + struct [[gnu::packed]] { + u8 revision; + u8 dtd_start_offset; + u8 flags; + union { + u8 bytes[123]; + }; + } cea861extension; + }; + u8 checksum; +}; + +} + +static_assert(sizeof(Definitions::EDID) == Parser::BufferSize); +static_assert(sizeof(Definitions::ExtensionBlock) == 128); + +class CEA861ExtensionBlock final { + friend class Parser; + +public: + enum class DataBlockTag : u8 { + Reserved = 0, + Audio, + Video, + VendorSpecific, + SpeakerAllocation, + VesaDTC, + Reserved2, + Extended + }; + + ErrorOr for_each_short_video_descriptor(Function callback) const + { + return for_each_data_block([&](DataBlockTag tag, ReadonlyBytes bytes) -> ErrorOr { + if (tag != DataBlockTag::Video) + return IterationDecision::Continue; + + // Short video descriptors are one byte values + for (size_t i = 0; i < bytes.size(); i++) { + u8 byte = m_edid.read_host(&bytes[i]); + bool is_native = (byte & 0x80) != 0; + u8 vic_id = byte & 0x7f; + + auto* vic_details = VIC::find_details_by_vic_id(vic_id); + if (!vic_details) + return Error::from_string_literal("CEA 861 extension block has invalid short video descriptor"sv); + + IterationDecision decision = callback(is_native, *vic_details); + if (decision != IterationDecision::Continue) + return decision; + } + return IterationDecision::Continue; + }); + } + + ErrorOr for_each_dtd(Function callback) const + { + u8 dtd_start = m_edid.read_host(&m_block->cea861extension.dtd_start_offset); + if (dtd_start <= 4) + return IterationDecision::Continue; + + if (dtd_start > offsetof(Definitions::ExtensionBlock, checksum) - sizeof(Definitions::DetailedTiming)) + return Error::from_string_literal("CEA 861 extension block has invalid DTD list"sv); + + size_t dtd_index = 0; + for (size_t offset = dtd_start; offset <= offsetof(Definitions::ExtensionBlock, checksum) - sizeof(Definitions::DetailedTiming); offset += sizeof(Definitions::DetailedTiming)) { + auto& dtd = *(Definitions::DetailedTiming const*)((u8 const*)m_block + offset); + if (m_edid.read_host(&dtd.pixel_clock) == 0) + break; + + IterationDecision decision = callback(Parser::DetailedTiming(m_edid, &dtd)); + if (decision != IterationDecision::Continue) + return decision; + + dtd_index++; + } + return IterationDecision::Continue; + } + +private: + CEA861ExtensionBlock(Parser const& edid, Definitions::ExtensionBlock const* block) + : m_edid(edid) + , m_block(block) + { + } + + ErrorOr for_each_data_block(Function(DataBlockTag, ReadonlyBytes)> callback) const + { + u8 dtd_start = m_edid.read_host(&m_block->cea861extension.dtd_start_offset); + if (dtd_start <= 4) + return IterationDecision::Continue; + + if (dtd_start > offsetof(Definitions::ExtensionBlock, checksum)) + return Error::from_string_literal("CEA 861 extension block has invalid DTD start offset"sv); + + auto* data_block_header = &m_block->cea861extension.bytes[0]; + auto* data_block_end = (u8 const*)m_block + dtd_start; + while (data_block_header < data_block_end) { + auto header_byte = m_edid.read_host(data_block_header); + size_t payload_size = header_byte & 0x1f; + auto tag = (DataBlockTag)((header_byte >> 5) & 0x7); + if (tag == DataBlockTag::Extended && payload_size == 0) + return Error::from_string_literal("CEA 861 extension block has invalid extended data block size"sv); + + auto decision = TRY(callback(tag, m_edid.m_bytes.slice(data_block_header - m_edid.m_bytes.data() + 1, payload_size))); + if (decision != IterationDecision::Continue) + return decision; + + data_block_header += 1 + payload_size; + } + return IterationDecision::Continue; + } + + ErrorOr for_each_display_descriptor(Function callback) const + { + u8 dtd_start = m_edid.read_host(&m_block->cea861extension.dtd_start_offset); + if (dtd_start <= 4) + return IterationDecision::Continue; + + if (dtd_start > offsetof(Definitions::ExtensionBlock, checksum) - sizeof(Definitions::DetailedTiming)) + return Error::from_string_literal("CEA 861 extension block has invalid DTD list"sv); + + for (size_t offset = dtd_start; offset <= offsetof(Definitions::ExtensionBlock, checksum) - sizeof(Definitions::DisplayDescriptor); offset += sizeof(Definitions::DisplayDescriptor)) { + auto& dd = *(Definitions::DisplayDescriptor const*)((u8 const*)m_block + offset); + if (m_edid.read_host(&dd.zero) != 0 || m_edid.read_host(&dd.reserved1) != 0) + continue; + + u8 tag = m_edid.read_host(&dd.tag); + IterationDecision decision = callback(tag, dd); + if (decision != IterationDecision::Continue) + return decision; + } + return IterationDecision::Continue; + } + + Parser const& m_edid; + Definitions::ExtensionBlock const* m_block; +}; + +template +T Parser::read_host(T const* field) const +{ + VERIFY((u8 const*)field >= m_bytes.data() && (u8 const*)field + sizeof(T) <= m_bytes.data() + m_bytes.size()); + size_t offset = (u8 const*)field - m_bytes.data(); + T value; + if constexpr (sizeof(T) > 1) + ByteReader::load(m_bytes.offset(offset), value); + else + value = m_bytes.at(offset); + + return value; +} + +template +requires(IsIntegral && sizeof(T) > 1) T Parser::read_le(T const* field) + const +{ + static_assert(sizeof(T) > 1); + return AK::convert_between_host_and_little_endian(read_host(field)); +} + +template +requires(IsIntegral && sizeof(T) > 1) T Parser::read_be(T const* field) + const +{ + static_assert(sizeof(T) > 1); + return AK::convert_between_host_and_big_endian(read_host(field)); +} + +ErrorOr Parser::from_bytes(ReadonlyBytes bytes) +{ + Parser edid(bytes); + if (auto parse_result = edid.parse(); parse_result.is_error()) + return parse_result.error(); + return edid; +} + +ErrorOr Parser::from_bytes(ByteBuffer&& bytes) +{ + Parser edid(move(bytes)); + if (auto parse_result = edid.parse(); parse_result.is_error()) + return parse_result.error(); + return edid; +} + +Parser::Parser(ReadonlyBytes bytes) + : m_bytes(move(bytes)) +{ +} + +Parser::Parser(ByteBuffer&& bytes) + : m_bytes_buffer(move(bytes)) + , m_bytes(m_bytes_buffer) +{ +} + +Parser::Parser(Parser const& other) + : m_bytes_buffer(other.m_bytes_buffer) + , m_revision(other.m_revision) +{ + if (m_bytes_buffer.is_empty()) + m_bytes = other.m_bytes_buffer; // We don't own the buffer + else + m_bytes = m_bytes_buffer; // We own the buffer +} + +Parser& Parser::operator=(Parser&& from) +{ + m_bytes_buffer = move(from.m_bytes_buffer); + m_bytes = move(from.m_bytes); + m_revision = from.m_revision; + return *this; +} + +Parser& Parser::operator=(Parser const& other) +{ + if (this == &other) + return *this; + + m_bytes_buffer = other.m_bytes_buffer; + if (m_bytes_buffer.is_empty()) + m_bytes = other.m_bytes_buffer; // We don't own the buffer + else + m_bytes = m_bytes_buffer; // We own the buffer + m_revision = other.m_revision; + return *this; +} + +bool Parser::operator==(Parser const& other) const +{ + if (this == &other) + return true; + return m_bytes == other.m_bytes; +} + +Definitions::EDID const& Parser::raw_edid() const +{ + return *(Definitions::EDID const*)m_bytes.data(); +} + +ErrorOr Parser::parse() +{ + if (m_bytes.size() < sizeof(Definitions::EDID)) + return Error::from_string_literal("Incomplete Parser structure"sv); + + auto const& edid = raw_edid(); + u64 header = read_le(&edid.header); + if (header != 0x00ffffffffffff00ull) + return Error::from_string_literal("No Parser header"sv); + + u8 major_version = read_host(&edid.version.version); + m_revision = read_host(&edid.version.revision); + if (major_version != 1 || m_revision > 4) + return Error::from_string_literal("Unsupported Parser version"sv); + + u8 checksum = 0x0; + for (size_t i = 0; i < sizeof(Definitions::EDID); i++) + checksum += m_bytes[i]; + + if (checksum != 0) { + if (m_revision >= 4) { + return Error::from_string_literal("Parser checksum mismatch"sv); + } else { + dbgln("EDID checksum mismatch, data may be corrupted!"); + } + } + + return {}; +} + +ErrorOr Parser::for_each_extension_block(Function callback) const +{ + auto& edid = raw_edid(); + u8 raw_extension_block_count = read_host(&edid.extension_block_count); + if (raw_extension_block_count == 0) + return IterationDecision::Continue; + if (sizeof(Definitions::EDID) + (size_t)raw_extension_block_count * sizeof(Definitions::ExtensionBlock) > m_bytes.size()) + return Error::from_string_literal("Truncated EDID"); + + auto validate_block_checksum = [&](Definitions::ExtensionBlock const& extension_map) { + u8 checksum = 0x0; + auto* bytes = (u8 const*)&extension_map; + for (size_t i = 0; i < sizeof(extension_map); i++) + checksum += bytes[i]; + + return checksum == 0; + }; + + size_t offset = sizeof(Definitions::EDID); + auto* raw_extension_blocks = (Definitions::ExtensionBlock const*)(m_bytes.data() + offset); + Definitions::ExtensionBlock const* current_extension_map = nullptr; + if (m_revision <= 3) { + if (raw_extension_block_count > 1) { + current_extension_map = &raw_extension_blocks[0]; + if (read_host(¤t_extension_map->tag) != Definitions::ExtensionBlockTag::ExtensionBlockMap) + return Error::from_string_literal("Did not find extension map at block 1"sv); + if (!validate_block_checksum(*current_extension_map)) + return Error::from_string_literal("Extension block map checksum mismatch"sv); + } + } else if (read_host(&raw_extension_blocks[0].tag) == Definitions::ExtensionBlockTag::ExtensionBlockMap) { + current_extension_map = &raw_extension_blocks[0]; + } + + for (unsigned raw_index = 0; raw_index < raw_extension_block_count; raw_index++) { + auto& raw_block = raw_extension_blocks[raw_index]; + u8 tag = read_host(&raw_block.tag); + if (current_extension_map && raw_index == 127) { + if (tag != Definitions::ExtensionBlockTag::ExtensionBlockMap) + return Error::from_string_literal("Did not find extension map at block 128"sv); + current_extension_map = &raw_extension_blocks[127]; + if (!validate_block_checksum(*current_extension_map)) + return Error::from_string_literal("Extension block map checksum mismatch"sv); + continue; + } + + if (tag == Definitions::ExtensionBlockTag::ExtensionBlockMap) + return Error::from_string_literal("Unexpected extension map encountered"sv); + + if (!validate_block_checksum(raw_block)) + return Error::from_string_literal("Extension block checksum mismatch"sv); + + IterationDecision decision = callback(raw_index + 1, tag, raw_block.block.revision, m_bytes.slice(offset, sizeof(Definitions::ExtensionBlock))); + if (decision != IterationDecision::Continue) + return decision; + + offset += sizeof(Definitions::ExtensionBlock); + } + return IterationDecision::Continue; +} + +String Parser::version() const +{ + return String::formatted("1.{}", (int)m_revision); +} + +String Parser::legacy_manufacturer_id() const +{ + u16 packed_id = read_be(&raw_edid().vendor.manufacturer_id); + char id[4] = { + (char)((u16)'A' + ((packed_id >> 10) & 0x1f) - 1), + (char)((u16)'A' + ((packed_id >> 5) & 0x1f) - 1), + (char)((u16)'A' + (packed_id & 0x1f) - 1), + '\0' + }; + return id; +} + +u16 Parser::product_code() const +{ + return read_le(&raw_edid().vendor.product_code); +} + +u32 Parser::serial_number() const +{ + return read_le(&raw_edid().vendor.serial_number); +} + +auto Parser::digital_display() const -> Optional +{ + auto& edid = raw_edid(); + u8 video_input_definition = read_host(&edid.basic_display_parameters.video_input_definition); + if (!(video_input_definition & 0x80)) + return {}; // This is an analog display + + u8 feature_support = read_host(&edid.basic_display_parameters.feature_support); + return DigitalDisplay(video_input_definition, feature_support, m_revision); +} + +auto Parser::analog_display() const -> Optional +{ + auto& edid = raw_edid(); + u8 video_input_definition = read_host(&edid.basic_display_parameters.video_input_definition); + if ((video_input_definition & 0x80) != 0) + return {}; // This is a digital display + + u8 feature_support = read_host(&edid.basic_display_parameters.feature_support); + return AnalogDisplay(video_input_definition, feature_support, m_revision); +} + +auto Parser::screen_size() const -> Optional +{ + auto& edid = raw_edid(); + u8 horizontal_size_or_aspect_ratio = read_host(&edid.basic_display_parameters.horizontal_size_or_aspect_ratio); + u8 vertical_size_or_aspect_ratio = read_host(&edid.basic_display_parameters.vertical_size_or_aspect_ratio); + + if (horizontal_size_or_aspect_ratio == 0 || vertical_size_or_aspect_ratio == 0) { + // EDID < 1.4: Unknown or undefined + // EDID >= 1.4: If both are 0 it is unknown or undefined + // If one of them is 0 then we're dealing with aspect ratios + return {}; + } + + return ScreenSize(horizontal_size_or_aspect_ratio, vertical_size_or_aspect_ratio); +} + +auto Parser::aspect_ratio() const -> Optional +{ + if (m_revision < 4) + return {}; + + auto& edid = raw_edid(); + u8 value_1 = read_host(&edid.basic_display_parameters.horizontal_size_or_aspect_ratio); + u8 value_2 = read_host(&edid.basic_display_parameters.vertical_size_or_aspect_ratio); + + if (value_1 == 0 && value_2 == 0) + return {}; // Unknown or undefined + if (value_1 != 0 && value_2 != 0) + return {}; // Dimensions are in cm + + if (value_1 == 0) + return ScreenAspectRatio(ScreenAspectRatio::Orientation::Portrait, FixedPoint<16>(100) / FixedPoint<16>((i32)value_2 + 99)); + + VERIFY(value_2 == 0); + return ScreenAspectRatio(ScreenAspectRatio::Orientation::Landscape, FixedPoint<16>((i32)value_1 + 99) / 100); +} + +Optional> Parser::gamma() const +{ + u8 display_transfer_characteristics = read_host(&raw_edid().basic_display_parameters.display_transfer_characteristics); + if (display_transfer_characteristics == 0xff) { + if (m_revision < 4) + return {}; + + // TODO: EDID >= 1.4 stores more gamma details in an extension block (e.g. DI-EXT) + return {}; + } + + FixedPoint<16> gamma { (i32)display_transfer_characteristics + 100 }; + gamma /= 100; + return gamma; +} + +u32 Parser::DetailedTiming::pixel_clock_khz() const +{ + return (u32)m_edid.read_le(&m_detailed_timings.pixel_clock) * 10000; +} + +u16 Parser::DetailedTiming::horizontal_addressable_pixels() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.horizontal_addressable_pixels_low); + u8 high = m_edid.read_host(&m_detailed_timings.horizontal_addressable_and_blanking_pixels_high) >> 4; + return ((u16)high << 8) | (u16)low; +} + +u16 Parser::DetailedTiming::horizontal_blanking_pixels() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.horizontal_blanking_pixels_low); + u8 high = m_edid.read_host(&m_detailed_timings.horizontal_addressable_and_blanking_pixels_high) & 0xf; + return ((u16)high << 8) | (u16)low; +} + +u16 Parser::DetailedTiming::vertical_addressable_lines() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.vertical_addressable_lines_low); + u8 high = m_edid.read_host(&m_detailed_timings.vertical_addressable_and_blanking_lines_high) >> 4; + return ((u16)high << 8) | (u16)low; +} + +u16 Parser::DetailedTiming::vertical_blanking_lines() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.vertical_blanking_lines_low); + u8 high = m_edid.read_host(&m_detailed_timings.vertical_addressable_and_blanking_lines_high) & 0xf; + return ((u16)high << 8) | (u16)low; +} + +u16 Parser::DetailedTiming::horizontal_front_porch_pixels() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.horizontal_front_porch_pixels_low); + u8 high = m_edid.read_host(&m_detailed_timings.horizontal_and_vertical_front_porch_sync_pulse_width_high) >> 6; + return ((u16)high << 8) | (u16)low; +} + +u16 Parser::DetailedTiming::horizontal_sync_pulse_width_pixels() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.horizontal_sync_pulse_width_pixels_low); + u8 high = (m_edid.read_host(&m_detailed_timings.horizontal_and_vertical_front_porch_sync_pulse_width_high) >> 4) & 3; + return ((u16)high << 8) | (u16)low; +} + +u16 Parser::DetailedTiming::vertical_front_porch_lines() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.vertical_front_porch_and_sync_pulse_width_lines_low) >> 4; + u8 high = (m_edid.read_host(&m_detailed_timings.horizontal_and_vertical_front_porch_sync_pulse_width_high) >> 2) & 3; + return ((u16)high << 4) | (u16)low; +} + +u16 Parser::DetailedTiming::vertical_sync_pulse_width_lines() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.vertical_front_porch_and_sync_pulse_width_lines_low) & 0xf; + u8 high = m_edid.read_host(&m_detailed_timings.horizontal_and_vertical_front_porch_sync_pulse_width_high) & 3; + return ((u16)high << 4) | (u16)low; +} + +u16 Parser::DetailedTiming::horizontal_image_size_mm() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.horizontal_addressable_image_size_mm_low); + u8 high = m_edid.read_host(&m_detailed_timings.horizontal_vertical_addressable_image_size_mm_high) >> 4; + return ((u16)high << 8) | (u16)low; +} + +u16 Parser::DetailedTiming::vertical_image_size_mm() const +{ + u8 low = m_edid.read_host(&m_detailed_timings.vertical_addressable_image_size_mm_low); + u8 high = m_edid.read_host(&m_detailed_timings.horizontal_vertical_addressable_image_size_mm_high) & 0xf; + return ((u16)high << 8) | (u16)low; +} + +u8 Parser::DetailedTiming::horizontal_right_or_left_border_pixels() const +{ + return m_edid.read_host(&m_detailed_timings.right_or_left_horizontal_border_pixels); +} + +u8 Parser::DetailedTiming::vertical_top_or_bottom_border_lines() const +{ + return m_edid.read_host(&m_detailed_timings.top_or_bottom_vertical_border_lines); +} + +bool Parser::DetailedTiming::is_interlaced() const +{ + return (m_edid.read_host(&m_detailed_timings.features) & (1 << 7)) != 0; +} + +FixedPoint<16, u32> Parser::DetailedTiming::refresh_rate() const +{ + // Blanking = front porch + sync pulse width = back porch + u32 total_horizontal_pixels = (u32)horizontal_addressable_pixels() + (u32)horizontal_blanking_pixels(); + u32 total_vertical_lines = (u32)vertical_addressable_lines() + (u32)vertical_blanking_lines(); + u32 total_pixels = total_horizontal_pixels * total_vertical_lines; + if (total_pixels == 0) + return {}; + // Use a bigger fixed point representation due to the large numbers involved and then downcast + return FixedPoint<32, u64>(pixel_clock_khz()) / total_pixels; +} + +ErrorOr Parser::for_each_established_timing(Function callback) const +{ + static constexpr EstablishedTiming established_timing_byte1[8] = { + { EstablishedTiming::Source::VESA, 800, 600, 60, 0x9 }, + { EstablishedTiming::Source::VESA, 800, 600, 56, 0x8 }, + { EstablishedTiming::Source::VESA, 640, 480, 75, 0x6 }, + { EstablishedTiming::Source::VESA, 640, 480, 73, 0x5 }, + { EstablishedTiming::Source::Apple, 640, 480, 67 }, + { EstablishedTiming::Source::IBM, 640, 480, 60, 0x4 }, + { EstablishedTiming::Source::IBM, 720, 400, 88 }, + { EstablishedTiming::Source::IBM, 720, 400, 70 } + }; + static constexpr EstablishedTiming established_timing_byte2[8] = { + { EstablishedTiming::Source::VESA, 1280, 1024, 75, 0x24 }, + { EstablishedTiming::Source::VESA, 1024, 768, 75, 0x12 }, + { EstablishedTiming::Source::VESA, 1024, 768, 70, 0x11 }, + { EstablishedTiming::Source::VESA, 1024, 768, 60, 0x10 }, + { EstablishedTiming::Source::IBM, 1024, 768, 87, 0xf }, + { EstablishedTiming::Source::Apple, 832, 624, 75 }, + { EstablishedTiming::Source::VESA, 800, 600, 75, 0xb }, + { EstablishedTiming::Source::VESA, 800, 600, 72, 0xa } + }; + static constexpr EstablishedTiming established_timing_byte3[1] = { + { EstablishedTiming::Source::Apple, 1152, 870, 75 } + }; + + auto& established_timings = raw_edid().established_timings; + for (int i = 7; i >= 0; i--) { + if (!(established_timings.timings_1 & (1 << i))) + continue; + IterationDecision decision = callback(established_timing_byte1[i]); + if (decision != IterationDecision::Continue) + return decision; + } + for (int i = 7; i >= 0; i--) { + if (!(established_timings.timings_2 & (1 << i))) + continue; + IterationDecision decision = callback(established_timing_byte2[i]); + if (decision != IterationDecision::Continue) + return decision; + } + + if ((established_timings.manufacturer_reserved & (1 << 7)) != 0) { + IterationDecision decision = callback(established_timing_byte3[0]); + if (decision != IterationDecision::Continue) + return decision; + } + + u8 manufacturer_specific = established_timings.manufacturer_reserved & 0x7f; + if (manufacturer_specific != 0) { + IterationDecision decision = callback(EstablishedTiming(EstablishedTiming::Source::Manufacturer, 0, 0, manufacturer_specific)); + if (decision != IterationDecision::Continue) + return decision; + } + + auto callback_decision = IterationDecision::Continue; + auto result = for_each_display_descriptor([&](u8 descriptor_tag, auto& display_descriptor) { + if (descriptor_tag != (u8)Definitions::DisplayDescriptorTag::EstablishedTimings3) + return IterationDecision::Continue; + + static constexpr EstablishedTiming established_timings3_bytes[] = { + // Byte 1 + { EstablishedTiming::Source::VESA, 640, 350, 85, 0x1 }, + { EstablishedTiming::Source::VESA, 640, 400, 85, 0x2 }, + { EstablishedTiming::Source::VESA, 720, 400, 85, 0x3 }, + { EstablishedTiming::Source::VESA, 640, 480, 85, 0x7 }, + { EstablishedTiming::Source::VESA, 848, 480, 60, 0xe }, + { EstablishedTiming::Source::VESA, 800, 600, 85, 0xc }, + { EstablishedTiming::Source::VESA, 1024, 768, 85, 0x13 }, + { EstablishedTiming::Source::VESA, 1152, 864, 75, 0x15 }, + // Byte 2 + { EstablishedTiming::Source::VESA, 1280, 768, 60, 0x16 }, + { EstablishedTiming::Source::VESA, 1280, 768, 60, 0x17 }, + { EstablishedTiming::Source::VESA, 1280, 768, 75, 0x18 }, + { EstablishedTiming::Source::VESA, 1280, 768, 85, 0x19 }, + { EstablishedTiming::Source::VESA, 1280, 960, 60, 0x20 }, + { EstablishedTiming::Source::VESA, 1280, 960, 85, 0x21 }, + { EstablishedTiming::Source::VESA, 1280, 1024, 60, 0x23 }, + { EstablishedTiming::Source::VESA, 1280, 1024, 85, 0x25 }, + // Byte 3 + { EstablishedTiming::Source::VESA, 1360, 768, 60, 0x27 }, + { EstablishedTiming::Source::VESA, 1440, 900, 60, 0x2e }, + { EstablishedTiming::Source::VESA, 1440, 900, 60, 0x2f }, + { EstablishedTiming::Source::VESA, 1440, 900, 75, 0x30 }, + { EstablishedTiming::Source::VESA, 1440, 900, 85, 0x31 }, + { EstablishedTiming::Source::VESA, 1400, 1050, 60, 0x29 }, + { EstablishedTiming::Source::VESA, 1400, 1050, 60, 0x2a }, + { EstablishedTiming::Source::VESA, 1400, 1050, 75, 0x2b }, + // Byte 4 + { EstablishedTiming::Source::VESA, 1400, 1050, 85, 0x2c }, + { EstablishedTiming::Source::VESA, 1680, 1050, 60, 0x39 }, + { EstablishedTiming::Source::VESA, 1680, 1050, 60, 0x3a }, + { EstablishedTiming::Source::VESA, 1680, 1050, 75, 0x3b }, + { EstablishedTiming::Source::VESA, 1680, 1050, 85, 0x3c }, + { EstablishedTiming::Source::VESA, 1600, 1200, 60, 0x33 }, + { EstablishedTiming::Source::VESA, 1600, 1200, 65, 0x34 }, + { EstablishedTiming::Source::VESA, 1600, 1200, 70, 0x35 }, + // Byte 5 + { EstablishedTiming::Source::VESA, 1600, 1200, 75, 0x36 }, + { EstablishedTiming::Source::VESA, 1600, 1200, 85, 0x37 }, + { EstablishedTiming::Source::VESA, 1792, 1344, 60, 0x3e }, + { EstablishedTiming::Source::VESA, 1792, 1344, 75, 0x3f }, + { EstablishedTiming::Source::VESA, 1856, 1392, 60, 0x41 }, + { EstablishedTiming::Source::VESA, 1856, 1392, 75, 0x42 }, + { EstablishedTiming::Source::VESA, 1920, 1200, 60, 0x44 }, + { EstablishedTiming::Source::VESA, 1920, 1200, 60, 0x45 }, + // Byte 6 + { EstablishedTiming::Source::VESA, 1920, 1200, 75, 0x46 }, + { EstablishedTiming::Source::VESA, 1920, 1200, 85, 0x47 }, + { EstablishedTiming::Source::VESA, 1920, 1440, 60, 0x49 }, + { EstablishedTiming::Source::VESA, 1920, 1440, 75, 0x4a } + // Reserved + }; + + size_t byte_index = 0; + for (u8 dmt_bits : display_descriptor.established_timings3.dmt_bits) { + for (int i = 7; i >= 0; i--) { + if ((dmt_bits & (1 << i)) == 0) + continue; + + size_t table_index = byte_index * 8 + (size_t)(7 - i); + if (table_index >= (sizeof(established_timings3_bytes) + 7) / sizeof(established_timings3_bytes[0])) + break; // Sometimes reserved bits are set + + callback_decision = callback(established_timings3_bytes[table_index]); + if (callback_decision != IterationDecision::Continue) + return IterationDecision::Break; + } + byte_index++; + } + return IterationDecision::Break; // Only process one descriptor + }); + if (result.is_error()) + return result.error(); + return callback_decision; +} + +ErrorOr Parser::for_each_standard_timing(Function callback) const +{ + for (size_t index = 0; index < 8; index++) { + auto& standard_timings = raw_edid().standard_timings[index]; + if (standard_timings.horizontal_8_pixels == 0x1 && standard_timings.ratio_and_refresh_rate == 0x1) + continue; // Skip unused records + u16 width = 8 * ((u16)read_host(&standard_timings.horizontal_8_pixels) + 31); + u8 aspect_ratio_and_refresh_rate = read_host(&standard_timings.ratio_and_refresh_rate); + u8 refresh_rate = (aspect_ratio_and_refresh_rate & 0x3f) + 60; + u16 height; + StandardTiming::AspectRatio aspect_ratio; + switch ((aspect_ratio_and_refresh_rate >> 6) & 3) { + case 0: + height = (width * 10) / 16; + aspect_ratio = StandardTiming::AspectRatio::AR_16_10; + break; + case 1: + height = (width * 3) / 4; + aspect_ratio = StandardTiming::AspectRatio::AR_4_3; + break; + case 2: + height = (width * 4) / 5; + aspect_ratio = StandardTiming::AspectRatio::AR_5_4; + break; + case 3: + height = (width * 9) / 16; + aspect_ratio = StandardTiming::AspectRatio::AR_16_9; + break; + default: + VERIFY_NOT_REACHED(); + } + + auto* dmt = DMT::find_timing_by_std_id(standard_timings.horizontal_8_pixels, standard_timings.ratio_and_refresh_rate); + IterationDecision decision = callback(StandardTiming(width, height, refresh_rate, aspect_ratio, dmt ? dmt->dmt_id : 0)); + if (decision != IterationDecision::Continue) + return decision; + } + + return IterationDecision::Continue; +} + +u16 Parser::CoordinatedVideoTiming::horizontal_addressable_pixels() const +{ + u32 aspect_h, aspect_v; + switch (aspect_ratio()) { + case AspectRatio::AR_4_3: + aspect_h = 4; + aspect_v = 3; + break; + case AspectRatio::AR_16_9: + aspect_h = 16; + aspect_v = 9; + break; + case AspectRatio::AR_16_10: + aspect_h = 16; + aspect_v = 10; + break; + case AspectRatio::AR_15_9: + aspect_h = 15; + aspect_v = 9; + break; + } + // Round down to nearest cell as per 3.10.3.8 + return (u16)(8 * ((((u32)vertical_addressable_lines() * aspect_h) / aspect_v) / 8)); +} + +u16 Parser::CoordinatedVideoTiming::vertical_addressable_lines() const +{ + return ((u16)(m_cvt.bytes[1] >> 4) << 8) | (u16)m_cvt.bytes[0]; +} + +auto Parser::CoordinatedVideoTiming::aspect_ratio() const -> AspectRatio +{ + return (AspectRatio)((m_cvt.bytes[2] >> 2) & 0x3); +} + +u16 Parser::CoordinatedVideoTiming::preferred_refresh_rate() +{ + switch ((m_cvt.bytes[2] >> 5) & 3) { + case 0: + return 50; + case 1: + return 60; + case 2: + return 75; + case 3: + return 85; + default: + VERIFY_NOT_REACHED(); + } +} + +ErrorOr Parser::for_each_coordinated_video_timing(Function callback) const +{ + return for_each_display_descriptor([&](u8 descriptor_tag, Definitions::DisplayDescriptor const& display_descriptor) { + if (descriptor_tag != (u8)Definitions::DisplayDescriptorTag::CVTTimingCodes) + return IterationDecision::Continue; + u8 version = read_host(&display_descriptor.coordinated_video_timings.version); + if (version != 1) { + dbgln("Unsupported CVT display descriptor version: {}", version); + return IterationDecision::Continue; + } + + for (size_t i = 0; i < 4; i++) { + const DMT::CVT cvt { + { + read_host(&display_descriptor.coordinated_video_timings.cvt[i][0]), + read_host(&display_descriptor.coordinated_video_timings.cvt[i][1]), + read_host(&display_descriptor.coordinated_video_timings.cvt[i][2]), + } + }; + if (cvt.bytes[0] == 0 && cvt.bytes[1] == 0 && cvt.bytes[2] == 0) + continue; + + IterationDecision decision = callback(CoordinatedVideoTiming(cvt)); + if (decision != IterationDecision::Continue) + return decision; + } + return IterationDecision::Continue; + }); +} + +ErrorOr Parser::for_each_detailed_timing(Function callback) const +{ + auto& edid = raw_edid(); + for (size_t raw_index = 0; raw_index < 4; raw_index++) { + if (raw_index == 0 || read_le(&edid.detailed_timing_or_display_descriptors[raw_index].detailed_timing.pixel_clock) != 0) { + IterationDecision decision = callback(DetailedTiming(*this, &edid.detailed_timing_or_display_descriptors[raw_index].detailed_timing), 0); + if (decision != IterationDecision::Continue) + return decision; + } + } + + Optional extension_error; + auto result = for_each_extension_block([&](u8 block_id, u8 tag, u8, ReadonlyBytes bytes) { + if (tag != Definitions::ExtensionBlockTag::CEA_861) + return IterationDecision::Continue; + + CEA861ExtensionBlock cea861(*this, (Definitions::ExtensionBlock const*)bytes.data()); + auto result = cea861.for_each_dtd([&](auto& dtd) { + return callback(dtd, block_id); + }); + if (result.is_error()) { + dbgln("Failed to iterate DTDs in CEA861 extension block: {}", result.error()); + extension_error = result.error(); + return IterationDecision::Break; + } + + return result.value(); + }); + if (!result.is_error()) { + if (extension_error.has_value()) + return extension_error.value(); + } + return result; +} + +auto Parser::detailed_timing(size_t index) const -> Optional +{ + Optional found_dtd; + auto result = for_each_detailed_timing([&](DetailedTiming const& dtd, unsigned) { + if (index == 0) { + found_dtd = dtd; + return IterationDecision::Break; + } + index--; + return IterationDecision::Continue; + }); + if (result.is_error()) { + dbgln("Error getting Parser detailed timing #{}: {}", index, result.error()); + return {}; + } + return found_dtd; +} + +ErrorOr Parser::for_each_short_video_descriptor(Function callback) const +{ + Optional extension_error; + auto result = for_each_extension_block([&](u8 block_id, u8 tag, u8, ReadonlyBytes bytes) { + if (tag != Definitions::ExtensionBlockTag::CEA_861) + return IterationDecision::Continue; + + CEA861ExtensionBlock cea861(*this, (Definitions::ExtensionBlock const*)bytes.data()); + auto result = cea861.for_each_short_video_descriptor([&](bool is_native, VIC::Details const& vic) { + return callback(block_id, is_native, vic); + }); + if (result.is_error()) { + extension_error = result.error(); + return IterationDecision::Break; + } + return result.value(); + }); + if (result.is_error()) { + dbgln("Failed to iterate Parser extension blocks: {}", result.error()); + return IterationDecision::Break; + } + return result.value(); +} + +ErrorOr Parser::for_each_display_descriptor(Function callback) const +{ + auto& edid = raw_edid(); + for (size_t raw_index = 1; raw_index < 4; raw_index++) { + auto& display_descriptor = edid.detailed_timing_or_display_descriptors[raw_index].display_descriptor; + if (read_le(&display_descriptor.zero) != 0 || read_host(&display_descriptor.reserved1) != 0) + continue; + + u8 tag = read_host(&display_descriptor.tag); + IterationDecision decision = callback(tag, display_descriptor); + if (decision != IterationDecision::Continue) + return decision; + } + + Optional extension_error; + auto result = for_each_extension_block([&](u8, u8 tag, u8, ReadonlyBytes bytes) { + if (tag != Definitions::ExtensionBlockTag::CEA_861) + return IterationDecision::Continue; + + CEA861ExtensionBlock cea861(*this, (Definitions::ExtensionBlock const*)bytes.data()); + auto result = cea861.for_each_display_descriptor([&](u8 tag, auto& display_descriptor) { + return callback(tag, display_descriptor); + }); + if (result.is_error()) { + dbgln("Failed to iterate display descriptors in CEA861 extension block: {}", result.error()); + extension_error = result.error(); + return IterationDecision::Break; + } + + return result.value(); + }); + if (!result.is_error()) { + if (extension_error.has_value()) + return extension_error.value(); + } + return result; +} + +String Parser::display_product_name() const +{ + String product_name; + auto result = for_each_display_descriptor([&](u8 descriptor_tag, Definitions::DisplayDescriptor const& display_descriptor) { + if (descriptor_tag != (u8)Definitions::DisplayDescriptorTag::DisplayProductName) + return IterationDecision::Continue; + + StringBuilder str; + for (u8 byte : display_descriptor.display_product_name.ascii_name) { + if (byte == 0xa) + break; + str.append((char)byte); + } + product_name = str.build(); + return IterationDecision::Break; + }); + if (result.is_error()) { + dbgln("Failed to locate product name display descriptor: {}", result.error()); + return {}; + } + return product_name; +} + +String Parser::display_product_serial_number() const +{ + String product_name; + auto result = for_each_display_descriptor([&](u8 descriptor_tag, Definitions::DisplayDescriptor const& display_descriptor) { + if (descriptor_tag != (u8)Definitions::DisplayDescriptorTag::DisplayProductSerialNumber) + return IterationDecision::Continue; + + StringBuilder str; + for (u8 byte : display_descriptor.display_product_serial_number.ascii_str) { + if (byte == 0xa) + break; + str.append((char)byte); + } + product_name = str.build(); + return IterationDecision::Break; + }); + if (result.is_error()) { + dbgln("Failed to locate product name display descriptor: {}", result.error()); + return {}; + } + return product_name; +} + +auto Parser::supported_resolutions() const -> ErrorOr> +{ + Vector resolutions; + + auto add_resolution = [&](unsigned width, unsigned height, FixedPoint<16, u32> refresh_rate, bool preferred = false) { + auto it = resolutions.find_if([&](auto& info) { + return info.width == width && info.height == height; + }); + if (it == resolutions.end()) { + resolutions.append({ width, height, { { refresh_rate, preferred } } }); + } else { + auto& info = *it; + SupportedResolution::RefreshRate* found_refresh_rate = nullptr; + for (auto& supported_refresh_rate : info.refresh_rates) { + if (supported_refresh_rate.rate == refresh_rate) { + found_refresh_rate = &supported_refresh_rate; + break; + } + } + if (found_refresh_rate) + found_refresh_rate->preferred |= preferred; + else + info.refresh_rates.append({ refresh_rate, preferred }); + } + }; + + auto result = for_each_established_timing([&](auto& established_timing) { + if (established_timing.source() != EstablishedTiming::Source::Manufacturer) + add_resolution(established_timing.width(), established_timing.height(), established_timing.refresh_rate()); + return IterationDecision::Continue; + }); + if (result.is_error()) + return result.error(); + + result = for_each_standard_timing([&](auto& standard_timing) { + add_resolution(standard_timing.width(), standard_timing.height(), standard_timing.refresh_rate()); + return IterationDecision::Continue; + }); + if (result.is_error()) + return result.error(); + + size_t detailed_timing_index = 0; + result = for_each_detailed_timing([&](auto& detailed_timing, auto) { + bool is_preferred = detailed_timing_index++ == 0; + add_resolution(detailed_timing.horizontal_addressable_pixels(), detailed_timing.vertical_addressable_lines(), detailed_timing.refresh_rate(), is_preferred); + return IterationDecision::Continue; + }); + if (result.is_error()) + return result.error(); + + result = for_each_short_video_descriptor([&](unsigned, bool, VIC::Details const& vic_details) { + add_resolution(vic_details.horizontal_pixels, vic_details.vertical_lines, vic_details.refresh_rate_hz()); + return IterationDecision::Continue; + }); + if (result.is_error()) + return result.error(); + + result = for_each_coordinated_video_timing([&](auto& coordinated_video_timing) { + if (auto* dmt = DMT::find_timing_by_cvt(coordinated_video_timing.cvt_code())) { + add_resolution(dmt->horizontal_pixels, dmt->vertical_lines, dmt->vertical_frequency_hz()); + } else { + // TODO: We couldn't find this cvt code, try to decode it + auto cvt = coordinated_video_timing.cvt_code(); + dbgln("TODO: Decode CVT code: {:02x},{:02x},{:02x}", cvt.bytes[0], cvt.bytes[1], cvt.bytes[2]); + } + return IterationDecision::Continue; + }); + + quick_sort(resolutions, [&](auto& info1, auto& info2) { + if (info1.width < info2.width) + return true; + if (info1.width == info2.width && info1.height < info2.height) + return true; + return false; + }); + for (auto& res : resolutions) { + if (res.refresh_rates.size() > 1) + quick_sort(res.refresh_rates); + } + return resolutions; +} + +} diff --git a/Userland/Libraries/LibEDID/EDID.h b/Userland/Libraries/LibEDID/EDID.h new file mode 100644 index 0000000000..82aced81fe --- /dev/null +++ b/Userland/Libraries/LibEDID/EDID.h @@ -0,0 +1,436 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace EDID { + +namespace Definitions { +struct EDID; +struct DetailedTiming; +struct DisplayDescriptor; +struct ExtensionBlock; +} + +class Parser final { + friend class CEA861ExtensionBlock; + +public: + static constexpr size_t BufferSize = 128; + using RawBytes = unsigned char[BufferSize]; + +protected: + class DisplayFeatures { + public: + bool supports_standby() const { return (m_features & (1 << 7)) != 0; } + bool supports_suspend() const { return (m_features & (1 << 6)) != 0; } + bool supports_off() const { return (m_features & (1 << 5)) != 0; } + + bool preferred_timing_mode_includes_pixel_format_and_refresh_rate() const + { + if (m_edid_revision < 4) + return true; // Bit 1 must be set to 1 + return (m_features & (1 << 1)) != 0; + } + + bool srgb_is_default_color_space() const { return (m_features & (1 << 2)) != 0; } + + enum class Frequency : u8 { + Continuous, + NonContinuous, + DefaultGTF, + VESA_DMT + }; + Frequency frequency() const + { + if (m_edid_revision < 4) + return ((m_features & 1) != 0) ? Frequency::DefaultGTF : Frequency::VESA_DMT; + return ((m_features & 1) != 0) ? Frequency::Continuous : Frequency::NonContinuous; + } + + protected: + DisplayFeatures(u8 features, u8 edid_revision) + : m_features(features) + , m_edid_revision(edid_revision) + { + } + + u8 m_features { 0 }; + u8 m_edid_revision { 0 }; + }; + +public: + static ErrorOr from_bytes(ReadonlyBytes); + static ErrorOr from_bytes(ByteBuffer&&); + + String legacy_manufacturer_id() const; + u16 product_code() const; + u32 serial_number() const; + + class DigitalDisplayFeatures final : public DisplayFeatures { + friend class Parser; + + public: + enum class SupportedColorEncodings : u8 { + RGB444, + RGB444_YCrCb444, + RGB444_YCrCb422, + RGB444_YCrCb444_YCrCb422 + }; + + SupportedColorEncodings supported_color_encodings() const { return (SupportedColorEncodings)((m_features >> 3) & 3); } + + private: + DigitalDisplayFeatures(u8 features, u8 edid_revision) + : DisplayFeatures(features, edid_revision) + { + } + }; + + class AnalogDisplayFeatures final : public DisplayFeatures { + friend class Parser; + + public: + enum class DisplayColorType : u8 { + MonochromeOrGrayscale, + RGB, + NonRGB, + Undefined + }; + + DisplayColorType display_color_type() const { return (DisplayColorType)((m_features >> 3) & 3); } + + private: + AnalogDisplayFeatures(u8 features, u8 edid_revision) + : DisplayFeatures(features, edid_revision) + { + } + }; + + class DigitalDisplay final { + friend class Parser; + + public: + enum class ColorBitDepth : u8 { + Undefined = 0, + BPP_6, + BPP_8, + BPP_10, + BPP_12, + BPP_14, + BPP_16, + Reserved + }; + enum class SupportedInterface : u8 { + Undefined = 0, + DVI, + HDMI_A, + HDMI_B, + MDDI, + DisplayPort, + Reserved + }; + + ColorBitDepth color_bit_depth() const { return (ColorBitDepth)((m_video_input_definition >> 4) & 7); } + SupportedInterface supported_interface() const { return ((m_video_input_definition & 0xf) <= 5) ? (SupportedInterface)(m_video_input_definition & 0xf) : SupportedInterface::Reserved; } + + DigitalDisplayFeatures const& features() { return m_features; } + + private: + DigitalDisplay(u8 video_input_definition, u8 features, u8 edid_revision) + : m_video_input_definition(video_input_definition) + , m_features(features, edid_revision) + { + } + + u8 m_video_input_definition { 0 }; + DigitalDisplayFeatures m_features; + }; + Optional digital_display() const; + + class AnalogDisplay final { + friend class Parser; + + public: + bool separate_sync_h_and_v_supported() const { return (m_video_input_definition & (1 << 3)) != 0; } + + private: + AnalogDisplay(u8 video_input_definition, u8 features, u8 edid_revision) + : m_video_input_definition(video_input_definition) + , m_features(features, edid_revision) + { + } + + u8 m_video_input_definition { 0 }; + AnalogDisplayFeatures m_features; + }; + Optional analog_display() const; + + class ScreenSize final { + friend class Parser; + + public: + unsigned horizontal_cm() const { return m_horizontal_cm; } + unsigned vertical_cm() const { return m_vertical_cm; } + + private: + ScreenSize(u8 horizontal_cm, u8 vertical_cm) + : m_horizontal_cm(horizontal_cm) + , m_vertical_cm(vertical_cm) + { + } + + u8 m_horizontal_cm { 0 }; + u8 m_vertical_cm { 0 }; + }; + Optional screen_size() const; + + class ScreenAspectRatio final { + friend class Parser; + + public: + enum class Orientation { + Landscape, + Portrait + }; + + Orientation orientation() const { return m_orientation; } + auto ratio() const { return m_ratio; } + + private: + ScreenAspectRatio(Orientation orientation, FixedPoint<16> ratio) + : m_orientation(orientation) + , m_ratio(ratio) + { + } + + Orientation m_orientation { Orientation::Landscape }; + FixedPoint<16> m_ratio {}; + }; + Optional aspect_ratio() const; + + Optional> gamma() const; + + class EstablishedTiming final { + friend class Parser; + + public: + enum class Source { + IBM, + Apple, + VESA, + Manufacturer + }; + + ALWAYS_INLINE Source source() const { return m_source; } + ALWAYS_INLINE unsigned width() const { return m_width; }; + ALWAYS_INLINE unsigned height() const { return m_height; } + + ALWAYS_INLINE unsigned refresh_rate() const + { + if (m_source == Source::Manufacturer) + return 0; + return m_refresh_rate_or_manufacturer_specific; + } + + ALWAYS_INLINE u8 manufacturer_specific() const + { + VERIFY(m_source == Source::Manufacturer); + return m_refresh_rate_or_manufacturer_specific; + } + + ALWAYS_INLINE u8 dmt_id() const { return m_dmt_id; } + + private: + constexpr EstablishedTiming(Source source, u16 width, u16 height, u8 refresh_rate_or_manufacturer_specific, u8 dmt_id = 0) + : m_source(source) + , m_width(width) + , m_height(height) + , m_refresh_rate_or_manufacturer_specific(refresh_rate_or_manufacturer_specific) + , m_dmt_id(dmt_id) + { + } + + Source m_source { Source::IBM }; + u16 m_width { 0 }; + u16 m_height { 0 }; + u8 m_refresh_rate_or_manufacturer_specific { 0 }; + u8 m_dmt_id { 0 }; + }; + + ErrorOr for_each_established_timing(Function) const; + + class StandardTiming final { + friend class Parser; + + public: + enum class AspectRatio { + AR_16_10, + AR_4_3, + AR_5_4, + AR_16_9 + }; + unsigned width() const { return m_width; } + unsigned height() const { return m_height; } + unsigned refresh_rate() const { return m_refresh_rate; } + AspectRatio aspect_ratio() const { return m_aspect_ratio; } + u8 dmt_id() const { return m_dmt_id; } + + private: + constexpr StandardTiming(u16 width, u16 height, u8 refresh_rate, AspectRatio aspect_ratio, u8 dmt_id) + : m_width(width) + , m_height(height) + , m_refresh_rate(refresh_rate) + , m_aspect_ratio(aspect_ratio) + , m_dmt_id(dmt_id) + { + } + + u16 m_width { 0 }; + u16 m_height { 0 }; + u8 m_refresh_rate { 0 }; + AspectRatio m_aspect_ratio { AspectRatio::AR_16_10 }; + u8 m_dmt_id { 0 }; + }; + + ErrorOr for_each_standard_timing(Function) const; + + class DetailedTiming final { + friend class Parser; + friend class CEA861ExtensionBlock; + + public: + u32 pixel_clock_khz() const; + u16 horizontal_addressable_pixels() const; + u16 horizontal_blanking_pixels() const; + u16 vertical_addressable_lines() const; + u16 vertical_blanking_lines() const; + u16 horizontal_front_porch_pixels() const; + ALWAYS_INLINE u16 horizontal_back_porch_pixels() const { return horizontal_blanking_pixels() - horizontal_sync_pulse_width_pixels() - horizontal_front_porch_pixels(); } + u16 horizontal_sync_pulse_width_pixels() const; + u16 vertical_front_porch_lines() const; + ALWAYS_INLINE u16 vertical_back_porch_lines() const { return vertical_blanking_lines() - vertical_sync_pulse_width_lines() - vertical_front_porch_lines(); } + u16 vertical_sync_pulse_width_lines() const; + u16 horizontal_image_size_mm() const; + u16 vertical_image_size_mm() const; + u8 horizontal_right_or_left_border_pixels() const; + u8 vertical_top_or_bottom_border_lines() const; + + bool is_interlaced() const; + FixedPoint<16, u32> refresh_rate() const; + + private: + DetailedTiming(Parser const& edid, Definitions::DetailedTiming const* detailed_timings) + : m_edid(edid) + , m_detailed_timings(*detailed_timings) + { + } + + Parser const& m_edid; + Definitions::DetailedTiming const& m_detailed_timings; + }; + + ErrorOr for_each_detailed_timing(Function) const; + Optional detailed_timing(size_t) const; + + String display_product_name() const; + String display_product_serial_number() const; + + ErrorOr for_each_short_video_descriptor(Function) const; + + class CoordinatedVideoTiming final { + friend class Parser; + + public: + enum class AspectRatio : u8 { + AR_4_3 = 0, + AR_16_9 = 1, + AR_16_10 = 2, + AR_15_9 = 3 + }; + + u16 horizontal_addressable_pixels() const; + u16 vertical_addressable_lines() const; + AspectRatio aspect_ratio() const; + u16 preferred_refresh_rate(); + + ALWAYS_INLINE DMT::CVT cvt_code() const { return m_cvt; } + + private: + CoordinatedVideoTiming(DMT::CVT const& cvt) + : m_cvt(cvt) + { + } + + DMT::CVT m_cvt; + }; + + ErrorOr for_each_coordinated_video_timing(Function) const; + + ErrorOr for_each_extension_block(Function) const; + + struct SupportedResolution { + unsigned width { 0 }; + unsigned height { 0 }; + struct RefreshRate { + FixedPoint<16, u32> rate; + bool preferred { false }; + + bool operator<(RefreshRate const& rhs) const { return rate < rhs.rate; } + }; + Vector refresh_rates; + }; + ErrorOr> supported_resolutions() const; + + Parser() = default; + Parser(Parser&&) = default; + Parser(Parser const&); + Parser& operator=(Parser&&); + Parser& operator=(Parser const&); + + bool operator==(Parser const& other) const; + + String version() const; + + auto bytes() const { return m_bytes; } + +private: + Parser(ReadonlyBytes); + Parser(ByteBuffer&&); + + ErrorOr parse(); + + template + T read_host(T const*) const; + + template + requires(IsIntegral && sizeof(T) > 1) T read_le(T const*) + const; + + template + requires(IsIntegral && sizeof(T) > 1) T read_be(T const*) + const; + + Definitions::EDID const& raw_edid() const; + ErrorOr for_each_display_descriptor(Function) const; + + ByteBuffer m_bytes_buffer; + ReadonlyBytes m_bytes; + u8 m_revision { 0 }; +}; + +} diff --git a/Userland/Libraries/LibEDID/VIC.cpp b/Userland/Libraries/LibEDID/VIC.cpp new file mode 100644 index 0000000000..dd9e375898 --- /dev/null +++ b/Userland/Libraries/LibEDID/VIC.cpp @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace EDID { + +// Video ID Code details as per CTA-861-G revised 2018 Table 3 +static constexpr VIC::Details s_vic_details[] = { + { 1, 640, 480, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 2, 720, 480, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 3, 720, 480, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 4, 1280, 720, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 5, 1920, 1080, 59940, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 6, 1440, 480, 59940, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 7, 1440, 480, 59940, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 8, 1440, 240, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 9, 1440, 240, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 10, 2880, 480, 59940, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 11, 2880, 480, 59940, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 12, 2880, 240, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 13, 2880, 240, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 14, 1440, 480, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 15, 1440, 480, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 16, 1920, 180, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 17, 720, 576, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 18, 720, 576, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 19, 1280, 720, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 20, 1920, 1080, 50000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 21, 1440, 576, 50000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 22, 1440, 576, 50000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 23, 1440, 288, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 24, 1440, 288, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 25, 2880, 576, 50000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 26, 2880, 576, 50000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 27, 2880, 288, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 28, 2880, 288, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 29, 1440, 576, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 30, 1440, 576, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 31, 1920, 1080, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 32, 1920, 1080, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 33, 1920, 1080, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 34, 1920, 1080, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 35, 2880, 480, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 36, 2880, 480, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 37, 2880, 576, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 38, 2880, 576, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 39, 1920, 1080, 50000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 40, 1920, 1080, 100000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 41, 1280, 720, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 42, 720, 576, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 43, 720, 576, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 44, 1440, 576, 100000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 45, 1440, 576, 100000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 46, 1920, 1080, 119880, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 47, 1280, 720, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 48, 720, 480, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 49, 720, 480, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 50, 1440, 480, 119880, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 51, 1440, 480, 119880, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 52, 720, 576, 200000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 53, 720, 576, 200000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 54, 1440, 576, 200000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 55, 1440, 576, 200000, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 56, 720, 480, 239760, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 57, 720, 480, 239760, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 58, 1440, 480, 239760, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_4_3 }, + { 59, 1440, 480, 239760, VIC::Details::ScanType::Interlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 60, 1280, 720, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 61, 1280, 720, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 62, 1280, 720, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 63, 1920, 1080, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 64, 1920, 1080, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 65, 1280, 720, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 66, 1280, 720, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 67, 1280, 720, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 68, 1280, 720, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 69, 1280, 720, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 70, 1280, 720, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 71, 1280, 720, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 72, 1920, 1080, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 73, 1920, 1080, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 74, 1920, 1080, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 75, 1920, 1080, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 76, 1920, 1080, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 77, 1920, 1080, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 78, 1920, 1080, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 79, 1680, 720, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 80, 1680, 720, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 81, 1680, 720, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 82, 1680, 720, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 83, 1680, 720, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 84, 1680, 720, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 85, 1680, 720, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 86, 2560, 1080, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 87, 2560, 1080, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 88, 2560, 1080, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 89, 2560, 1080, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 90, 2560, 1080, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 91, 2560, 1080, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 92, 2560, 1080, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 93, 3840, 2160, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 94, 3840, 2160, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 95, 3840, 2160, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 96, 3840, 2160, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 97, 3840, 2160, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 98, 4096, 2160, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_256_135 }, + { 99, 4096, 2160, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_256_135 }, + { 100, 4096, 2160, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_256_135 }, + { 101, 4096, 2160, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_256_135 }, + { 102, 4096, 2160, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_256_135 }, + { 103, 3840, 2160, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 104, 3840, 2160, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 105, 3840, 2160, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 106, 3840, 2160, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 107, 3840, 2160, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 108, 1280, 720, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 109, 1280, 720, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 110, 1680, 720, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 111, 1920, 1080, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 112, 1920, 1080, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 113, 2560, 1080, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 114, 3840, 2160, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 115, 4096, 2160, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_256_135 }, + { 116, 3840, 2160, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 117, 3840, 2160, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 118, 3840, 2160, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 119, 3840, 2160, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 120, 3840, 2160, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 121, 5120, 2160, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 122, 5120, 2160, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 123, 5120, 2160, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 124, 5120, 2160, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 125, 5120, 2160, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 126, 5120, 2160, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 127, 5120, 2160, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + // 128...192 forbidden range + { 193, 5120, 2160, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 194, 7680, 4320, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 195, 7680, 4320, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 196, 7680, 4320, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 197, 7680, 4320, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 198, 7680, 4320, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 199, 7680, 4320, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 200, 7680, 4320, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 201, 7680, 4320, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_16_9 }, + { 202, 7680, 4320, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 203, 7680, 4320, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 204, 7680, 4320, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 205, 7680, 4320, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 206, 7680, 4320, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 207, 7680, 4320, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 208, 7680, 4320, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 209, 7680, 4320, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 210, 10240, 4320, 23980, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 211, 10240, 4320, 25000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 212, 10240, 4320, 29970, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 213, 10240, 4320, 47950, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 214, 10240, 4320, 50000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 215, 10240, 4320, 59940, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 216, 10240, 4320, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 217, 10240, 4320, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_64_27 }, + { 218, 4096, 2160, 100000, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_256_135 }, + { 219, 4096, 2160, 119880, VIC::Details::ScanType::NonInterlaced, VIC::Details::AspectRatio::AR_256_135 }, + // 220...255 reserved +}; + +static constexpr size_t s_vic_details_count = sizeof(s_vic_details) / sizeof(s_vic_details[0]); +static constexpr u8 s_reserved_vic_id_start = 220; +static_assert(s_vic_details[s_vic_details_count - 1].vic_id == s_reserved_vic_id_start - 1); + +FixedPoint<16, u32> VIC::Details::refresh_rate_hz() const +{ + return FixedPoint<16, u32>(refresh_rate_millihz) / 1000; +} + +auto VIC::find_details_by_vic_id(u8 vic_id) -> Details const* +{ + if (vic_id == 0 || (vic_id >= 128 && vic_id <= 192) || vic_id >= s_reserved_vic_id_start) + return nullptr; + + u8 table_index = vic_id - 1; + if (table_index < 128) { + // Before the forbidden block (128...192) + VERIFY(s_vic_details[table_index].vic_id == vic_id); + return &s_vic_details[table_index]; + } + + // After the forbidden block range (128...192) + table_index = table_index - 192 + 128 - 1; + VERIFY(s_vic_details[table_index].vic_id == vic_id); + return &s_vic_details[table_index]; +} + +} diff --git a/Userland/Libraries/LibEDID/VIC.h b/Userland/Libraries/LibEDID/VIC.h new file mode 100644 index 0000000000..2ccb61ed29 --- /dev/null +++ b/Userland/Libraries/LibEDID/VIC.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace EDID { + +class VIC final { +public: + struct Details { + enum class ScanType : u8 { + NonInterlaced, + Interlaced + }; + enum class AspectRatio : u8 { + AR_4_3, + AR_16_9, + AR_64_27, + AR_256_135, + }; + + u8 vic_id; + u16 horizontal_pixels; + u16 vertical_lines; + u32 refresh_rate_millihz; + ScanType scan_type; + AspectRatio aspect_ratio; + + FixedPoint<16, u32> refresh_rate_hz() const; + }; + + static Details const* find_details_by_vic_id(u8); +}; + +}