Serenity Operating System
1/*
2 * Copyright (c) 2022, the SerenityOS developers.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/Concepts.h>
8#include <AK/Function.h>
9#include <AK/QuickSort.h>
10#include <AK/Try.h>
11#include <LibEDID/EDID.h>
12
13#ifdef KERNEL
14# include <Kernel/StdLib.h>
15#else
16# include <AK/ScopeGuard.h>
17# include <Kernel/API/Graphics.h>
18# include <fcntl.h>
19# include <unistd.h>
20
21# ifdef ENABLE_PNP_IDS_DATA
22# include <LibEDID/PnpIDs.h>
23# endif
24#endif
25
26namespace EDID {
27
28// clang doesn't like passing around pointers to members in packed structures,
29// even though we're only using them for arithmetic purposes
30#if defined(AK_COMPILER_CLANG)
31# pragma clang diagnostic ignored "-Waddress-of-packed-member"
32#endif
33
34static_assert(sizeof(Definitions::EDID) == Parser::BufferSize);
35
36class CEA861ExtensionBlock final {
37 friend class Parser;
38
39public:
40 enum class DataBlockTag : u8 {
41 Reserved = 0,
42 Audio,
43 Video,
44 VendorSpecific,
45 SpeakerAllocation,
46 VesaDTC,
47 Reserved2,
48 Extended
49 };
50
51 ErrorOr<IterationDecision> for_each_short_video_descriptor(Function<IterationDecision(bool, VIC::Details const&)> callback) const
52 {
53 return for_each_data_block([&](DataBlockTag tag, ReadonlyBytes bytes) -> ErrorOr<IterationDecision> {
54 if (tag != DataBlockTag::Video)
55 return IterationDecision::Continue;
56
57 // Short video descriptors are one byte values
58 for (size_t i = 0; i < bytes.size(); i++) {
59 u8 byte = m_edid.read_host(&bytes[i]);
60 bool is_native = (byte & 0x80) != 0;
61 u8 vic_id = byte & 0x7f;
62
63 auto* vic_details = VIC::find_details_by_vic_id(vic_id);
64 if (!vic_details)
65 return Error::from_string_view_or_print_error_and_return_errno("CEA 861 extension block has invalid short video descriptor"sv, EINVAL);
66
67 IterationDecision decision = callback(is_native, *vic_details);
68 if (decision != IterationDecision::Continue)
69 return decision;
70 }
71 return IterationDecision::Continue;
72 });
73 }
74
75 ErrorOr<IterationDecision> for_each_dtd(Function<IterationDecision(Parser::DetailedTiming const&)> callback) const
76 {
77 u8 dtd_start = m_edid.read_host(&m_block->cea861extension.dtd_start_offset);
78 if (dtd_start < 4) {
79 // dtd_start == 4 means there are no data blocks, but there are still DTDs
80 return IterationDecision::Continue;
81 }
82
83 if (dtd_start > offsetof(Definitions::ExtensionBlock, checksum) - sizeof(Definitions::DetailedTiming))
84 return Error::from_string_view_or_print_error_and_return_errno("CEA 861 extension block has invalid DTD list"sv, EINVAL);
85
86 for (size_t offset = dtd_start; offset <= offsetof(Definitions::ExtensionBlock, checksum) - sizeof(Definitions::DetailedTiming); offset += sizeof(Definitions::DetailedTiming)) {
87 auto& dtd = *(Definitions::DetailedTiming const*)((u8 const*)m_block + offset);
88 if (m_edid.read_host(&dtd.pixel_clock) == 0)
89 break;
90
91 IterationDecision decision = callback(Parser::DetailedTiming(m_edid, &dtd));
92 if (decision != IterationDecision::Continue)
93 return decision;
94 }
95 return IterationDecision::Continue;
96 }
97
98private:
99 CEA861ExtensionBlock(Parser const& edid, Definitions::ExtensionBlock const* block)
100 : m_edid(edid)
101 , m_block(block)
102 {
103 }
104
105 ErrorOr<IterationDecision> for_each_data_block(Function<ErrorOr<IterationDecision>(DataBlockTag, ReadonlyBytes)> callback) const
106 {
107 u8 dtd_start = m_edid.read_host(&m_block->cea861extension.dtd_start_offset);
108 if (dtd_start <= 4)
109 return IterationDecision::Continue;
110
111 if (dtd_start > offsetof(Definitions::ExtensionBlock, checksum))
112 return Error::from_string_view_or_print_error_and_return_errno("CEA 861 extension block has invalid DTD start offset"sv, EINVAL);
113
114 auto* data_block_header = &m_block->cea861extension.bytes[0];
115 auto* data_block_end = (u8 const*)m_block + dtd_start;
116 while (data_block_header < data_block_end) {
117 auto header_byte = m_edid.read_host(data_block_header);
118 size_t payload_size = header_byte & 0x1f;
119 auto tag = (DataBlockTag)((header_byte >> 5) & 0x7);
120 if (tag == DataBlockTag::Extended && payload_size == 0)
121 return Error::from_string_view_or_print_error_and_return_errno("CEA 861 extension block has invalid extended data block size"sv, EINVAL);
122
123 auto decision = TRY(callback(tag, m_edid.m_bytes.slice(data_block_header - m_edid.m_bytes.data() + 1, payload_size)));
124 if (decision != IterationDecision::Continue)
125 return decision;
126
127 data_block_header += 1 + payload_size;
128 }
129 return IterationDecision::Continue;
130 }
131
132 ErrorOr<IterationDecision> for_each_display_descriptor(Function<IterationDecision(u8, Definitions::DisplayDescriptor const&)> callback) const
133 {
134 u8 dtd_start = m_edid.read_host(&m_block->cea861extension.dtd_start_offset);
135 if (dtd_start <= 4)
136 return IterationDecision::Continue;
137
138 if (dtd_start > offsetof(Definitions::ExtensionBlock, checksum) - sizeof(Definitions::DetailedTiming))
139 return Error::from_string_view_or_print_error_and_return_errno("CEA 861 extension block has invalid DTD list"sv, EINVAL);
140
141 for (size_t offset = dtd_start; offset <= offsetof(Definitions::ExtensionBlock, checksum) - sizeof(Definitions::DisplayDescriptor); offset += sizeof(Definitions::DisplayDescriptor)) {
142 auto& dd = *(Definitions::DisplayDescriptor const*)((u8 const*)m_block + offset);
143 if (m_edid.read_host(&dd.zero) != 0 || m_edid.read_host(&dd.reserved1) != 0)
144 continue;
145
146 u8 tag = m_edid.read_host(&dd.tag);
147 IterationDecision decision = callback(tag, dd);
148 if (decision != IterationDecision::Continue)
149 return decision;
150 }
151 return IterationDecision::Continue;
152 }
153
154 Parser const& m_edid;
155 Definitions::ExtensionBlock const* m_block;
156};
157
158template<typename T>
159T Parser::read_host(T const* field) const
160{
161 VERIFY((u8 const*)field >= m_bytes.data() && (u8 const*)field + sizeof(T) <= m_bytes.data() + m_bytes.size());
162 size_t offset = (u8 const*)field - m_bytes.data();
163 T value;
164 if constexpr (sizeof(T) > 1)
165 ByteReader::load(m_bytes.offset(offset), value);
166 else
167 value = m_bytes.at(offset);
168
169 return value;
170}
171
172template<Integral T>
173requires(sizeof(T) > 1)
174T Parser::read_le(T const* field) const
175{
176 static_assert(sizeof(T) > 1);
177 return AK::convert_between_host_and_little_endian(read_host(field));
178}
179
180template<Integral T>
181requires(sizeof(T) > 1)
182T Parser::read_be(T const* field) const
183{
184 static_assert(sizeof(T) > 1);
185 return AK::convert_between_host_and_big_endian(read_host(field));
186}
187
188ErrorOr<Parser> Parser::from_bytes(ReadonlyBytes bytes)
189{
190 Parser edid(bytes);
191 TRY(edid.parse());
192 return edid;
193}
194
195ErrorOr<Parser> Parser::from_bytes(ByteBuffer&& bytes)
196{
197 Parser edid(move(bytes));
198 TRY(edid.parse());
199 return edid;
200}
201
202#ifndef KERNEL
203ErrorOr<Parser> Parser::from_display_connector_device(int display_connector_fd)
204{
205 RawBytes edid_bytes;
206 GraphicsHeadEDID edid_info {};
207 edid_info.bytes = &edid_bytes[0];
208 edid_info.bytes_size = sizeof(edid_bytes);
209 if (graphics_connector_get_head_edid(display_connector_fd, &edid_info) < 0) {
210 int err = errno;
211 if (err == EOVERFLOW) {
212 // We need a bigger buffer with at least bytes_size bytes
213 auto edid_byte_buffer = TRY(ByteBuffer::create_zeroed(edid_info.bytes_size));
214 edid_info.bytes = edid_byte_buffer.data();
215 if (graphics_connector_get_head_edid(display_connector_fd, &edid_info) < 0) {
216 err = errno;
217 return Error::from_errno(err);
218 }
219
220 return from_bytes(move(edid_byte_buffer));
221 }
222
223 return Error::from_errno(err);
224 }
225
226 auto edid_byte_buffer = TRY(ByteBuffer::copy((void const*)edid_bytes, sizeof(edid_bytes)));
227 return from_bytes(move(edid_byte_buffer));
228}
229
230ErrorOr<Parser> Parser::from_display_connector_device(DeprecatedString const& display_connector_device)
231{
232 int display_connector_fd = open(display_connector_device.characters(), O_RDWR | O_CLOEXEC);
233 if (display_connector_fd < 0) {
234 int err = errno;
235 return Error::from_errno(err);
236 }
237 ScopeGuard fd_guard([&] {
238 close(display_connector_fd);
239 });
240 return from_display_connector_device(display_connector_fd);
241}
242#endif
243
244Parser::Parser(ReadonlyBytes bytes)
245 : m_bytes(move(bytes))
246{
247}
248
249Parser::Parser(ByteBuffer&& bytes)
250 : m_bytes_buffer(move(bytes))
251 , m_bytes(m_bytes_buffer)
252{
253}
254
255Parser::Parser(Parser const& other)
256 : m_bytes_buffer(other.m_bytes_buffer)
257 , m_revision(other.m_revision)
258{
259 if (m_bytes_buffer.is_empty())
260 m_bytes = other.m_bytes_buffer; // We don't own the buffer
261 else
262 m_bytes = m_bytes_buffer; // We own the buffer
263}
264
265Parser& Parser::operator=(Parser&& from)
266{
267 m_bytes_buffer = move(from.m_bytes_buffer);
268 m_bytes = move(from.m_bytes);
269 m_revision = from.m_revision;
270 return *this;
271}
272
273Parser& Parser::operator=(Parser const& other)
274{
275 if (this == &other)
276 return *this;
277
278 m_bytes_buffer = other.m_bytes_buffer;
279 if (m_bytes_buffer.is_empty())
280 m_bytes = other.m_bytes_buffer; // We don't own the buffer
281 else
282 m_bytes = m_bytes_buffer; // We own the buffer
283 m_revision = other.m_revision;
284 return *this;
285}
286
287bool Parser::operator==(Parser const& other) const
288{
289 if (this == &other)
290 return true;
291 return m_bytes == other.m_bytes;
292}
293
294Definitions::EDID const& Parser::raw_edid() const
295{
296 return *(Definitions::EDID const*)m_bytes.data();
297}
298
299ErrorOr<void> Parser::parse()
300{
301 if (m_bytes.size() < sizeof(Definitions::EDID))
302 return Error::from_string_view_or_print_error_and_return_errno("Incomplete Parser structure"sv, EINVAL);
303
304 auto const& edid = raw_edid();
305 u64 header = read_le(&edid.header);
306 if (header != 0x00ffffffffffff00ull)
307 return Error::from_string_view_or_print_error_and_return_errno("No Parser header"sv, EINVAL);
308
309 u8 major_version = read_host(&edid.version.version);
310 m_revision = read_host(&edid.version.revision);
311 if (major_version != 1 || m_revision > 4)
312 return Error::from_string_view_or_print_error_and_return_errno("Unsupported Parser version"sv, EINVAL);
313
314#ifdef KERNEL
315 m_version = TRY(Kernel::KString::formatted("1.{}", (int)m_revision));
316#else
317 m_version = DeprecatedString::formatted("1.{}", (int)m_revision);
318#endif
319
320 u8 checksum = 0x0;
321 for (size_t i = 0; i < sizeof(Definitions::EDID); i++)
322 checksum += m_bytes[i];
323
324 if (checksum != 0) {
325 if (m_revision >= 4)
326 return Error::from_string_view_or_print_error_and_return_errno("Parser checksum mismatch"sv, EINVAL);
327 else
328 dbgln("EDID checksum mismatch, data may be corrupted!");
329 }
330
331 u16 packed_id = read_be(&raw_edid().vendor.manufacturer_id);
332 if (packed_id == 0x0)
333 return {};
334 m_legacy_manufacturer_id[0] = (char)((u16)'A' + ((packed_id >> 10) & 0x1f) - 1);
335 m_legacy_manufacturer_id[1] = (char)((u16)'A' + ((packed_id >> 5) & 0x1f) - 1);
336 m_legacy_manufacturer_id[2] = (char)((u16)'A' + (packed_id & 0x1f) - 1);
337 m_legacy_manufacturer_id[3] = '\0';
338 m_legacy_manufacturer_id_valid = true;
339
340 return {};
341}
342
343ErrorOr<IterationDecision> Parser::for_each_extension_block(Function<IterationDecision(unsigned, u8, u8, ReadonlyBytes)> callback) const
344{
345 auto& edid = raw_edid();
346 u8 raw_extension_block_count = read_host(&edid.extension_block_count);
347 if (raw_extension_block_count == 0)
348 return IterationDecision::Continue;
349 if (sizeof(Definitions::EDID) + (size_t)raw_extension_block_count * sizeof(Definitions::ExtensionBlock) > m_bytes.size())
350 return Error::from_string_view_or_print_error_and_return_errno("Truncated EDID"sv, EINVAL);
351
352 auto validate_block_checksum = [&](Definitions::ExtensionBlock const& block) {
353 u8 checksum = 0x0;
354 auto* bytes = (u8 const*)█
355 for (size_t i = 0; i < sizeof(block); i++)
356 checksum += bytes[i];
357
358 return checksum == 0;
359 };
360
361 auto* raw_extension_blocks = (Definitions::ExtensionBlock const*)(m_bytes.data() + sizeof(Definitions::EDID));
362 Definitions::ExtensionBlock const* current_extension_map = nullptr;
363
364 unsigned raw_index = 0;
365 if (m_revision <= 3) {
366 if (raw_extension_block_count > 1) {
367 current_extension_map = &raw_extension_blocks[0];
368 raw_index++;
369 if (read_host(¤t_extension_map->tag) != (u8)Definitions::ExtensionBlockTag::ExtensionBlockMap)
370 return Error::from_string_view_or_print_error_and_return_errno("Did not find extension map at block 1"sv, EINVAL);
371
372 if (!validate_block_checksum(*current_extension_map))
373 return Error::from_string_view_or_print_error_and_return_errno("Extension block map checksum mismatch"sv, EINVAL);
374 }
375 } else if (read_host(&raw_extension_blocks[0].tag) == (u8)Definitions::ExtensionBlockTag::ExtensionBlockMap) {
376 current_extension_map = &raw_extension_blocks[0];
377 raw_index++;
378 }
379
380 for (; raw_index < raw_extension_block_count; raw_index++) {
381 auto& raw_block = raw_extension_blocks[raw_index];
382 u8 tag = read_host(&raw_block.tag);
383
384 if (current_extension_map && raw_index == 127) {
385 if (tag != (u8)Definitions::ExtensionBlockTag::ExtensionBlockMap)
386 return Error::from_string_view_or_print_error_and_return_errno("Did not find extension map at block 128"sv, EINVAL);
387
388 current_extension_map = &raw_extension_blocks[127];
389 if (!validate_block_checksum(*current_extension_map))
390 return Error::from_string_view_or_print_error_and_return_errno("Extension block map checksum mismatch"sv, EINVAL);
391 continue;
392 }
393
394 if (tag == (u8)Definitions::ExtensionBlockTag::ExtensionBlockMap)
395 return Error::from_string_view_or_print_error_and_return_errno("Unexpected extension map encountered"sv, EINVAL);
396
397 if (!validate_block_checksum(raw_block))
398 return Error::from_string_view_or_print_error_and_return_errno("Extension block checksum mismatch"sv, EINVAL);
399
400 size_t offset = (u8 const*)&raw_block - m_bytes.data();
401 IterationDecision decision = callback(raw_index + 1, tag, raw_block.block.revision, m_bytes.slice(offset, sizeof(Definitions::ExtensionBlock)));
402 if (decision != IterationDecision::Continue)
403 return decision;
404 }
405 return IterationDecision::Continue;
406}
407
408StringView Parser::version() const
409{
410#ifdef KERNEL
411 return m_version->view();
412#else
413 return m_version;
414#endif
415}
416
417StringView Parser::legacy_manufacturer_id() const
418{
419 return { m_legacy_manufacturer_id, strlen(m_legacy_manufacturer_id) };
420}
421
422#ifndef KERNEL
423DeprecatedString Parser::manufacturer_name() const
424{
425 if (!m_legacy_manufacturer_id_valid)
426 return "Unknown";
427 auto manufacturer_id = legacy_manufacturer_id();
428# ifdef ENABLE_PNP_IDS_DATA
429 if (auto pnp_id_data = PnpIDs::find_by_manufacturer_id(manufacturer_id); pnp_id_data.has_value())
430 return pnp_id_data.value().manufacturer_name;
431# endif
432 return manufacturer_id;
433}
434#endif
435
436u16 Parser::product_code() const
437{
438 return read_le(&raw_edid().vendor.product_code);
439}
440
441u32 Parser::serial_number() const
442{
443 return read_le(&raw_edid().vendor.serial_number);
444}
445
446auto Parser::digital_display() const -> Optional<DigitalDisplay>
447{
448 auto& edid = raw_edid();
449 u8 video_input_definition = read_host(&edid.basic_display_parameters.video_input_definition);
450 if (!(video_input_definition & 0x80))
451 return {}; // This is an analog display
452
453 u8 feature_support = read_host(&edid.basic_display_parameters.feature_support);
454 return DigitalDisplay(video_input_definition, feature_support, m_revision);
455}
456
457auto Parser::analog_display() const -> Optional<AnalogDisplay>
458{
459 auto& edid = raw_edid();
460 u8 video_input_definition = read_host(&edid.basic_display_parameters.video_input_definition);
461 if ((video_input_definition & 0x80) != 0)
462 return {}; // This is a digital display
463
464 u8 feature_support = read_host(&edid.basic_display_parameters.feature_support);
465 return AnalogDisplay(video_input_definition, feature_support, m_revision);
466}
467
468auto Parser::screen_size() const -> Optional<ScreenSize>
469{
470 auto& edid = raw_edid();
471 u8 horizontal_size_or_aspect_ratio = read_host(&edid.basic_display_parameters.horizontal_size_or_aspect_ratio);
472 u8 vertical_size_or_aspect_ratio = read_host(&edid.basic_display_parameters.vertical_size_or_aspect_ratio);
473
474 if (horizontal_size_or_aspect_ratio == 0 || vertical_size_or_aspect_ratio == 0) {
475 // EDID < 1.4: Unknown or undefined
476 // EDID >= 1.4: If both are 0 it is unknown or undefined
477 // If one of them is 0 then we're dealing with aspect ratios
478 return {};
479 }
480
481 return ScreenSize(horizontal_size_or_aspect_ratio, vertical_size_or_aspect_ratio);
482}
483
484auto Parser::aspect_ratio() const -> Optional<ScreenAspectRatio>
485{
486 if (m_revision < 4)
487 return {};
488
489 auto& edid = raw_edid();
490 u8 value_1 = read_host(&edid.basic_display_parameters.horizontal_size_or_aspect_ratio);
491 u8 value_2 = read_host(&edid.basic_display_parameters.vertical_size_or_aspect_ratio);
492
493 if (value_1 == 0 && value_2 == 0)
494 return {}; // Unknown or undefined
495 if (value_1 != 0 && value_2 != 0)
496 return {}; // Dimensions are in cm
497
498 if (value_1 == 0)
499 return ScreenAspectRatio(ScreenAspectRatio::Orientation::Portrait, FixedPoint<16>(100) / FixedPoint<16>((i32)value_2 + 99));
500
501 VERIFY(value_2 == 0);
502 return ScreenAspectRatio(ScreenAspectRatio::Orientation::Landscape, FixedPoint<16>((i32)value_1 + 99) / 100);
503}
504
505Optional<FixedPoint<16>> Parser::gamma() const
506{
507 u8 display_transfer_characteristics = read_host(&raw_edid().basic_display_parameters.display_transfer_characteristics);
508 if (display_transfer_characteristics == 0xff) {
509 if (m_revision < 4)
510 return {};
511
512 // TODO: EDID >= 1.4 stores more gamma details in an extension block (e.g. DI-EXT)
513 return {};
514 }
515
516 FixedPoint<16> gamma { (i32)display_transfer_characteristics + 100 };
517 gamma /= 100;
518 return gamma;
519}
520
521u32 Parser::DetailedTiming::pixel_clock_khz() const
522{
523 // Note: The stored value is in units of 10 kHz, which means that to get the
524 // value in kHz, we need to multiply it by 10.
525 return (u32)m_edid.read_le(&m_detailed_timings.pixel_clock) * 10;
526}
527
528u16 Parser::DetailedTiming::horizontal_addressable_pixels() const
529{
530 u8 low = m_edid.read_host(&m_detailed_timings.horizontal_addressable_pixels_low);
531 u8 high = m_edid.read_host(&m_detailed_timings.horizontal_addressable_and_blanking_pixels_high) >> 4;
532 return ((u16)high << 8) | (u16)low;
533}
534
535u16 Parser::DetailedTiming::horizontal_blanking_pixels() const
536{
537 u8 low = m_edid.read_host(&m_detailed_timings.horizontal_blanking_pixels_low);
538 u8 high = m_edid.read_host(&m_detailed_timings.horizontal_addressable_and_blanking_pixels_high) & 0xf;
539 return ((u16)high << 8) | (u16)low;
540}
541
542u16 Parser::DetailedTiming::vertical_addressable_lines_raw() const
543{
544 u8 low = m_edid.read_host(&m_detailed_timings.vertical_addressable_lines_low);
545 u8 high = m_edid.read_host(&m_detailed_timings.vertical_addressable_and_blanking_lines_high) >> 4;
546 return ((u16)high << 8) | (u16)low;
547}
548
549u16 Parser::DetailedTiming::vertical_addressable_lines() const
550{
551 auto lines = vertical_addressable_lines_raw();
552 return is_interlaced() ? lines * 2 : lines;
553}
554
555u16 Parser::DetailedTiming::vertical_blanking_lines() const
556{
557 u8 low = m_edid.read_host(&m_detailed_timings.vertical_blanking_lines_low);
558 u8 high = m_edid.read_host(&m_detailed_timings.vertical_addressable_and_blanking_lines_high) & 0xf;
559 return ((u16)high << 8) | (u16)low;
560}
561
562u16 Parser::DetailedTiming::horizontal_front_porch_pixels() const
563{
564 u8 low = m_edid.read_host(&m_detailed_timings.horizontal_front_porch_pixels_low);
565 u8 high = m_edid.read_host(&m_detailed_timings.horizontal_and_vertical_front_porch_sync_pulse_width_high) >> 6;
566 return ((u16)high << 8) | (u16)low;
567}
568
569u16 Parser::DetailedTiming::horizontal_sync_pulse_width_pixels() const
570{
571 u8 low = m_edid.read_host(&m_detailed_timings.horizontal_sync_pulse_width_pixels_low);
572 u8 high = (m_edid.read_host(&m_detailed_timings.horizontal_and_vertical_front_porch_sync_pulse_width_high) >> 4) & 3;
573 return ((u16)high << 8) | (u16)low;
574}
575
576u16 Parser::DetailedTiming::vertical_front_porch_lines() const
577{
578 u8 low = m_edid.read_host(&m_detailed_timings.vertical_front_porch_and_sync_pulse_width_lines_low) >> 4;
579 u8 high = (m_edid.read_host(&m_detailed_timings.horizontal_and_vertical_front_porch_sync_pulse_width_high) >> 2) & 3;
580 return ((u16)high << 4) | (u16)low;
581}
582
583u16 Parser::DetailedTiming::vertical_sync_pulse_width_lines() const
584{
585 u8 low = m_edid.read_host(&m_detailed_timings.vertical_front_porch_and_sync_pulse_width_lines_low) & 0xf;
586 u8 high = m_edid.read_host(&m_detailed_timings.horizontal_and_vertical_front_porch_sync_pulse_width_high) & 3;
587 return ((u16)high << 4) | (u16)low;
588}
589
590u16 Parser::DetailedTiming::horizontal_image_size_mm() const
591{
592 u8 low = m_edid.read_host(&m_detailed_timings.horizontal_addressable_image_size_mm_low);
593 u8 high = m_edid.read_host(&m_detailed_timings.horizontal_vertical_addressable_image_size_mm_high) >> 4;
594 return ((u16)high << 8) | (u16)low;
595}
596
597u16 Parser::DetailedTiming::vertical_image_size_mm() const
598{
599 u8 low = m_edid.read_host(&m_detailed_timings.vertical_addressable_image_size_mm_low);
600 u8 high = m_edid.read_host(&m_detailed_timings.horizontal_vertical_addressable_image_size_mm_high) & 0xf;
601 return ((u16)high << 8) | (u16)low;
602}
603
604u8 Parser::DetailedTiming::horizontal_right_or_left_border_pixels() const
605{
606 return m_edid.read_host(&m_detailed_timings.right_or_left_horizontal_border_pixels);
607}
608
609u8 Parser::DetailedTiming::vertical_top_or_bottom_border_lines() const
610{
611 return m_edid.read_host(&m_detailed_timings.top_or_bottom_vertical_border_lines);
612}
613
614bool Parser::DetailedTiming::is_interlaced() const
615{
616 return (m_edid.read_host(&m_detailed_timings.features) & (1 << 7)) != 0;
617}
618
619FixedPoint<16, u32> Parser::DetailedTiming::refresh_rate() const
620{
621 // Blanking = front porch + sync pulse width + back porch
622 u32 total_horizontal_pixels = (u32)horizontal_addressable_pixels() + (u32)horizontal_blanking_pixels();
623 u32 total_vertical_lines = (u32)vertical_addressable_lines_raw() + (u32)vertical_blanking_lines();
624 u32 total_pixels = total_horizontal_pixels * total_vertical_lines;
625 if (total_pixels == 0)
626 return {};
627 // Use a bigger fixed point representation due to the large numbers involved and then downcast
628 // Note: We need to convert the pixel clock from kHz to Hertz to actually calculate this correctly.
629 return FixedPoint<32, u64>(pixel_clock_khz() * 1000) / total_pixels;
630}
631
632ErrorOr<IterationDecision> Parser::for_each_established_timing(Function<IterationDecision(EstablishedTiming const&)> callback) const
633{
634 static constexpr EstablishedTiming established_timing_byte1[8] = {
635 { EstablishedTiming::Source::VESA, 800, 600, 60, 0x9 },
636 { EstablishedTiming::Source::VESA, 800, 600, 56, 0x8 },
637 { EstablishedTiming::Source::VESA, 640, 480, 75, 0x6 },
638 { EstablishedTiming::Source::VESA, 640, 480, 73, 0x5 },
639 { EstablishedTiming::Source::Apple, 640, 480, 67 },
640 { EstablishedTiming::Source::IBM, 640, 480, 60, 0x4 },
641 { EstablishedTiming::Source::IBM, 720, 400, 88 },
642 { EstablishedTiming::Source::IBM, 720, 400, 70 }
643 };
644 static constexpr EstablishedTiming established_timing_byte2[8] = {
645 { EstablishedTiming::Source::VESA, 1280, 1024, 75, 0x24 },
646 { EstablishedTiming::Source::VESA, 1024, 768, 75, 0x12 },
647 { EstablishedTiming::Source::VESA, 1024, 768, 70, 0x11 },
648 { EstablishedTiming::Source::VESA, 1024, 768, 60, 0x10 },
649 { EstablishedTiming::Source::IBM, 1024, 768, 87, 0xf },
650 { EstablishedTiming::Source::Apple, 832, 624, 75 },
651 { EstablishedTiming::Source::VESA, 800, 600, 75, 0xb },
652 { EstablishedTiming::Source::VESA, 800, 600, 72, 0xa }
653 };
654 static constexpr EstablishedTiming established_timing_byte3[1] = {
655 { EstablishedTiming::Source::Apple, 1152, 870, 75 }
656 };
657
658 auto& established_timings = raw_edid().established_timings;
659 for (int i = 7; i >= 0; i--) {
660 if (!(established_timings.timings_1 & (1 << i)))
661 continue;
662 IterationDecision decision = callback(established_timing_byte1[i]);
663 if (decision != IterationDecision::Continue)
664 return decision;
665 }
666 for (int i = 7; i >= 0; i--) {
667 if (!(established_timings.timings_2 & (1 << i)))
668 continue;
669 IterationDecision decision = callback(established_timing_byte2[i]);
670 if (decision != IterationDecision::Continue)
671 return decision;
672 }
673
674 if ((established_timings.manufacturer_reserved & (1 << 7)) != 0) {
675 IterationDecision decision = callback(established_timing_byte3[0]);
676 if (decision != IterationDecision::Continue)
677 return decision;
678 }
679
680 u8 manufacturer_specific = established_timings.manufacturer_reserved & 0x7f;
681 if (manufacturer_specific != 0) {
682 IterationDecision decision = callback(EstablishedTiming(EstablishedTiming::Source::Manufacturer, 0, 0, manufacturer_specific));
683 if (decision != IterationDecision::Continue)
684 return decision;
685 }
686
687 auto callback_decision = IterationDecision::Continue;
688 TRY(for_each_display_descriptor([&](u8 descriptor_tag, auto& display_descriptor) {
689 if (descriptor_tag != (u8)Definitions::DisplayDescriptorTag::EstablishedTimings3)
690 return IterationDecision::Continue;
691
692 static constexpr EstablishedTiming established_timings3_bytes[] = {
693 // Byte 1
694 { EstablishedTiming::Source::VESA, 640, 350, 85, 0x1 },
695 { EstablishedTiming::Source::VESA, 640, 400, 85, 0x2 },
696 { EstablishedTiming::Source::VESA, 720, 400, 85, 0x3 },
697 { EstablishedTiming::Source::VESA, 640, 480, 85, 0x7 },
698 { EstablishedTiming::Source::VESA, 848, 480, 60, 0xe },
699 { EstablishedTiming::Source::VESA, 800, 600, 85, 0xc },
700 { EstablishedTiming::Source::VESA, 1024, 768, 85, 0x13 },
701 { EstablishedTiming::Source::VESA, 1152, 864, 75, 0x15 },
702 // Byte 2
703 { EstablishedTiming::Source::VESA, 1280, 768, 60, 0x16 },
704 { EstablishedTiming::Source::VESA, 1280, 768, 60, 0x17 },
705 { EstablishedTiming::Source::VESA, 1280, 768, 75, 0x18 },
706 { EstablishedTiming::Source::VESA, 1280, 768, 85, 0x19 },
707 { EstablishedTiming::Source::VESA, 1280, 960, 60, 0x20 },
708 { EstablishedTiming::Source::VESA, 1280, 960, 85, 0x21 },
709 { EstablishedTiming::Source::VESA, 1280, 1024, 60, 0x23 },
710 { EstablishedTiming::Source::VESA, 1280, 1024, 85, 0x25 },
711 // Byte 3
712 { EstablishedTiming::Source::VESA, 1360, 768, 60, 0x27 },
713 { EstablishedTiming::Source::VESA, 1440, 900, 60, 0x2e },
714 { EstablishedTiming::Source::VESA, 1440, 900, 60, 0x2f },
715 { EstablishedTiming::Source::VESA, 1440, 900, 75, 0x30 },
716 { EstablishedTiming::Source::VESA, 1440, 900, 85, 0x31 },
717 { EstablishedTiming::Source::VESA, 1400, 1050, 60, 0x29 },
718 { EstablishedTiming::Source::VESA, 1400, 1050, 60, 0x2a },
719 { EstablishedTiming::Source::VESA, 1400, 1050, 75, 0x2b },
720 // Byte 4
721 { EstablishedTiming::Source::VESA, 1400, 1050, 85, 0x2c },
722 { EstablishedTiming::Source::VESA, 1680, 1050, 60, 0x39 },
723 { EstablishedTiming::Source::VESA, 1680, 1050, 60, 0x3a },
724 { EstablishedTiming::Source::VESA, 1680, 1050, 75, 0x3b },
725 { EstablishedTiming::Source::VESA, 1680, 1050, 85, 0x3c },
726 { EstablishedTiming::Source::VESA, 1600, 1200, 60, 0x33 },
727 { EstablishedTiming::Source::VESA, 1600, 1200, 65, 0x34 },
728 { EstablishedTiming::Source::VESA, 1600, 1200, 70, 0x35 },
729 // Byte 5
730 { EstablishedTiming::Source::VESA, 1600, 1200, 75, 0x36 },
731 { EstablishedTiming::Source::VESA, 1600, 1200, 85, 0x37 },
732 { EstablishedTiming::Source::VESA, 1792, 1344, 60, 0x3e },
733 { EstablishedTiming::Source::VESA, 1792, 1344, 75, 0x3f },
734 { EstablishedTiming::Source::VESA, 1856, 1392, 60, 0x41 },
735 { EstablishedTiming::Source::VESA, 1856, 1392, 75, 0x42 },
736 { EstablishedTiming::Source::VESA, 1920, 1200, 60, 0x44 },
737 { EstablishedTiming::Source::VESA, 1920, 1200, 60, 0x45 },
738 // Byte 6
739 { EstablishedTiming::Source::VESA, 1920, 1200, 75, 0x46 },
740 { EstablishedTiming::Source::VESA, 1920, 1200, 85, 0x47 },
741 { EstablishedTiming::Source::VESA, 1920, 1440, 60, 0x49 },
742 { EstablishedTiming::Source::VESA, 1920, 1440, 75, 0x4a }
743 // Reserved
744 };
745
746 size_t byte_index = 0;
747 for (u8 dmt_bits : display_descriptor.established_timings3.dmt_bits) {
748 for (int i = 7; i >= 0; i--) {
749 if ((dmt_bits & (1 << i)) == 0)
750 continue;
751
752 size_t table_index = byte_index * 8 + (size_t)(7 - i);
753 if (table_index >= (sizeof(established_timings3_bytes) + 7) / sizeof(established_timings3_bytes[0]))
754 break; // Sometimes reserved bits are set
755
756 callback_decision = callback(established_timings3_bytes[table_index]);
757 if (callback_decision != IterationDecision::Continue)
758 return IterationDecision::Break;
759 }
760 byte_index++;
761 }
762 return IterationDecision::Break; // Only process one descriptor
763 }));
764 return callback_decision;
765}
766
767ErrorOr<IterationDecision> Parser::for_each_standard_timing(Function<IterationDecision(StandardTiming const&)> callback) const
768{
769 for (size_t index = 0; index < 8; index++) {
770 auto& standard_timings = raw_edid().standard_timings[index];
771 if (standard_timings.horizontal_8_pixels == 0x1 && standard_timings.ratio_and_refresh_rate == 0x1)
772 continue; // Skip unused records
773 u16 width = 8 * ((u16)read_host(&standard_timings.horizontal_8_pixels) + 31);
774 u8 aspect_ratio_and_refresh_rate = read_host(&standard_timings.ratio_and_refresh_rate);
775 u8 refresh_rate = (aspect_ratio_and_refresh_rate & 0x3f) + 60;
776 u16 height;
777 StandardTiming::AspectRatio aspect_ratio;
778 switch ((aspect_ratio_and_refresh_rate >> 6) & 3) {
779 case 0:
780 height = (width * 10) / 16;
781 aspect_ratio = StandardTiming::AspectRatio::AR_16_10;
782 break;
783 case 1:
784 height = (width * 3) / 4;
785 aspect_ratio = StandardTiming::AspectRatio::AR_4_3;
786 break;
787 case 2:
788 height = (width * 4) / 5;
789 aspect_ratio = StandardTiming::AspectRatio::AR_5_4;
790 break;
791 case 3:
792 height = (width * 9) / 16;
793 aspect_ratio = StandardTiming::AspectRatio::AR_16_9;
794 break;
795 default:
796 VERIFY_NOT_REACHED();
797 }
798
799 auto* dmt = DMT::find_timing_by_std_id(standard_timings.horizontal_8_pixels, standard_timings.ratio_and_refresh_rate);
800 IterationDecision decision = callback(StandardTiming(width, height, refresh_rate, aspect_ratio, dmt ? dmt->dmt_id : 0));
801 if (decision != IterationDecision::Continue)
802 return decision;
803 }
804
805 return IterationDecision::Continue;
806}
807
808u16 Parser::CoordinatedVideoTiming::horizontal_addressable_pixels() const
809{
810 u32 aspect_h, aspect_v;
811 switch (aspect_ratio()) {
812 case AspectRatio::AR_4_3:
813 aspect_h = 4;
814 aspect_v = 3;
815 break;
816 case AspectRatio::AR_16_9:
817 aspect_h = 16;
818 aspect_v = 9;
819 break;
820 case AspectRatio::AR_16_10:
821 aspect_h = 16;
822 aspect_v = 10;
823 break;
824 case AspectRatio::AR_15_9:
825 aspect_h = 15;
826 aspect_v = 9;
827 break;
828 }
829 // Round down to nearest cell as per 3.10.3.8
830 return (u16)(8 * ((((u32)vertical_addressable_lines() * aspect_h) / aspect_v) / 8));
831}
832
833u16 Parser::CoordinatedVideoTiming::vertical_addressable_lines() const
834{
835 return ((u16)(m_cvt.bytes[1] >> 4) << 8) | (u16)m_cvt.bytes[0];
836}
837
838auto Parser::CoordinatedVideoTiming::aspect_ratio() const -> AspectRatio
839{
840 return (AspectRatio)((m_cvt.bytes[2] >> 2) & 0x3);
841}
842
843u16 Parser::CoordinatedVideoTiming::preferred_refresh_rate()
844{
845 switch ((m_cvt.bytes[2] >> 5) & 3) {
846 case 0:
847 return 50;
848 case 1:
849 return 60;
850 case 2:
851 return 75;
852 case 3:
853 return 85;
854 default:
855 VERIFY_NOT_REACHED();
856 }
857}
858
859ErrorOr<IterationDecision> Parser::for_each_coordinated_video_timing(Function<IterationDecision(CoordinatedVideoTiming const&)> callback) const
860{
861 return for_each_display_descriptor([&](u8 descriptor_tag, Definitions::DisplayDescriptor const& display_descriptor) {
862 if (descriptor_tag != (u8)Definitions::DisplayDescriptorTag::CVTTimingCodes)
863 return IterationDecision::Continue;
864 u8 version = read_host(&display_descriptor.coordinated_video_timings.version);
865 if (version != 1) {
866 dbgln("Unsupported CVT display descriptor version: {}", version);
867 return IterationDecision::Continue;
868 }
869
870 for (size_t i = 0; i < 4; i++) {
871 const DMT::CVT cvt {
872 {
873 read_host(&display_descriptor.coordinated_video_timings.cvt[i][0]),
874 read_host(&display_descriptor.coordinated_video_timings.cvt[i][1]),
875 read_host(&display_descriptor.coordinated_video_timings.cvt[i][2]),
876 }
877 };
878 if (cvt.bytes[0] == 0 && cvt.bytes[1] == 0 && cvt.bytes[2] == 0)
879 continue;
880
881 IterationDecision decision = callback(CoordinatedVideoTiming(cvt));
882 if (decision != IterationDecision::Continue)
883 return decision;
884 }
885 return IterationDecision::Continue;
886 });
887}
888
889ErrorOr<IterationDecision> Parser::for_each_detailed_timing(Function<IterationDecision(DetailedTiming const&, unsigned)> callback) const
890{
891 auto& edid = raw_edid();
892 for (size_t raw_index = 0; raw_index < 4; raw_index++) {
893 if (raw_index == 0 || read_le(&edid.detailed_timing_or_display_descriptors[raw_index].detailed_timing.pixel_clock) != 0) {
894 IterationDecision decision = callback(DetailedTiming(*this, &edid.detailed_timing_or_display_descriptors[raw_index].detailed_timing), 0);
895 if (decision != IterationDecision::Continue)
896 return decision;
897 }
898 }
899
900 Optional<Error> extension_error;
901 auto result = TRY(for_each_extension_block([&](u8 block_id, u8 tag, u8, ReadonlyBytes bytes) {
902 if (tag != (u8)Definitions::ExtensionBlockTag::CEA_861)
903 return IterationDecision::Continue;
904
905 CEA861ExtensionBlock cea861(*this, (Definitions::ExtensionBlock const*)bytes.data());
906 auto result = cea861.for_each_dtd([&](auto& dtd) {
907 return callback(dtd, block_id);
908 });
909 if (result.is_error()) {
910 dbgln("Failed to iterate DTDs in CEA861 extension block: {}", result.error());
911 extension_error = result.release_error();
912 return IterationDecision::Break;
913 }
914
915 return result.value();
916 }));
917 if (extension_error.has_value())
918 return extension_error.release_value();
919 return result;
920}
921
922auto Parser::detailed_timing(size_t index) const -> Optional<DetailedTiming>
923{
924 Optional<DetailedTiming> found_dtd;
925 auto result = for_each_detailed_timing([&](DetailedTiming const& dtd, unsigned) {
926 if (index == 0) {
927 found_dtd = dtd;
928 return IterationDecision::Break;
929 }
930 index--;
931 return IterationDecision::Continue;
932 });
933 if (result.is_error()) {
934 dbgln("Error getting Parser detailed timing #{}: {}", index, result.error());
935 return {};
936 }
937 return found_dtd;
938}
939
940ErrorOr<IterationDecision> Parser::for_each_short_video_descriptor(Function<IterationDecision(unsigned, bool, VIC::Details const&)> callback) const
941{
942 Optional<Error> extension_error;
943 auto result = for_each_extension_block([&](u8 block_id, u8 tag, u8, ReadonlyBytes bytes) {
944 if (tag != (u8)Definitions::ExtensionBlockTag::CEA_861)
945 return IterationDecision::Continue;
946
947 CEA861ExtensionBlock cea861(*this, (Definitions::ExtensionBlock const*)bytes.data());
948 auto result = cea861.for_each_short_video_descriptor([&](bool is_native, VIC::Details const& vic) {
949 return callback(block_id, is_native, vic);
950 });
951 if (result.is_error()) {
952 extension_error = result.release_error();
953 return IterationDecision::Break;
954 }
955 return result.value();
956 });
957 if (result.is_error()) {
958 dbgln("Failed to iterate Parser extension blocks: {}", result.error());
959 return IterationDecision::Break;
960 }
961 return result.value();
962}
963
964ErrorOr<IterationDecision> Parser::for_each_display_descriptor(Function<IterationDecision(u8, Definitions::DisplayDescriptor const&)> callback) const
965{
966 auto& edid = raw_edid();
967 for (size_t raw_index = 1; raw_index < 4; raw_index++) {
968 auto& display_descriptor = edid.detailed_timing_or_display_descriptors[raw_index].display_descriptor;
969 if (read_le(&display_descriptor.zero) != 0 || read_host(&display_descriptor.reserved1) != 0)
970 continue;
971
972 u8 tag = read_host(&display_descriptor.tag);
973 IterationDecision decision = callback(tag, display_descriptor);
974 if (decision != IterationDecision::Continue)
975 return decision;
976 }
977
978 Optional<Error> extension_error;
979 auto result = TRY(for_each_extension_block([&](u8, u8 tag, u8, ReadonlyBytes bytes) {
980 if (tag != (u8)Definitions::ExtensionBlockTag::CEA_861)
981 return IterationDecision::Continue;
982
983 CEA861ExtensionBlock cea861(*this, (Definitions::ExtensionBlock const*)bytes.data());
984 auto result = cea861.for_each_display_descriptor([&](u8 tag, auto& display_descriptor) {
985 return callback(tag, display_descriptor);
986 });
987 if (result.is_error()) {
988 dbgln("Failed to iterate display descriptors in CEA861 extension block: {}", result.error());
989 extension_error = result.release_error();
990 return IterationDecision::Break;
991 }
992
993 return result.value();
994 }));
995 if (extension_error.has_value())
996 return extension_error.release_value();
997 return result;
998}
999
1000#ifndef KERNEL
1001DeprecatedString Parser::display_product_name() const
1002{
1003 DeprecatedString product_name;
1004 auto result = for_each_display_descriptor([&](u8 descriptor_tag, Definitions::DisplayDescriptor const& display_descriptor) {
1005 if (descriptor_tag != (u8)Definitions::DisplayDescriptorTag::DisplayProductName)
1006 return IterationDecision::Continue;
1007
1008 StringBuilder str;
1009 for (u8 byte : display_descriptor.display_product_name.ascii_name) {
1010 if (byte == 0xa)
1011 break;
1012 str.append((char)byte);
1013 }
1014 product_name = str.to_deprecated_string();
1015 return IterationDecision::Break;
1016 });
1017 if (result.is_error()) {
1018 dbgln("Failed to locate product name display descriptor: {}", result.error());
1019 return {};
1020 }
1021 return product_name;
1022}
1023
1024DeprecatedString Parser::display_product_serial_number() const
1025{
1026 DeprecatedString product_name;
1027 auto result = for_each_display_descriptor([&](u8 descriptor_tag, Definitions::DisplayDescriptor const& display_descriptor) {
1028 if (descriptor_tag != (u8)Definitions::DisplayDescriptorTag::DisplayProductSerialNumber)
1029 return IterationDecision::Continue;
1030
1031 StringBuilder str;
1032 for (u8 byte : display_descriptor.display_product_serial_number.ascii_str) {
1033 if (byte == 0xa)
1034 break;
1035 str.append((char)byte);
1036 }
1037 product_name = str.to_deprecated_string();
1038 return IterationDecision::Break;
1039 });
1040 if (result.is_error()) {
1041 dbgln("Failed to locate product name display descriptor: {}", result.error());
1042 return {};
1043 }
1044 return product_name;
1045}
1046#endif
1047
1048auto Parser::supported_resolutions() const -> ErrorOr<Vector<SupportedResolution>>
1049{
1050 Vector<SupportedResolution> resolutions;
1051
1052 auto add_resolution = [&](unsigned width, unsigned height, FixedPoint<16, u32> refresh_rate, bool preferred = false) {
1053 auto it = resolutions.find_if([&](auto& info) {
1054 return info.width == width && info.height == height;
1055 });
1056 if (it == resolutions.end()) {
1057 resolutions.append({ width, height, { { refresh_rate, preferred } } });
1058 } else {
1059 auto& info = *it;
1060 SupportedResolution::RefreshRate* found_refresh_rate = nullptr;
1061 for (auto& supported_refresh_rate : info.refresh_rates) {
1062 if (supported_refresh_rate.rate == refresh_rate) {
1063 found_refresh_rate = &supported_refresh_rate;
1064 break;
1065 }
1066 }
1067 if (found_refresh_rate)
1068 found_refresh_rate->preferred |= preferred;
1069 else
1070 info.refresh_rates.append({ refresh_rate, preferred });
1071 }
1072 };
1073
1074 TRY(for_each_established_timing([&](auto& established_timing) {
1075 if (established_timing.source() != EstablishedTiming::Source::Manufacturer)
1076 add_resolution(established_timing.width(), established_timing.height(), established_timing.refresh_rate());
1077 return IterationDecision::Continue;
1078 }));
1079
1080 TRY(for_each_standard_timing([&](auto& standard_timing) {
1081 add_resolution(standard_timing.width(), standard_timing.height(), standard_timing.refresh_rate());
1082 return IterationDecision::Continue;
1083 }));
1084
1085 size_t detailed_timing_index = 0;
1086 TRY(for_each_detailed_timing([&](auto& detailed_timing, auto) {
1087 bool is_preferred = detailed_timing_index++ == 0;
1088 add_resolution(detailed_timing.horizontal_addressable_pixels(), detailed_timing.vertical_addressable_lines(), detailed_timing.refresh_rate(), is_preferred);
1089 return IterationDecision::Continue;
1090 }));
1091
1092 TRY(for_each_short_video_descriptor([&](unsigned, bool, VIC::Details const& vic_details) {
1093 add_resolution(vic_details.horizontal_pixels, vic_details.vertical_lines, vic_details.refresh_rate_hz());
1094 return IterationDecision::Continue;
1095 }));
1096
1097 TRY(for_each_coordinated_video_timing([&](auto& coordinated_video_timing) {
1098 if (auto* dmt = DMT::find_timing_by_cvt(coordinated_video_timing.cvt_code())) {
1099 add_resolution(dmt->horizontal_pixels, dmt->vertical_lines, dmt->vertical_frequency_hz());
1100 } else {
1101 // TODO: We couldn't find this cvt code, try to decode it
1102 auto cvt = coordinated_video_timing.cvt_code();
1103 dbgln("TODO: Decode CVT code: {:02x},{:02x},{:02x}", cvt.bytes[0], cvt.bytes[1], cvt.bytes[2]);
1104 }
1105 return IterationDecision::Continue;
1106 }));
1107
1108 quick_sort(resolutions, [&](auto& info1, auto& info2) {
1109 if (info1.width < info2.width)
1110 return true;
1111 if (info1.width == info2.width && info1.height < info2.height)
1112 return true;
1113 return false;
1114 });
1115 for (auto& res : resolutions) {
1116 if (res.refresh_rates.size() > 1)
1117 quick_sort(res.refresh_rates);
1118 }
1119 return resolutions;
1120}
1121
1122}