diff --git a/IntelPresentMon/ControlLib/ControlLib.vcxproj b/IntelPresentMon/ControlLib/ControlLib.vcxproj index 2b22cf10..d358dde7 100644 --- a/IntelPresentMon/ControlLib/ControlLib.vcxproj +++ b/IntelPresentMon/ControlLib/ControlLib.vcxproj @@ -116,6 +116,7 @@ + @@ -144,6 +145,7 @@ + diff --git a/IntelPresentMon/ControlLib/ControlLib.vcxproj.filters b/IntelPresentMon/ControlLib/ControlLib.vcxproj.filters index c531d96b..16431c77 100644 --- a/IntelPresentMon/ControlLib/ControlLib.vcxproj.filters +++ b/IntelPresentMon/ControlLib/ControlLib.vcxproj.filters @@ -70,6 +70,9 @@ + + Intel + Intel @@ -117,6 +120,9 @@ Adl + + Intel + Intel diff --git a/IntelPresentMon/ControlLib/igcl/IgclControlLibCompatibility.cpp b/IntelPresentMon/ControlLib/igcl/IgclControlLibCompatibility.cpp new file mode 100644 index 00000000..2d7907cb --- /dev/null +++ b/IntelPresentMon/ControlLib/igcl/IgclControlLibCompatibility.cpp @@ -0,0 +1,257 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: MIT +#include "IgclControlLibCompatibility.h" + +#include "../Logging.h" + +// Per-GPU ControlApiPath values are read from each present DISPLAY adapter +// driver registry key (SetupDi DIREG_DRV), analogous to indexed +// IDeviceConfigFactory GPU enumeration in the shared C# helper. + +#include +#include +#include + +#include +#include +#include +#include +#include + +#pragma comment(lib, "setupapi.lib") +#pragma comment(lib, "version.lib") + +namespace pmon::tel::igcl +{ + namespace + { + constexpr wchar_t kControlApiPathValueName[] = L"ControlApiPath"; + + bool IsPreHotfixControlLibVersion_(ControlLibFileVersion version) noexcept + { + if (version.major < 1) { + return true; + } + if (version.major > 1) { + return false; + } + + if (version.minor < 2) { + return true; + } + if (version.minor > 2) { + return false; + } + + if (version.build > 269) { + return false; + } + if (version.build < 269) { + return true; + } + return version.privatePart < 269; + } + + bool IsZesInitOnlyControlLibVersion_(ControlLibFileVersion version) noexcept + { + if (version.major > 1) { + return true; + } + return version.major == 1 && version.minor >= 3; + } + + std::optional ReadRegistryStringValue_(HKEY key, const wchar_t* valueName) + { + DWORD valueType = 0; + DWORD bufferBytes = 0; + const LSTATUS sizeStatus = RegQueryValueExW( + key, + valueName, + nullptr, + &valueType, + nullptr, + &bufferBytes); + if (sizeStatus != ERROR_SUCCESS || bufferBytes == 0) { + return std::nullopt; + } + if (valueType != REG_SZ && valueType != REG_EXPAND_SZ) { + return std::nullopt; + } + + std::wstring value; + value.resize((size_t)bufferBytes / sizeof(wchar_t)); + const LSTATUS readStatus = RegQueryValueExW( + key, + valueName, + nullptr, + &valueType, + reinterpret_cast(value.data()), + &bufferBytes); + if (readStatus != ERROR_SUCCESS) { + return std::nullopt; + } + + while (!value.empty() && value.back() == L'\0') { + value.pop_back(); + } + if (value.empty()) { + return std::nullopt; + } + + if (valueType == REG_EXPAND_SZ) { + const DWORD expandedChars = ExpandEnvironmentStringsW(value.c_str(), nullptr, 0); + if (expandedChars == 0) { + return std::nullopt; + } + std::wstring expanded; + expanded.resize((size_t)expandedChars); + if (ExpandEnvironmentStringsW(value.c_str(), expanded.data(), expandedChars) == 0) { + return std::nullopt; + } + while (!expanded.empty() && expanded.back() == L'\0') { + expanded.pop_back(); + } + value = std::move(expanded); + } + + return value; + } + + std::optional TryReadPeFileVersion_(const std::wstring& path) + { + DWORD handle = 0; + const DWORD infoSize = GetFileVersionInfoSizeW(path.c_str(), &handle); + if (infoSize == 0) { + return std::nullopt; + } + + std::vector infoBuffer((size_t)infoSize); + if (!GetFileVersionInfoW(path.c_str(), 0, infoSize, infoBuffer.data())) { + return std::nullopt; + } + + VS_FIXEDFILEINFO* pFileInfo = nullptr; + UINT fileInfoLength = 0; + if (!VerQueryValueW( + infoBuffer.data(), + L"\\", + reinterpret_cast(&pFileInfo), + &fileInfoLength) + || pFileInfo == nullptr) { + return std::nullopt; + } + + ControlLibFileVersion version{}; + version.major = HIWORD(pFileInfo->dwFileVersionMS); + version.minor = LOWORD(pFileInfo->dwFileVersionMS); + version.build = HIWORD(pFileInfo->dwFileVersionLS); + version.privatePart = LOWORD(pFileInfo->dwFileVersionLS); + return version; + } + + std::optional> CollectControlLibFileVersionsFromRegistry_() + { + std::vector versions; + + const HDEVINFO deviceInfoSet = SetupDiGetClassDevsW( + &GUID_DEVCLASS_DISPLAY, + nullptr, + nullptr, + DIGCF_PRESENT); + if (deviceInfoSet == INVALID_HANDLE_VALUE) { + return std::nullopt; + } + + SP_DEVINFO_DATA deviceInfoData{}; + deviceInfoData.cbSize = sizeof(deviceInfoData); + + for (DWORD deviceIndex = 0;; ++deviceIndex) { + if (!SetupDiEnumDeviceInfo(deviceInfoSet, deviceIndex, &deviceInfoData)) { + if (GetLastError() == ERROR_NO_MORE_ITEMS) { + break; + } + SetupDiDestroyDeviceInfoList(deviceInfoSet); + return std::nullopt; + } + + const HKEY driverKey = SetupDiOpenDevRegKey( + deviceInfoSet, + &deviceInfoData, + DICS_FLAG_GLOBAL, + 0, + DIREG_DRV, + KEY_READ); + if (driverKey == INVALID_HANDLE_VALUE) { + continue; + } + + const auto controlApiPath = ReadRegistryStringValue_(driverKey, kControlApiPathValueName); + RegCloseKey(driverKey); + if (!controlApiPath.has_value()) { + continue; + } + + std::error_code fsError; + if (!std::filesystem::exists(*controlApiPath, fsError)) { + continue; + } + + const auto fileVersion = TryReadPeFileVersion_(*controlApiPath); + if (fileVersion.has_value()) { + versions.push_back(*fileVersion); + } + } + + SetupDiDestroyDeviceInfoList(deviceInfoSet); + return versions; + } + } + + bool IsPreHotfixControlLibVersion(ControlLibFileVersion version) noexcept + { + return IsPreHotfixControlLibVersion_(version); + } + + bool IsZesInitOnlyControlLibVersion(ControlLibFileVersion version) noexcept + { + return IsZesInitOnlyControlLibVersion_(version); + } + + bool IsControlLibVersionMismatch(std::span versions) noexcept + { + bool hasPreHotfix = false; + bool hasZesInitOnly = false; + for (const ControlLibFileVersion& version : versions) { + if (IsPreHotfixControlLibVersion_(version)) { + hasPreHotfix = true; + } + if (IsZesInitOnlyControlLibVersion_(version)) { + hasZesInitOnly = true; + } + if (hasPreHotfix && hasZesInitOnly) { + return true; + } + } + return false; + } + + bool AreIgclControlLibsMismatched() noexcept + { + try { + const auto versions = CollectControlLibFileVersionsFromRegistry_(); + if (!versions.has_value()) { + pmlog_warn("IGCL control library compatibility check could not enumerate display adapters; skipping ctlInit"); + return true; + } + if (IsControlLibVersionMismatch(*versions)) { + pmlog_warn("Mixed IGCL control library versions detected across GPUs; skipping ctlInit"); + return true; + } + return false; + } + catch (...) { + pmlog_warn("IGCL control library compatibility check failed; skipping ctlInit"); + return true; + } + } +} diff --git a/IntelPresentMon/ControlLib/igcl/IgclControlLibCompatibility.h b/IntelPresentMon/ControlLib/igcl/IgclControlLibCompatibility.h new file mode 100644 index 00000000..81f2e2b8 --- /dev/null +++ b/IntelPresentMon/ControlLib/igcl/IgclControlLibCompatibility.h @@ -0,0 +1,29 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include + +namespace pmon::tel::igcl +{ + struct ControlLibFileVersion + { + uint16_t major = 0; + uint16_t minor = 0; + uint16_t build = 0; + uint16_t privatePart = 0; + }; + + bool IsPreHotfixControlLibVersion(ControlLibFileVersion version) noexcept; + bool IsZesInitOnlyControlLibVersion(ControlLibFileVersion version) noexcept; + + // True when collected versions include both pre-hotfix (< 1.2.269.269) and 1.3+ builds. + bool IsControlLibVersionMismatch(std::span versions) noexcept; + + // Enumerates per-GPU ControlApiPath registry entries and compares file versions. + // Returns true when versions are mismatched OR when the compatibility check cannot be completed. + // Caller must not invoke ctlInit when true. + bool AreIgclControlLibsMismatched() noexcept; +} diff --git a/IntelPresentMon/ControlLib/igcl/IgclTelemetryProvider.cpp b/IntelPresentMon/ControlLib/igcl/IgclTelemetryProvider.cpp index 6baa2e6f..390bf899 100644 --- a/IntelPresentMon/ControlLib/igcl/IgclTelemetryProvider.cpp +++ b/IntelPresentMon/ControlLib/igcl/IgclTelemetryProvider.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT #include "IgclTelemetryProvider.h" +#include "IgclControlLibCompatibility.h" #include "../TelemetryMetricDiscovery.h" #include "../Exceptions.h" #include "../../CommonUtilities/Qpc.h" @@ -73,6 +74,10 @@ namespace pmon::tel::igcl IgclTelemetryProvider::IgclTelemetryProvider() { + if (AreIgclControlLibsMismatched()) { + throw Except("IGCL control library unavailable"); + } + ctl_init_args_t ctlInitArgs{ .Size = sizeof(ctl_init_args_t), .AppVersion = CTL_MAKE_VERSION(CTL_IMPL_MAJOR_VERSION, CTL_IMPL_MINOR_VERSION), diff --git a/IntelPresentMon/UnitTests/IgclControlLibCompatibilityTests.cpp b/IntelPresentMon/UnitTests/IgclControlLibCompatibilityTests.cpp new file mode 100644 index 00000000..80de1a33 --- /dev/null +++ b/IntelPresentMon/UnitTests/IgclControlLibCompatibilityTests.cpp @@ -0,0 +1,45 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: MIT +#include +#include +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace IgclControlLibCompatibilityTests +{ + using pmon::tel::igcl::ControlLibFileVersion; + using pmon::tel::igcl::IsControlLibVersionMismatch; + using pmon::tel::igcl::IsPreHotfixControlLibVersion; + using pmon::tel::igcl::IsZesInitOnlyControlLibVersion; + + TEST_CLASS(ControlLibVersionPredicateTests) + { + public: + TEST_METHOD(PreHotfixBelowOneTwoTwoSixNine) + { + Assert::IsTrue(IsPreHotfixControlLibVersion({ 1, 2, 268, 999 })); + Assert::IsFalse(IsPreHotfixControlLibVersion({ 1, 2, 269, 269 })); + Assert::IsFalse(IsPreHotfixControlLibVersion({ 1, 2, 270, 0 })); + } + + TEST_METHOD(ZesInitOnlyAtOneThree) + { + Assert::IsTrue(IsZesInitOnlyControlLibVersion({ 1, 3, 0, 0 })); + Assert::IsFalse(IsZesInitOnlyControlLibVersion({ 1, 2, 999, 999 })); + } + + TEST_METHOD(MismatchRequiresBothBuckets) + { + const ControlLibFileVersion preHotfix{ 1, 2, 100, 0 }; + const ControlLibFileVersion zesInit{ 1, 3, 0, 0 }; + const ControlLibFileVersion hotfixNeutral{ 1, 2, 269, 269 }; + + Assert::IsTrue(IsControlLibVersionMismatch(std::array{ preHotfix, zesInit })); + Assert::IsFalse(IsControlLibVersionMismatch(std::array{ preHotfix, hotfixNeutral })); + Assert::IsFalse(IsControlLibVersionMismatch(std::array{ zesInit, hotfixNeutral })); + Assert::IsFalse(IsControlLibVersionMismatch(std::span{})); + } + }; +} diff --git a/IntelPresentMon/UnitTests/UnitTests.vcxproj b/IntelPresentMon/UnitTests/UnitTests.vcxproj index 753e744c..d83a7920 100644 --- a/IntelPresentMon/UnitTests/UnitTests.vcxproj +++ b/IntelPresentMon/UnitTests/UnitTests.vcxproj @@ -108,6 +108,7 @@ + diff --git a/IntelPresentMon/UnitTests/UnitTests.vcxproj.filters b/IntelPresentMon/UnitTests/UnitTests.vcxproj.filters index 5b66b8b8..76b8fa10 100644 --- a/IntelPresentMon/UnitTests/UnitTests.vcxproj.filters +++ b/IntelPresentMon/UnitTests/UnitTests.vcxproj.filters @@ -7,6 +7,7 @@ +