diff --git a/src/common/include/display_device/detail/json_serializer.h b/src/common/include/display_device/detail/json_serializer.h index d7e5eeb..0ee26bc 100644 --- a/src/common/include/display_device/detail/json_serializer.h +++ b/src/common/include/display_device/detail/json_serializer.h @@ -17,6 +17,7 @@ namespace display_device { DD_JSON_DECLARE_SERIALIZE_TYPE(Resolution) DD_JSON_DECLARE_SERIALIZE_TYPE(Rational) DD_JSON_DECLARE_SERIALIZE_TYPE(Point) + DD_JSON_DECLARE_SERIALIZE_TYPE(EdidData) DD_JSON_DECLARE_SERIALIZE_TYPE(EnumeratedDevice::Info) DD_JSON_DECLARE_SERIALIZE_TYPE(EnumeratedDevice) DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfiguration) diff --git a/src/common/include/display_device/json.h b/src/common/include/display_device/json.h index 68609ab..de9a7a6 100644 --- a/src/common/include/display_device/json.h +++ b/src/common/include/display_device/json.h @@ -25,6 +25,7 @@ namespace display_device { extern const std::optional JSON_COMPACT; + DD_JSON_DECLARE_CONVERTER(EdidData) DD_JSON_DECLARE_CONVERTER(EnumeratedDevice) DD_JSON_DECLARE_CONVERTER(EnumeratedDeviceList) DD_JSON_DECLARE_CONVERTER(SingleDisplayConfiguration) diff --git a/src/common/include/display_device/types.h b/src/common/include/display_device/types.h index 456ee53..0c62b79 100644 --- a/src/common/include/display_device/types.h +++ b/src/common/include/display_device/types.h @@ -5,6 +5,7 @@ #pragma once // system includes +#include #include #include #include @@ -63,6 +64,27 @@ namespace display_device { */ using FloatingPoint = std::variant; + /** + * @brief Parsed EDID data. + */ + struct EdidData { + std::string m_manufacturer_id {}; + std::string m_product_code {}; + std::uint32_t m_serial_number {}; + + /** + * @brief Parse EDID data. + * @param data Data to parse. + * @return Parsed data or empty optional if failed to parse it. + */ + static std::optional parse(const std::vector &data); + + /** + * @brief Comparator for strict equality. + */ + friend bool operator==(const EdidData &lhs, const EdidData &rhs); + }; + /** * @brief Enumerated display device information. */ @@ -87,6 +109,7 @@ namespace display_device { std::string m_device_id {}; /**< A unique device ID used by this API to identify the device. */ std::string m_display_name {}; /**< A logical name representing given by the OS for a display. */ std::string m_friendly_name {}; /**< A human-readable name for the device. */ + std::optional m_edid {}; /**< Some basic parsed EDID data. */ std::optional m_info {}; /**< Additional information about an active display device. */ /** diff --git a/src/common/json.cpp b/src/common/json.cpp index b997510..de7abef 100644 --- a/src/common/json.cpp +++ b/src/common/json.cpp @@ -13,6 +13,7 @@ // clang-format on namespace display_device { + DD_JSON_DEFINE_CONVERTER(EdidData) DD_JSON_DEFINE_CONVERTER(EnumeratedDevice) DD_JSON_DEFINE_CONVERTER(EnumeratedDeviceList) DD_JSON_DEFINE_CONVERTER(SingleDisplayConfiguration) diff --git a/src/common/json_serializer.cpp b/src/common/json_serializer.cpp index 627c801..ff56800 100644 --- a/src/common/json_serializer.cpp +++ b/src/common/json_serializer.cpp @@ -18,7 +18,8 @@ namespace display_device { DD_JSON_DEFINE_SERIALIZE_STRUCT(Resolution, width, height) DD_JSON_DEFINE_SERIALIZE_STRUCT(Rational, numerator, denominator) DD_JSON_DEFINE_SERIALIZE_STRUCT(Point, x, y) + DD_JSON_DEFINE_SERIALIZE_STRUCT(EdidData, manufacturer_id, product_code, serial_number) DD_JSON_DEFINE_SERIALIZE_STRUCT(EnumeratedDevice::Info, resolution, resolution_scale, refresh_rate, primary, origin_point, hdr_state) - DD_JSON_DEFINE_SERIALIZE_STRUCT(EnumeratedDevice, device_id, display_name, friendly_name, info) + DD_JSON_DEFINE_SERIALIZE_STRUCT(EnumeratedDevice, device_id, display_name, friendly_name, edid, info) DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfiguration, device_id, device_prep, resolution, refresh_rate, hdr_state) } // namespace display_device diff --git a/src/common/types.cpp b/src/common/types.cpp index eb243b9..de5fdd8 100644 --- a/src/common/types.cpp +++ b/src/common/types.cpp @@ -5,6 +5,13 @@ // header include #include "display_device/types.h" +// system includes +#include +#include + +// local includes +#include "display_device/logging.h" + namespace { bool fuzzyCompare(const double lhs, const double rhs) { return std::abs(lhs - rhs) * 1000000000000. <= std::min(std::abs(lhs), std::abs(rhs)); @@ -19,6 +26,10 @@ namespace { } return false; } + + std::byte operator+(const std::byte &lhs, const std::byte &rhs) { + return std::byte {static_cast(std::to_integer(lhs) + std::to_integer(rhs))}; + } } // namespace namespace display_device { @@ -34,6 +45,89 @@ namespace display_device { return lhs.m_height == rhs.m_height && lhs.m_width == rhs.m_width; } + std::optional EdidData::parse(const std::vector &data) { + if (data.empty()) { + return std::nullopt; + } + + if (data.size() < 128) { + DD_LOG(warning) << "EDID data size is too small: " << data.size(); + return std::nullopt; + } + + // ---- Verify fixed header + static const std::vector fixed_header {std::byte {0x00}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0xFF}, std::byte {0x00}}; + if (!std::equal(std::begin(fixed_header), std::end(fixed_header), std::begin(data))) { + DD_LOG(warning) << "EDID data does not contain fixed header."; + return std::nullopt; + } + + // ---- Verify checksum + { + int sum = 0; + for (std::size_t i = 0; i < 128; ++i) { + sum += static_cast(data[i]); + } + + if (sum % 256 != 0) { + DD_LOG(warning) << "EDID checksum verification failed."; + return std::nullopt; + } + } + + EdidData edid {}; + + // ---- Get manufacturer ID (ASCII code A-Z) + { + constexpr std::byte ascii_offset {'@'}; + + const auto byte_a {data[8]}; + const auto byte_b {data[9]}; + std::array man_id {}; + + man_id[0] = static_cast(ascii_offset + ((byte_a & std::byte {0x7C}) >> 2)); + man_id[1] = static_cast(ascii_offset + ((byte_a & std::byte {0x03}) << 3) + ((byte_b & std::byte {0xE0}) >> 5)); + man_id[2] = static_cast(ascii_offset + (byte_b & std::byte {0x1F})); + + for (const char ch : man_id) { + if (ch < 'A' || ch > 'Z') { + DD_LOG(warning) << "EDID manufacturer id is out of range."; + return std::nullopt; + } + } + + edid.m_manufacturer_id = {std::begin(man_id), std::end(man_id)}; + } + + // ---- Product code (HEX representation) + { + std::uint16_t prod_num {0}; + prod_num |= std::to_integer(data[10]) << 0; + prod_num |= std::to_integer(data[11]) << 8; + + std::stringstream stream; + stream << std::setfill('0') << std::setw(4) << std::hex << std::uppercase << prod_num; + edid.m_product_code = stream.str(); + } + + // ---- Serial number + { + std::uint32_t serial_num {0}; + serial_num |= std::to_integer(data[12]) << 0; + serial_num |= std::to_integer(data[13]) << 8; + serial_num |= std::to_integer(data[14]) << 16; + serial_num |= std::to_integer(data[15]) << 24; + + edid.m_serial_number = serial_num; + } + + return edid; + } + + bool operator==(const EdidData &lhs, const EdidData &rhs) { + return lhs.m_manufacturer_id == rhs.m_manufacturer_id && lhs.m_product_code == rhs.m_product_code && lhs.m_serial_number == rhs.m_serial_number; + } + bool operator==(const EnumeratedDevice::Info &lhs, const EnumeratedDevice::Info &rhs) { return lhs.m_resolution == rhs.m_resolution && fuzzyCompare(lhs.m_resolution_scale, rhs.m_resolution_scale) && fuzzyCompare(lhs.m_refresh_rate, rhs.m_refresh_rate) && lhs.m_primary == rhs.m_primary && @@ -41,7 +135,7 @@ namespace display_device { } bool operator==(const EnumeratedDevice &lhs, const EnumeratedDevice &rhs) { - return lhs.m_device_id == rhs.m_device_id && lhs.m_display_name == rhs.m_display_name && lhs.m_friendly_name == rhs.m_friendly_name && lhs.m_info == rhs.m_info; + return lhs.m_device_id == rhs.m_device_id && lhs.m_display_name == rhs.m_display_name && lhs.m_friendly_name == rhs.m_friendly_name && lhs.m_edid == rhs.m_edid && lhs.m_info == rhs.m_info; } bool operator==(const SingleDisplayConfiguration &lhs, const SingleDisplayConfiguration &rhs) { diff --git a/src/windows/include/display_device/windows/types.h b/src/windows/include/display_device/windows/types.h index dc049ec..b8af9c2 100644 --- a/src/windows/include/display_device/windows/types.h +++ b/src/windows/include/display_device/windows/types.h @@ -51,7 +51,7 @@ namespace display_device { */ struct ValidatedDeviceInfo { std::string m_device_path {}; /**< Unique device path string. */ - std::string m_device_id {}; /**< A device id (made up by us) that is identifies the device. */ + std::string m_device_id {}; /**< A device id (made up by us) that identifies the device. */ }; /** diff --git a/src/windows/include/display_device/windows/win_api_layer.h b/src/windows/include/display_device/windows/win_api_layer.h index d328421..fe8bb33 100644 --- a/src/windows/include/display_device/windows/win_api_layer.h +++ b/src/windows/include/display_device/windows/win_api_layer.h @@ -22,6 +22,9 @@ namespace display_device { /** For details @see WinApiLayerInterface::getDeviceId */ [[nodiscard]] std::string getDeviceId(const DISPLAYCONFIG_PATH_INFO &path) const override; + /** For details @see WinApiLayerInterface::getEdid */ + [[nodiscard]] std::vector getEdid(const DISPLAYCONFIG_PATH_INFO &path) const override; + /** For details @see WinApiLayerInterface::getMonitorDevicePath */ [[nodiscard]] std::string getMonitorDevicePath(const DISPLAYCONFIG_PATH_INFO &path) const override; diff --git a/src/windows/include/display_device/windows/win_api_layer_interface.h b/src/windows/include/display_device/windows/win_api_layer_interface.h index 877997a..c9e43d2 100644 --- a/src/windows/include/display_device/windows/win_api_layer_interface.h +++ b/src/windows/include/display_device/windows/win_api_layer_interface.h @@ -57,10 +57,10 @@ namespace display_device { * * The accepted solution was to use the "InstanceID" and EDID (just to be * on the safe side). "InstanceID" is semi-stable, it has some parts that - * change between driver re-installs and it has a part that changes based + * change between driver re-installs, and it has a part that changes based * on the GPU port that the display is connected to. It is most likely to * be unique, but since the MS documentation is lacking we are also hashing - * EDID information (contains serial ids, timestamps and etc. that should + * EDID information (contains serial ids, timestamps, etc. that should * guarantee that identical displays are differentiated like with the * "ContainerID"). Most importantly this information is stable for the virtual * displays. @@ -84,6 +84,13 @@ namespace display_device { */ [[nodiscard]] virtual std::string getDeviceId(const DISPLAYCONFIG_PATH_INFO &path) const = 0; + /** + * @brief Get EDID byte array for the path. + * @param path Path to get the EDID for. + * @return EDID byte array, or an empty array if error has occurred. + */ + [[nodiscard]] virtual std::vector getEdid(const DISPLAYCONFIG_PATH_INFO &path) const = 0; + /** * @brief Get a string that represents a path from the adapter to the display target. * @param path Path to get the string for. @@ -101,9 +108,9 @@ namespace display_device { [[nodiscard]] virtual std::string getMonitorDevicePath(const DISPLAYCONFIG_PATH_INFO &path) const = 0; /** - * @brief Get the user friendly name for the path. - * @param path Path to get user friendly name for. - * @returns User friendly name for the path if available, empty string otherwise. + * @brief Get the user-friendly name for the path. + * @param path Path to get user-friendly name for. + * @returns User-friendly name for the path if available, empty string otherwise. * @see queryDisplayConfig on how to get paths from the system. * @note This is usually a monitor name (like "ROG PG279Q") and is most likely taken from EDID. * @examples diff --git a/src/windows/win_api_layer.cpp b/src/windows/win_api_layer.cpp index f4eb494..6bb2692 100644 --- a/src/windows/win_api_layer.cpp +++ b/src/windows/win_api_layer.cpp @@ -208,7 +208,7 @@ namespace display_device { * @returns True if EDID was retrieved and is non-empty, false otherwise. * @see getDeviceId implementation for more context regarding this madness. */ - bool getDeviceEdid(const WinApiLayerInterface &w_api, HDEVINFO dev_info_handle, SP_DEVINFO_DATA &dev_info_data, std::vector &edid) { + bool getDeviceEdid(const WinApiLayerInterface &w_api, HDEVINFO dev_info_handle, SP_DEVINFO_DATA &dev_info_data, std::vector &edid) { // We could just directly open the registry key as the path is known, but we can also use the this HKEY reg_key {SetupDiOpenDevRegKey(dev_info_handle, &dev_info_data, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ)}; if (reg_key == INVALID_HANDLE_VALUE) { @@ -234,7 +234,7 @@ namespace display_device { edid.resize(required_size_in_bytes); - status = RegQueryValueExW(reg_key, L"EDID", nullptr, nullptr, edid.data(), &required_size_in_bytes); + status = RegQueryValueExW(reg_key, L"EDID", nullptr, nullptr, reinterpret_cast(edid.data()), &required_size_in_bytes); if (status != ERROR_SUCCESS) { DD_LOG(error) << w_api.getErrorString(status) << " \"RegQueryValueExW\" failed when getting data."; return false; @@ -243,6 +243,69 @@ namespace display_device { return !edid.empty(); } + /** + * @brief Get instance ID and EDID via SetupAPI. + * @param w_api Reference to the WinApiLayer. + * @param device_path Device path to find device for. + * @return A tuple of instance ID and EDID, or empty optional if not device was found or error has occurred. + */ + std::optional>> getInstanceIdAndEdid(const WinApiLayerInterface &w_api, const std::wstring &device_path) { + static const GUID monitor_guid {0xe6f07b5f, 0xee97, 0x4a90, {0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7}}; + + HDEVINFO dev_info_handle {SetupDiGetClassDevsW(&monitor_guid, nullptr, nullptr, DIGCF_DEVICEINTERFACE)}; + if (dev_info_handle) { + const auto dev_info_handle_cleanup { + boost::scope::scope_exit([&dev_info_handle, &w_api]() { + if (!SetupDiDestroyDeviceInfoList(dev_info_handle)) { + DD_LOG(error) << w_api.getErrorString(static_cast(GetLastError())) << " \"SetupDiDestroyDeviceInfoList\" failed."; + } + }) + }; + + SP_DEVICE_INTERFACE_DATA dev_interface_data {}; + dev_interface_data.cbSize = sizeof(dev_interface_data); + for (DWORD monitor_index = 0;; ++monitor_index) { + if (!SetupDiEnumDeviceInterfaces(dev_info_handle, nullptr, &monitor_guid, monitor_index, &dev_interface_data)) { + const DWORD error_code {GetLastError()}; + if (error_code == ERROR_NO_MORE_ITEMS) { + break; + } + + DD_LOG(warning) << w_api.getErrorString(static_cast(error_code)) << " \"SetupDiEnumDeviceInterfaces\" failed."; + continue; + } + + std::wstring dev_interface_path; + SP_DEVINFO_DATA dev_info_data {}; + dev_info_data.cbSize = sizeof(dev_info_data); + if (!getDeviceInterfaceDetail(w_api, dev_info_handle, dev_interface_data, dev_interface_path, dev_info_data)) { + // Error already logged + continue; + } + + if (!boost::iequals(dev_interface_path, device_path)) { + continue; + } + + std::wstring instance_id; + if (!getDeviceInstanceId(w_api, dev_info_handle, dev_info_data, instance_id)) { + // Error already logged + break; + } + + std::vector edid; + if (!getDeviceEdid(w_api, dev_info_handle, dev_info_data, edid)) { + // Error already logged + break; + } + + return std::make_tuple(std::move(instance_id), std::move(edid)); + } + } + + return std::nullopt; + } + /** * @brief Converts a UTF-16 wide string into a UTF-8 string. * @param w_api Reference to the WinApiLayer. @@ -380,68 +443,24 @@ namespace display_device { return {}; } - static const GUID monitor_guid {0xe6f07b5f, 0xee97, 0x4a90, {0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7}}; - std::vector device_id_data; - - HDEVINFO dev_info_handle {SetupDiGetClassDevsW(&monitor_guid, nullptr, nullptr, DIGCF_DEVICEINTERFACE)}; - if (dev_info_handle) { - const auto dev_info_handle_cleanup { - boost::scope::scope_exit([this, &dev_info_handle]() { - if (!SetupDiDestroyDeviceInfoList(dev_info_handle)) { - DD_LOG(error) << getErrorString(static_cast(GetLastError())) << " \"SetupDiDestroyDeviceInfoList\" failed."; - } - }) - }; - - SP_DEVICE_INTERFACE_DATA dev_interface_data {}; - dev_interface_data.cbSize = sizeof(dev_interface_data); - for (DWORD monitor_index = 0;; ++monitor_index) { - if (!SetupDiEnumDeviceInterfaces(dev_info_handle, nullptr, &monitor_guid, monitor_index, &dev_interface_data)) { - const DWORD error_code {GetLastError()}; - if (error_code == ERROR_NO_MORE_ITEMS) { - break; - } - - DD_LOG(warning) << getErrorString(static_cast(error_code)) << " \"SetupDiEnumDeviceInterfaces\" failed."; - continue; - } - - std::wstring dev_interface_path; - SP_DEVINFO_DATA dev_info_data {}; - dev_info_data.cbSize = sizeof(dev_info_data); - if (!getDeviceInterfaceDetail(*this, dev_info_handle, dev_interface_data, dev_interface_path, dev_info_data)) { - // Error already logged - continue; - } - - if (!boost::iequals(dev_interface_path, device_path)) { - continue; - } - - // Instance ID is unique in the system and persists restarts, but not driver re-installs. - // It looks like this: - // DISPLAY\ACI27EC\5&4FD2DE4&5&UID4352 (also used in the device path it seems) - // a b c d e - // - // a) Hardware ID - stable - // b) Either a bus number or has something to do with device capabilities - stable - // c) Another ID, somehow tied to adapter (not an adapter ID from path object) - stable - // d) Some sort of rotating counter thing, changes after driver reinstall - unstable - // e) Seems to be the same as a target ID from path, it changes based on GPU port - semi-stable - // - // The instance ID also seems to be a part of the registry key (in case some other info is needed in the future): - // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\DISPLAY\ACI27EC\5&4fd2de4&5&UID4352 - - std::wstring instance_id; - if (!getDeviceInstanceId(*this, dev_info_handle, dev_info_data, instance_id)) { - // Error already logged - break; - } - - if (!getDeviceEdid(*this, dev_info_handle, dev_info_data, device_id_data)) { - // Error already logged - break; - } + std::vector device_id_data; + auto instance_id_and_edid {getInstanceIdAndEdid(*this, device_path)}; + if (instance_id_and_edid) { + // Instance ID is unique in the system and persists restarts, but not driver re-installs. + // It looks like this: + // DISPLAY\ACI27EC\5&4FD2DE4&5&UID4352 (also used in the device path it seems) + // a b c d e + // + // a) Hardware ID - stable + // b) Either a bus number or has something to do with device capabilities - stable + // c) Another ID, somehow tied to adapter (not an adapter ID from path object) - stable + // d) Some sort of rotating counter thing, changes after driver reinstall - unstable + // e) Seems to be the same as a target ID from path, it changes based on GPU port - semi-stable + // + // The instance ID also seems to be a part of the registry key (in case some other info is needed in the future): + // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\DISPLAY\ACI27EC\5&4fd2de4&5&UID4352 + [this, &device_id_data, &instance_id_and_edid]() { + auto [instance_id, edid] = *instance_id_and_edid; // We are going to discard the unstable parts of the instance ID and merge the stable parts with the edid buffer (if available) auto unstable_part_index = instance_id.find_first_of(L'&', 0); @@ -451,17 +470,18 @@ namespace display_device { if (unstable_part_index == std::wstring::npos) { DD_LOG(error) << "Failed to split off the stable part from instance id string " << toUtf8(*this, instance_id); - break; + return; } auto semi_stable_part_index = instance_id.find_first_of(L'&', unstable_part_index + 1); if (semi_stable_part_index == std::wstring::npos) { DD_LOG(error) << "Failed to split off the semi-stable part from instance id string " << toUtf8(*this, instance_id); - break; + return; } - device_id_data.insert(std::end(device_id_data), reinterpret_cast(instance_id.data()), reinterpret_cast(instance_id.data() + unstable_part_index)); - device_id_data.insert(std::end(device_id_data), reinterpret_cast(instance_id.data() + semi_stable_part_index), reinterpret_cast(instance_id.data() + instance_id.size())); + device_id_data.swap(edid); + device_id_data.insert(std::end(device_id_data), reinterpret_cast(instance_id.data()), reinterpret_cast(instance_id.data() + unstable_part_index)); + device_id_data.insert(std::end(device_id_data), reinterpret_cast(instance_id.data() + semi_stable_part_index), reinterpret_cast(instance_id.data() + instance_id.size())); static const auto dump_device_id_data {[](const auto &data) -> std::string { if (data.empty()) { @@ -471,7 +491,7 @@ namespace display_device { std::ostringstream output; output << "["; for (std::size_t i = 0; i < data.size(); ++i) { - output << "0x" << std::setw(2) << std::setfill('0') << std::hex << std::uppercase << static_cast(data[i]); + output << "0x" << std::setw(2) << std::setfill('0') << std::hex << std::uppercase << std::to_integer(data[i]); if (i + 1 < data.size()) { output << " "; } @@ -481,14 +501,13 @@ namespace display_device { return output.str(); }}; DD_LOG(verbose) << "Creating device id from EDID + instance ID: " << dump_device_id_data(device_id_data); - break; - } + }(); } if (device_id_data.empty()) { // Using the device path as a fallback, which is always unique, but not as stable as the preferred one DD_LOG(verbose) << "Creating device id from path " << toUtf8(*this, device_path); - device_id_data.insert(std::end(device_id_data), reinterpret_cast(device_path.data()), reinterpret_cast(device_path.data() + device_path.size())); + device_id_data.insert(std::end(device_id_data), reinterpret_cast(device_path.data()), reinterpret_cast(device_path.data() + device_path.size())); } static constexpr boost::uuids::uuid ns_id {}; // null namespace = no salt @@ -499,6 +518,17 @@ namespace display_device { return device_id; } + std::vector WinApiLayer::getEdid(const DISPLAYCONFIG_PATH_INFO &path) const { + const auto device_path {getMonitorDevicePathWstr(*this, path)}; + if (device_path.empty()) { + // Error already logged + return {}; + } + + auto instance_id_and_edid {getInstanceIdAndEdid(*this, device_path)}; + return instance_id_and_edid ? std::get<1>(*instance_id_and_edid) : std::vector {}; + } + std::string WinApiLayer::getMonitorDevicePath(const DISPLAYCONFIG_PATH_INFO &path) const { return toUtf8(*this, getMonitorDevicePathWstr(*this, path)); } diff --git a/src/windows/win_display_device_general.cpp b/src/windows/win_display_device_general.cpp index 7550511..5f835d0 100644 --- a/src/windows/win_display_device_general.cpp +++ b/src/windows/win_display_device_general.cpp @@ -48,6 +48,7 @@ namespace display_device { const bool is_active {win_utils::isActive(best_path)}; const auto source_mode {is_active ? win_utils::getSourceMode(win_utils::getSourceIndex(best_path, display_data->m_modes), display_data->m_modes) : nullptr}; const auto display_name {is_active ? m_w_api->getDisplayName(best_path) : std::string {}}; // Inactive devices can have multiple display names, so it's just meaningless use any + const auto edid {EdidData::parse(m_w_api->getEdid(best_path))}; if (is_active && !source_mode) { DD_LOG(warning) << "Device " << device_id << " is missing source mode!"; @@ -68,6 +69,7 @@ namespace display_device { {device_id, display_name, friendly_name, + edid, info} ); } else { @@ -75,6 +77,7 @@ namespace display_device { {device_id, display_name, friendly_name, + edid, std::nullopt} ); } diff --git a/tests/fixtures/include/fixtures/test_utils.h b/tests/fixtures/include/fixtures/test_utils.h index 1376e7d..6a14f7b 100644 --- a/tests/fixtures/include/fixtures/test_utils.h +++ b/tests/fixtures/include/fixtures/test_utils.h @@ -3,6 +3,19 @@ // system includes #include #include +#include + +// local includes +#include "display_device/types.h" + +/** + * @brief Contains some useful predefined structures for UTs. + * @note Data is to be extended with relevant information as needed. + */ +namespace ut_consts { + extern const std::vector DEFAULT_EDID; + extern const display_device::EdidData DEFAULT_EDID_DATA; +} // namespace ut_consts /** * @brief Test regular expression against string. diff --git a/tests/fixtures/test_utils.cpp b/tests/fixtures/test_utils.cpp index b82ed70..c290998 100644 --- a/tests/fixtures/test_utils.cpp +++ b/tests/fixtures/test_utils.cpp @@ -5,9 +5,42 @@ #include // system includes +#include #include #include +namespace ut_consts { + namespace { + template + std::vector makeBytes(Ts &&...args) { + return {std::byte {static_cast(args)}...}; + } + } // namespace + + const std::vector DEFAULT_EDID {makeBytes( + // clang-format off + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x04, 0x69, + 0xEC, 0x27, 0xAA, 0x55, 0x00, 0x00, 0x13, 0x1D, 0x01, 0x04, + 0xA5, 0x3C, 0x22, 0x78, 0x06, 0xEE, 0x91, 0xA3, 0x54, 0x4C, + 0x99, 0x26, 0x0F, 0x50, 0x54, 0x21, 0x08, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x56, 0x5E, 0x00, 0xA0, 0xA0, 0xA0, + 0x29, 0x50, 0x30, 0x20, 0x35, 0x00, 0x56, 0x50, 0x21, 0x00, + 0x00, 0x1A, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x23, 0x41, 0x53, + 0x4E, 0x39, 0x4A, 0x36, 0x6E, 0x4E, 0x49, 0x54, 0x62, 0x64, + 0x00, 0x00, 0x00, 0xFD, 0x00, 0x1E, 0x90, 0x22, 0xDE, 0x3B, + 0x01, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, + 0x00, 0xFC, 0x00, 0x52, 0x4F, 0x47, 0x20, 0x50, 0x47, 0x32, + 0x37, 0x39, 0x51, 0x0A, 0x20, 0x20, 0x01, 0x8B + // clang-format on + )}; + const display_device::EdidData DEFAULT_EDID_DATA { + .m_manufacturer_id = "ACI", + .m_product_code = "27EC", + .m_serial_number = 21930 + }; +} // namespace ut_consts + bool testRegex(const std::string &input, const std::string &pattern) { std::regex regex(pattern); std::smatch match; diff --git a/tests/unit/general/test_comparison.cpp b/tests/unit/general/test_comparison.cpp index 0295ce0..131aebb 100644 --- a/tests/unit/general/test_comparison.cpp +++ b/tests/unit/general/test_comparison.cpp @@ -25,6 +25,13 @@ TEST_S(Resolution) { EXPECT_NE(display_device::Resolution({1, 1}), display_device::Resolution({1, 0})); } +TEST_S(EdidData) { + EXPECT_EQ(display_device::EdidData({"LOL", "1337", 1234}), display_device::EdidData({"LOL", "1337", 1234})); + EXPECT_NE(display_device::EdidData({"LOL", "1337", 1234}), display_device::EdidData({"MEH", "1337", 1234})); + EXPECT_NE(display_device::EdidData({"LOL", "1337", 1234}), display_device::EdidData({"LOL", "1338", 1234})); + EXPECT_NE(display_device::EdidData({"LOL", "1337", 1234}), display_device::EdidData({"LOL", "1337", 1235})); +} + TEST_S(EnumeratedDevice, Info) { using Rat = display_device::Rational; EXPECT_EQ(display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt}), display_device::EnumeratedDevice::Info({{1, 1}, 1., 1., true, {1, 1}, std::nullopt})); @@ -42,11 +49,12 @@ TEST_S(EnumeratedDevice, Info) { } TEST_S(EnumeratedDevice) { - EXPECT_EQ(display_device::EnumeratedDevice({"1", "1", "1", display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "1", display_device::EnumeratedDevice::Info {}})); - EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"0", "1", "1", display_device::EnumeratedDevice::Info {}})); - EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "0", "1", display_device::EnumeratedDevice::Info {}})); - EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "0", display_device::EnumeratedDevice::Info {}})); - EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "1", std::nullopt})); + EXPECT_EQ(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}})); + EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"0", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}})); + EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "0", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}})); + EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "0", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}})); + EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "1", std::nullopt, display_device::EnumeratedDevice::Info {}})); + EXPECT_NE(display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, display_device::EnumeratedDevice::Info {}}), display_device::EnumeratedDevice({"1", "1", "1", display_device::EdidData {}, std::nullopt})); } TEST_S(SingleDisplayConfiguration) { diff --git a/tests/unit/general/test_edid_parsing.cpp b/tests/unit/general/test_edid_parsing.cpp new file mode 100644 index 0000000..2a003db --- /dev/null +++ b/tests/unit/general/test_edid_parsing.cpp @@ -0,0 +1,48 @@ +// local includes +#include "display_device/types.h" +#include "fixtures/fixtures.h" + +namespace { + // Specialized TEST macro(s) for this test file +#define TEST_S(...) DD_MAKE_TEST(TEST, EdidParsing, __VA_ARGS__) +} // namespace + +TEST_S(NoData) { + EXPECT_EQ(display_device::EdidData::parse({}), std::nullopt); +} + +TEST_S(TooLittleData) { + EXPECT_EQ(display_device::EdidData::parse({std::byte {0x11}}), std::nullopt); +} + +TEST_S(BadFixedHeader) { + auto EDID_DATA {ut_consts::DEFAULT_EDID}; + EDID_DATA[1] = std::byte {0xAA}; + EXPECT_EQ(display_device::EdidData::parse(EDID_DATA), std::nullopt); +} + +TEST_S(BadChecksum) { + auto EDID_DATA {ut_consts::DEFAULT_EDID}; + EDID_DATA[16] = std::byte {0x00}; + EXPECT_EQ(display_device::EdidData::parse(EDID_DATA), std::nullopt); +} + +TEST_S(InvalidManufacturerId, BelowLimit) { + auto EDID_DATA {ut_consts::DEFAULT_EDID}; + // The sum of 8th and 9th bytes should remain 109 + EDID_DATA[8] = std::byte {0x00}; + EDID_DATA[9] = std::byte {0x6D}; + EXPECT_EQ(display_device::EdidData::parse(EDID_DATA), std::nullopt); +} + +TEST_S(InvalidManufacturerId, AboveLimit) { + auto EDID_DATA {ut_consts::DEFAULT_EDID}; + // The sum of 8th and 9th bytes should remain 109 + EDID_DATA[8] = std::byte {0x6D}; + EDID_DATA[9] = std::byte {0x00}; + EXPECT_EQ(display_device::EdidData::parse(EDID_DATA), std::nullopt); +} + +TEST_S(ValidOutput) { + EXPECT_EQ(display_device::EdidData::parse(ut_consts::DEFAULT_EDID), ut_consts::DEFAULT_EDID_DATA); +} diff --git a/tests/unit/general/test_json_converter.cpp b/tests/unit/general/test_json_converter.cpp index 59c89b9..ef53304 100644 --- a/tests/unit/general/test_json_converter.cpp +++ b/tests/unit/general/test_json_converter.cpp @@ -6,11 +6,23 @@ namespace { #define TEST_F_S(...) DD_MAKE_TEST(TEST_F, JsonConverterTest, __VA_ARGS__) } // namespace +TEST_F_S(EdidData) { + display_device::EdidData item { + .m_manufacturer_id = "LOL", + .m_product_code = "ABCD", + .m_serial_number = 777777 + }; + + executeTestCase(display_device::EdidData {}, R"({"manufacturer_id":"","product_code":"","serial_number":0})"); + executeTestCase(item, R"({"manufacturer_id":"LOL","product_code":"ABCD","serial_number":777777})"); +} + TEST_F_S(EnumeratedDevice) { display_device::EnumeratedDevice item_1 { "ID_1", "NAME_2", "FU_NAME_3", + std::nullopt, display_device::EnumeratedDevice::Info { {1920, 1080}, display_device::Rational {175, 100}, @@ -24,6 +36,7 @@ TEST_F_S(EnumeratedDevice) { "ID_2", "NAME_2", "FU_NAME_2", + display_device::EdidData {}, display_device::EnumeratedDevice::Info { {1920, 1080}, 1.75, @@ -34,9 +47,9 @@ TEST_F_S(EnumeratedDevice) { } }; - executeTestCase(display_device::EnumeratedDevice {}, R"({"device_id":"","display_name":"","friendly_name":"","info":null})"); - executeTestCase(item_1, R"({"device_id":"ID_1","display_name":"NAME_2","friendly_name":"FU_NAME_3","info":{"hdr_state":"Enabled","origin_point":{"x":1,"y":2},"primary":false,"refresh_rate":{"type":"double","value":119.9554},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"rational","value":{"denominator":100,"numerator":175}}}})"); - executeTestCase(item_2, R"({"device_id":"ID_2","display_name":"NAME_2","friendly_name":"FU_NAME_2","info":{"hdr_state":"Disabled","origin_point":{"x":0,"y":0},"primary":true,"refresh_rate":{"type":"rational","value":{"denominator":10000,"numerator":1199554}},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"double","value":1.75}}})"); + executeTestCase(display_device::EnumeratedDevice {}, R"({"device_id":"","display_name":"","edid":null,"friendly_name":"","info":null})"); + executeTestCase(item_1, R"({"device_id":"ID_1","display_name":"NAME_2","edid":null,"friendly_name":"FU_NAME_3","info":{"hdr_state":"Enabled","origin_point":{"x":1,"y":2},"primary":false,"refresh_rate":{"type":"double","value":119.9554},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"rational","value":{"denominator":100,"numerator":175}}}})"); + executeTestCase(item_2, R"({"device_id":"ID_2","display_name":"NAME_2","edid":{"manufacturer_id":"","product_code":"","serial_number":0},"friendly_name":"FU_NAME_2","info":{"hdr_state":"Disabled","origin_point":{"x":0,"y":0},"primary":true,"refresh_rate":{"type":"rational","value":{"denominator":10000,"numerator":1199554}},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"double","value":1.75}}})"); } TEST_F_S(EnumeratedDeviceList) { @@ -44,6 +57,7 @@ TEST_F_S(EnumeratedDeviceList) { "ID_1", "NAME_2", "FU_NAME_3", + std::nullopt, display_device::EnumeratedDevice::Info { {1920, 1080}, display_device::Rational {175, 100}, @@ -57,6 +71,7 @@ TEST_F_S(EnumeratedDeviceList) { "ID_2", "NAME_2", "FU_NAME_2", + display_device::EdidData {}, display_device::EnumeratedDevice::Info { {1920, 1080}, 1.75, @@ -69,9 +84,9 @@ TEST_F_S(EnumeratedDeviceList) { display_device::EnumeratedDevice item_3 {}; executeTestCase(display_device::EnumeratedDeviceList {}, R"([])"); - executeTestCase(display_device::EnumeratedDeviceList {item_1, item_2, item_3}, R"([{"device_id":"ID_1","display_name":"NAME_2","friendly_name":"FU_NAME_3","info":{"hdr_state":"Enabled","origin_point":{"x":1,"y":2},"primary":false,"refresh_rate":{"type":"double","value":119.9554},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"rational","value":{"denominator":100,"numerator":175}}}},)" - R"({"device_id":"ID_2","display_name":"NAME_2","friendly_name":"FU_NAME_2","info":{"hdr_state":"Disabled","origin_point":{"x":0,"y":0},"primary":true,"refresh_rate":{"type":"rational","value":{"denominator":10000,"numerator":1199554}},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"double","value":1.75}}},)" - R"({"device_id":"","display_name":"","friendly_name":"","info":null}])"); + executeTestCase(display_device::EnumeratedDeviceList {item_1, item_2, item_3}, R"([{"device_id":"ID_1","display_name":"NAME_2","edid":null,"friendly_name":"FU_NAME_3","info":{"hdr_state":"Enabled","origin_point":{"x":1,"y":2},"primary":false,"refresh_rate":{"type":"double","value":119.9554},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"rational","value":{"denominator":100,"numerator":175}}}},)" + R"({"device_id":"ID_2","display_name":"NAME_2","edid":{"manufacturer_id":"","product_code":"","serial_number":0},"friendly_name":"FU_NAME_2","info":{"hdr_state":"Disabled","origin_point":{"x":0,"y":0},"primary":true,"refresh_rate":{"type":"rational","value":{"denominator":10000,"numerator":1199554}},"resolution":{"height":1080,"width":1920},"resolution_scale":{"type":"double","value":1.75}}},)" + R"({"device_id":"","display_name":"","edid":null,"friendly_name":"","info":null}])"); } TEST_F_S(SingleDisplayConfiguration) { diff --git a/tests/unit/windows/test_win_display_device_general.cpp b/tests/unit/windows/test_win_display_device_general.cpp index 4c74ec3..365bb6c 100644 --- a/tests/unit/windows/test_win_display_device_general.cpp +++ b/tests/unit/windows/test_win_display_device_general.cpp @@ -129,6 +129,10 @@ TEST_F_S_MOCKED(EnumAvailableDevices) { .Times(1) .WillOnce(Return("DisplayName1")) .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getEdid(_)) + .Times(1) + .WillOnce(Return(std::vector {})) + .RetiresOnSaturation(); EXPECT_CALL(*m_layer, getDisplayScale(_, _)) .Times(1) .WillOnce(Return(std::nullopt)) @@ -146,6 +150,10 @@ TEST_F_S_MOCKED(EnumAvailableDevices) { .Times(1) .WillOnce(Return("DisplayName2")) .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getEdid(_)) + .Times(1) + .WillOnce(Return(ut_consts::DEFAULT_EDID)) + .RetiresOnSaturation(); EXPECT_CALL(*m_layer, getDisplayScale(_, _)) .Times(1) .WillOnce(Return(display_device::Rational {175, 100})) @@ -159,11 +167,16 @@ TEST_F_S_MOCKED(EnumAvailableDevices) { .Times(1) .WillOnce(Return("FriendlyName3")) .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getEdid(_)) + .Times(1) + .WillOnce(Return(std::vector {})) + .RetiresOnSaturation(); const display_device::EnumeratedDeviceList expected_list { {"DeviceId1", "DisplayName1", "FriendlyName1", + std::nullopt, display_device::EnumeratedDevice::Info { {1920, 1080}, display_device::Rational {0, 1}, @@ -175,6 +188,7 @@ TEST_F_S_MOCKED(EnumAvailableDevices) { {"DeviceId2", "DisplayName2", "FriendlyName2", + ut_consts::DEFAULT_EDID_DATA, display_device::EnumeratedDevice::Info { {1920, 2160}, display_device::Rational {175, 100}, @@ -186,6 +200,7 @@ TEST_F_S_MOCKED(EnumAvailableDevices) { {"DeviceId3", "", "FriendlyName3", + std::nullopt, std::nullopt} }; EXPECT_EQ(m_win_dd.enumAvailableDevices(), expected_list); @@ -225,6 +240,10 @@ TEST_F_S_MOCKED(EnumAvailableDevices, MissingSourceModes) { .Times(1) .WillOnce(Return("DisplayName1")) .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getEdid(_)) + .Times(1) + .WillOnce(Return(std::vector {})) + .RetiresOnSaturation(); EXPECT_CALL(*m_layer, getDisplayScale(_, _)) .Times(1) .WillOnce(Return(std::nullopt)) @@ -242,11 +261,16 @@ TEST_F_S_MOCKED(EnumAvailableDevices, MissingSourceModes) { .Times(1) .WillOnce(Return("DisplayName2")) .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getEdid(_)) + .Times(1) + .WillOnce(Return(ut_consts::DEFAULT_EDID)) + .RetiresOnSaturation(); const display_device::EnumeratedDeviceList expected_list { {"DeviceId1", "DisplayName1", "FriendlyName1", + std::nullopt, display_device::EnumeratedDevice::Info { {1920, 1080}, display_device::Rational {0, 1}, @@ -258,6 +282,7 @@ TEST_F_S_MOCKED(EnumAvailableDevices, MissingSourceModes) { {"DeviceId2", "DisplayName2", "FriendlyName2", + ut_consts::DEFAULT_EDID_DATA, std::nullopt} }; EXPECT_EQ(m_win_dd.enumAvailableDevices(), expected_list); diff --git a/tests/unit/windows/utils/mock_win_api_layer.h b/tests/unit/windows/utils/mock_win_api_layer.h index 3dd7dfb..1c5edbf 100644 --- a/tests/unit/windows/utils/mock_win_api_layer.h +++ b/tests/unit/windows/utils/mock_win_api_layer.h @@ -12,6 +12,7 @@ namespace display_device { MOCK_METHOD(std::string, getErrorString, (LONG), (const, override)); MOCK_METHOD(std::optional, queryDisplayConfig, (QueryType), (const, override)); MOCK_METHOD(std::string, getDeviceId, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); + MOCK_METHOD(std::vector, getEdid, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); MOCK_METHOD(std::string, getMonitorDevicePath, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); MOCK_METHOD(std::string, getFriendlyName, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); MOCK_METHOD(std::string, getDisplayName, (const DISPLAYCONFIG_PATH_INFO &), (const, override));