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 @@
+