diff options
-rw-r--r-- | Tests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Tests/LibEDID/CMakeLists.txt | 7 | ||||
-rw-r--r-- | Tests/LibEDID/TestEDID.cpp | 408 | ||||
-rw-r--r-- | Userland/Libraries/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibEDID/CMakeLists.txt | 8 | ||||
-rw-r--r-- | Userland/Libraries/LibEDID/DMT.cpp | 163 | ||||
-rw-r--r-- | Userland/Libraries/LibEDID/DMT.h | 64 | ||||
-rw-r--r-- | Userland/Libraries/LibEDID/EDID.cpp | 1190 | ||||
-rw-r--r-- | Userland/Libraries/LibEDID/EDID.h | 436 | ||||
-rw-r--r-- | Userland/Libraries/LibEDID/VIC.cpp | 199 | ||||
-rw-r--r-- | Userland/Libraries/LibEDID/VIC.h | 42 |
11 files changed, 2519 insertions, 0 deletions
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 <LibEDID/DMT.h> +#include <LibEDID/EDID.h> +#include <LibTest/TestCase.h> + +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 <AK/String.h> +#include <LibEDID/DMT.h> + +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 <AK/FixedPoint.h> +#include <AK/Optional.h> +#include <AK/Types.h> + +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 <AK/Function.h> +#include <AK/QuickSort.h> +#include <LibEDID/EDID.h> + +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<IterationDecision> for_each_short_video_descriptor(Function<IterationDecision(bool, VIC::Details const&)> callback) const + { + return for_each_data_block([&](DataBlockTag tag, ReadonlyBytes bytes) -> ErrorOr<IterationDecision> { + 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<IterationDecision> for_each_dtd(Function<IterationDecision(Parser::DetailedTiming const&)> 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<IterationDecision> for_each_data_block(Function<ErrorOr<IterationDecision>(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<IterationDecision> for_each_display_descriptor(Function<IterationDecision(u8, Definitions::DisplayDescriptor const&)> 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<typename T> +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<typename T> +requires(IsIntegral<T> && 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<typename T> +requires(IsIntegral<T> && 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> 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> 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<void> 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<IterationDecision> Parser::for_each_extension_block(Function<IterationDecision(unsigned, u8, u8, ReadonlyBytes)> 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<DigitalDisplay> +{ + 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<AnalogDisplay> +{ + 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<ScreenSize> +{ + 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<ScreenAspectRatio> +{ + 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<FixedPoint<16>> 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<IterationDecision> Parser::for_each_established_timing(Function<IterationDecision(EstablishedTiming const&)> 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<IterationDecision> Parser::for_each_standard_timing(Function<IterationDecision(StandardTiming const&)> 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<IterationDecision> Parser::for_each_coordinated_video_timing(Function<IterationDecision(CoordinatedVideoTiming const&)> 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<IterationDecision> Parser::for_each_detailed_timing(Function<IterationDecision(DetailedTiming const&, unsigned)> 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<Error> 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<DetailedTiming> +{ + Optional<DetailedTiming> 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<IterationDecision> Parser::for_each_short_video_descriptor(Function<IterationDecision(unsigned, bool, VIC::Details const&)> callback) const +{ + Optional<Error> 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<IterationDecision> Parser::for_each_display_descriptor(Function<IterationDecision(u8, Definitions::DisplayDescriptor const&)> 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<Error> 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<SupportedResolution>> +{ + Vector<SupportedResolution> 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 <AK/ByteReader.h> +#include <AK/Endian.h> +#include <AK/Error.h> +#include <AK/FixedPoint.h> +#include <AK/Forward.h> +#include <AK/Span.h> +#include <AK/String.h> +#include <AK/Vector.h> +#include <LibEDID/DMT.h> +#include <LibEDID/VIC.h> + +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<Parser> from_bytes(ReadonlyBytes); + static ErrorOr<Parser> 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<DigitalDisplay> 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<AnalogDisplay> 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<ScreenSize> 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<ScreenAspectRatio> aspect_ratio() const; + + Optional<FixedPoint<16>> 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<IterationDecision> for_each_established_timing(Function<IterationDecision(EstablishedTiming const&)>) 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<IterationDecision> for_each_standard_timing(Function<IterationDecision(StandardTiming const&)>) 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<IterationDecision> for_each_detailed_timing(Function<IterationDecision(DetailedTiming const&, unsigned)>) const; + Optional<DetailedTiming> detailed_timing(size_t) const; + + String display_product_name() const; + String display_product_serial_number() const; + + ErrorOr<IterationDecision> for_each_short_video_descriptor(Function<IterationDecision(unsigned, bool, VIC::Details const&)>) 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<IterationDecision> for_each_coordinated_video_timing(Function<IterationDecision(CoordinatedVideoTiming const&)>) const; + + ErrorOr<IterationDecision> for_each_extension_block(Function<IterationDecision(unsigned, u8, u8, ReadonlyBytes)>) 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<RefreshRate, 4> refresh_rates; + }; + ErrorOr<Vector<SupportedResolution>> 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<void> parse(); + + template<typename T> + T read_host(T const*) const; + + template<typename T> + requires(IsIntegral<T> && sizeof(T) > 1) T read_le(T const*) + const; + + template<typename T> + requires(IsIntegral<T> && sizeof(T) > 1) T read_be(T const*) + const; + + Definitions::EDID const& raw_edid() const; + ErrorOr<IterationDecision> for_each_display_descriptor(Function<IterationDecision(u8, Definitions::DisplayDescriptor const&)>) 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 <AK/String.h> +#include <LibEDID/VIC.h> + +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 <AK/FixedPoint.h> +#include <AK/Optional.h> +#include <AK/Types.h> + +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); +}; + +} |