summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Tests/CMakeLists.txt1
-rw-r--r--Tests/LibEDID/CMakeLists.txt7
-rw-r--r--Tests/LibEDID/TestEDID.cpp408
-rw-r--r--Userland/Libraries/CMakeLists.txt1
-rw-r--r--Userland/Libraries/LibEDID/CMakeLists.txt8
-rw-r--r--Userland/Libraries/LibEDID/DMT.cpp163
-rw-r--r--Userland/Libraries/LibEDID/DMT.h64
-rw-r--r--Userland/Libraries/LibEDID/EDID.cpp1190
-rw-r--r--Userland/Libraries/LibEDID/EDID.h436
-rw-r--r--Userland/Libraries/LibEDID/VIC.cpp199
-rw-r--r--Userland/Libraries/LibEDID/VIC.h42
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(&current_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);
+};
+
+}