A modern C# wrapper for Intel Graphics Control Library (IGCL), providing easy access to Intel GPU features and settings.
- IntPtr-based API surface; no custom handle types to manage
- Automatic cleanup via
IDisposable(SafeHandle-backed) - Strongly typed structs/enums matching IGCL headers
- Helper methods for common adapter/display queries
- Facade DTOs for bool-friendly helper results
- ClangSharp-generated bindings kept in sync with the SDK
- Tests skip gracefully when hardware is absent
- Split test suites:
IGCLWrapper.NativeTestsexercises only ClangSharp-generated APIs.IGCLWrapper.FacadeTestsexercises the new facade helpers.
- Intel GPU with IGCL support
- Windows 10/11 x64
- .NET 10.0 SDK
- Intel Graphics drivers (25.20.100.6618+)
git clone https://github.com/terrymacdonald/IGCLWrapper.git
cd IGCLWrapper
./prepare_igcl.ps1 # pulls the IGCL SDK
./build_igcl.ps1 # restores, regenerates bindings, builds, tests- Local build artifacts: after
./build_igcl.ps1, referenceIGCLWrapper\bin\Debug\net10.0\IGCLWrapper.dll(orReleaseif you build release) from your project. - Add as a project reference: add
IGCLWrapper/IGCLWrapper.csprojto your solution and reference it. - Requirements at runtime: Windows x64, Intel GPU/driver, and IGCL DLLs available (the wrapper dynamically loads
ControlLibfrom the installed Intel drivers).
using IGCLWrapper;
using var api = IGCLApiHelper.Initialize();
var adapters = api.EnumerateAdapters();
Console.WriteLine($"Found {adapters.Count} Intel GPU(s)");
foreach (var adapter in adapters)
{
var props = adapter.GetProperties();
Console.WriteLine($"\nGPU: {adapter.Name}");
Console.WriteLine($"Device ID: 0x{props.pci_device_id:X}");
foreach (var display in adapter.EnumerateDisplayOutputs())
{
if (!display.IsActive()) continue;
var (width, height) = display.GetResolution();
var refresh = display.GetRefreshRateHz();
Console.WriteLine($" {width}x{height} @ {refresh:F2} Hz");
}
}try
{
using var igcl = IGCLApi.Initialize();
// use the API
}
catch (IGCLException ex)
{
Console.WriteLine($"IGCL Error: {ex.Result} - {ex.Message}");
}
catch (DllNotFoundException)
{
Console.WriteLine("IGCL DLL not found. Install Intel Graphics drivers.");
}Use the facade helpers to avoid manual struct sizing/handle management.
DTO-returning helpers use bool properties instead of native byte fields.
Get/Set operations are split into separate Get*() and Set*() helpers. For advanced use cases requiring raw IGCL structs (e.g. pixel transformation with pointer fields), use the IGCLApi native layer directly.
using IGCLWrapper;
using var api = IGCLApiHelper.Initialize();
foreach (var adapter in api.EnumerateAdapters())
{
foreach (var display in adapter.EnumerateDisplayOutputs())
{
if (!display.IsActive()) continue;
var (w, h) = display.GetResolution();
var hz = display.GetRefreshRateHz();
Console.WriteLine($"{adapter.Name}: {w}x{h} @ {hz:F2} Hz");
}
}using IGCLWrapper;
using System.Linq;
using var api = IGCLApiHelper.Initialize();
foreach (var adapter in api.EnumerateAdapters())
{
var result = adapter.GetCombinedDisplay();
if (result.IsSupported && result.NumOutputs > 0)
{
Console.WriteLine($"Adapter {adapter.Name} has a combined display with {result.NumOutputs} outputs.");
}
}using IGCLWrapper;
using System.Linq;
using var api = IGCLApiHelper.Initialize();
foreach (var adapter in api.EnumerateAdapters())
{
var combined = adapter.GetCombinedDisplay();
if (combined.NumOutputs == 0 || combined.ChildInfos == null)
{
Console.WriteLine($"Adapter {adapter.Name} has no combined display configured.");
continue;
}
Console.WriteLine($"Combined display: {combined.CombinedDesktopWidth}x{combined.CombinedDesktopHeight}");
for (var i = 0; i < combined.NumOutputs; i++)
{
var child = combined.ChildInfos[i];
Console.WriteLine(
$" Output {i}: handle={child.DisplayOutput}, " +
$"FbSrc={child.FbSrc.Left},{child.FbSrc.Top},{child.FbSrc.Right},{child.FbSrc.Bottom}, " +
$"FbPos={child.FbPos.Left},{child.FbPos.Top},{child.FbPos.Right},{child.FbPos.Bottom}, " +
$"Orientation={child.DisplayOrientation}, " +
$"Target={child.TargetMode.Width}x{child.TargetMode.Height}@{child.TargetMode.RefreshRate}");
}
}using IGCLWrapper;
using var api = IGCLApiHelper.Initialize();
foreach (var adapter in api.EnumerateAdapters())
{
var tempHelper = api.GetTemperatureHelper(adapter);
var sensor = tempHelper.EnumTemperatureSensors().FirstOrDefault();
if (sensor != IntPtr.Zero)
{
var tempC = tempHelper.TemperatureGetState(sensor);
Console.WriteLine($"{adapter.Name}: {tempC:F1} C");
}
}using IGCLWrapper;
using var api = IGCLApiHelper.Initialize();
var adapter = api.EnumerateAdapters().First();
var args = new ctl_wait_property_change_args_t
{
Size = (uint)sizeof(ctl_wait_property_change_args_t),
Version = 0,
PropertyType = ctl_property_type_flags_t.CTL_PROPERTY_TYPE_FLAG_DISPLAY,
TimeOutMilliSec = 5_000 // 5 seconds
};
adapter.WaitForPropertyChange(args);
Console.WriteLine("Property change observed or timeout reached.");If you prefer direct P/Invoke access, use IGCLApi and the generated structs/functions.
using IGCLWrapper;
using var igcl = IGCLApi.Initialize();
foreach (var adapter in igcl.EnumerateAdapters())
{
var displays = igcl.EnumerateDisplays(adapter);
foreach (var display in displays)
{
var props = new ctl_display_properties_t { Size = (uint)sizeof(ctl_display_properties_t), Version = 0 };
if (IGCL.ctlGetDisplayProperties((_ctl_display_output_handle_t*)display, &props) != ctl_result_t.CTL_RESULT_SUCCESS)
continue;
var timing = props.Display_Timing_Info;
if (timing.HActive == 0 || timing.VActive == 0) continue;
Console.WriteLine($"{timing.HActive}x{timing.VActive} @ {timing.RefreshRate / 1000.0:F2} Hz");
}
}using IGCLWrapper;
using var igcl = IGCLApi.Initialize();
foreach (var adapter in igcl.EnumerateAdapters())
{
var args = new ctl_combined_display_args_t
{
Size = (uint)sizeof(ctl_combined_display_args_t),
Version = 0,
OpType = ctl_combined_display_optype_t.CTL_COMBINED_DISPLAY_OPTYPE_QUERY_CONFIG
};
var result = IGCL.ctlGetSetCombinedDisplay((_ctl_device_adapter_handle_t*)adapter, &args);
if (result == ctl_result_t.CTL_RESULT_SUCCESS && args.IsSupported && args.NumOutputs > 0)
{
Console.WriteLine("Combined display present.");
}
}using IGCLWrapper;
using var igcl = IGCLApi.Initialize();
foreach (var adapter in igcl.EnumerateAdapters())
{
uint count = 0;
if (IGCL.ctlEnumTemperatureSensors((_ctl_device_adapter_handle_t*)adapter, &count, null) != ctl_result_t.CTL_RESULT_SUCCESS || count == 0)
continue;
var sensors = new IntPtr[count];
unsafe
{
fixed (IntPtr* pSensors = sensors)
{
if (IGCL.ctlEnumTemperatureSensors((_ctl_device_adapter_handle_t*)adapter, &count, (_ctl_temp_handle_t**)pSensors) != ctl_result_t.CTL_RESULT_SUCCESS)
continue;
}
double temp = 0;
if (IGCL.ctlTemperatureGetState((_ctl_temp_handle_t*)sensors[0], &temp) == ctl_result_t.CTL_RESULT_SUCCESS)
{
Console.WriteLine($"{temp:F1} C");
}
}
}Tests require Intel GPU hardware and the IGCL DLLs present. They skip gracefully if not available.
./test_igcl.ps1
# or individually:
dotnet test IGCLWrapper.NativeTests/IGCLWrapper.NativeTests.csproj
dotnet test IGCLWrapper.FacadeTests/IGCLWrapper.FacadeTests.csproj./test_igcl.ps1 runs both suites in sequence (native first, then facade) and reports a combined result.
The majority of tests are passive — they read state from the GPU but never modify it. These run safely on any machine with Intel GPU hardware and are always included in the default test run. Both IGCLWrapper.NativeTests and IGCLWrapper.FacadeTests are passive by default.
A subset of facade tests modify hardware state (e.g. change sharpness, brightness, scaling). These live in *ActiveTests.cs files (e.g. IGCLDisplayHelperActiveTests.cs, IGCLAdapterHelperActiveTests.cs) and are tagged [Trait("Category", "Active")]. Each active test applies a change, verifies it, then reverts to the original value — even if the test fails.
Active tests are included in the default ./test_igcl.ps1 run. To run them explicitly or to exclude them:
# Run only active tests
dotnet test IGCLWrapper.FacadeTests/IGCLWrapper.FacadeTests.csproj --filter "Category=Active"
# Run everything except active tests
dotnet test IGCLWrapper.FacadeTests/IGCLWrapper.FacadeTests.csproj --filter "Category!=Active"Active tests run sequentially within their collection (ActiveDisplay, then ActiveCombined) to avoid conflicting hardware state changes.
When Intel releases a new IGCL:
./prepare_igcl.ps1 # update SDK bits
./build_igcl.ps1 # regenerates bindings via ClangSharp and rebuildsIGCLWrapper/- main wrappercs_generated/- ClangSharp output (auto-generated)IGCLApi.cs- high-level APIIGCLExtensions.cs- helpers for common ops
IGCLWrapper.NativeTests/- native test suiteIGCLWrapper.FacadeTests/- facade test suiteSamples/- sample appsdrivers.gpu.control-library/- IGCL SDK payload (populated by prepare script)
- Always dispose
IGCLApi(useusing); SafeHandle + finalizer backstops leaks. - Handles returned from enumerate calls are opaque; pass them back to IGCL or helper methods.
- Facade helpers return DTOs with
boolproperties. For advanced native struct access, useIGCLApidirectly. - Struct
Versionfields are bytes in native structs; use(byte)0/1in native code paths.
PRs welcome-please add/keep tests passing and let the generator own cs_generated.