Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions LightBulb.PlatformInterop/DeviceContext.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
using System.Diagnostics;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using LightBulb.PlatformInterop.Internal;

namespace LightBulb.PlatformInterop;

public partial class DeviceContext(nint handle) : NativeResource(handle)
public partial class DeviceContext(nint handle, IReadOnlyList<PhysicalMonitor> physicalMonitors)
: NativeResource(handle)
{
private readonly IReadOnlyList<PhysicalMonitor> _physicalMonitors = physicalMonitors;
private int _gammaChannelOffset;

private void SetGammaRamp(GammaRamp ramp)
Expand Down Expand Up @@ -48,10 +53,46 @@ public void SetGamma(double redMultiplier, double greenMultiplier, double blueMu
SetGammaRamp(ramp);
}

public void SetBrightness(double brightness)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move the brightness control to the Monitor class and have the GammaService call that directly. The Monitor class is already a wrapper around an hMonitor handle, which is the same as your PhysicalMonitor, if I understand correctly.

Copy link
Author

@caesay caesay Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of the API's are that physical monitors != hMonitor. The GetPhysicalMonitorsFromHMONITOR function takes an hMonitor pointer and returns an array of physical monitor structs per hMonitor. I'd be lying if I said I know exactly how it works, but the reference implementations I've found all store an array of physical monitors per hMonitor. This is why I felt it was best in DeviceContext - it's actually only very loosely related to the hMonitor, in the same way that your DeviceContext is created from an hMonitor. Plus we now have a single DeviceContext per "display" which is capable of adjusting gamma and brightness.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the remarks:

A single HMONITOR handle can be associated with more than one physical monitor. This function returns a handle and a text description for each physical monitor.
When you are done using the monitor handles, close them by passing the pPhysicalMonitorArray array to the DestroyPhysicalMonitors function.

{
// Brightness is a value from 0.0 to 1.0, convert to 0-100
var brightnessPercentage = (uint)Math.Clamp(brightness * 100, 0, 100);

foreach (var physicalMonitor in _physicalMonitors)
{
if (!NativeMethods.SetMonitorBrightness(physicalMonitor.Handle, brightnessPercentage))
{
Debug.WriteLine(
$"Failed to set brightness on monitor #{physicalMonitor.Handle}. "
+ $"Error {Marshal.GetLastWin32Error()}."
);
}
}
}

public void ResetGamma() => SetGamma(1, 1, 1);

protected override void Dispose(bool disposing)
{
if (_physicalMonitors.Any())
{
// This is a bit of a hack, but we need to convert the read-only list to an array
var physicalMonitorsArray = _physicalMonitors.ToArray();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more efficient way to convert would be

Suggested change
var physicalMonitorsArray = _physicalMonitors.ToArray();
var physicalMonitorsArray = _physicalMonitors as PhysicalMonitor[] ?? _physicalMonitors.ToArray();


if (
!NativeMethods.DestroyPhysicalMonitors(
(uint)physicalMonitorsArray.Length,
physicalMonitorsArray
)
)
{
Debug.WriteLine(
"Failed to destroy physical monitors. "
+ $"Error {Marshal.GetLastWin32Error()}."
);
}
}

// Don't reset gamma during dispose because this method is also called whenever
// the device context gets invalidated.
// Resetting gamma in such cases will cause unwanted flickering.
Expand All @@ -68,7 +109,10 @@ protected override void Dispose(bool disposing)

public partial class DeviceContext
{
public static DeviceContext? TryCreate(string deviceName)
public static DeviceContext? TryCreate(
string deviceName,
IReadOnlyList<PhysicalMonitor> physicalMonitors
)
{
var handle = NativeMethods.CreateDC(deviceName, deviceName, null, 0);
if (handle == 0)
Expand All @@ -80,6 +124,6 @@ public partial class DeviceContext
return null;
}

return new DeviceContext(handle);
return new DeviceContext(handle, physicalMonitors);
}
}
38 changes: 38 additions & 0 deletions LightBulb.PlatformInterop/Internal/NativeMethods.Dxva2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Runtime.InteropServices;

namespace LightBulb.PlatformInterop.Internal;

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct PhysicalMonitor
{
public nint Handle;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string Description;
}

internal static partial class NativeMethods
{
[DllImport("dxva2.dll", SetLastError = true)]
public static extern bool GetNumberOfPhysicalMonitorsFromHMONITOR(
nint hMonitor,
out uint pdwNumberOfPhysicalMonitors
);

[DllImport("dxva2.dll", SetLastError = true)]
public static extern bool GetPhysicalMonitorsFromHMONITOR(
nint hMonitor,
uint dwPhysicalMonitorArraySize,
[Out] PhysicalMonitor[] pPhysicalMonitorArray
);

[DllImport("dxva2.dll", SetLastError = true)]
public static extern bool SetMonitorBrightness(nint hMonitor, uint dwNewBrightness);

[DllImport("dxva2.dll", SetLastError = true)]
public static extern bool DestroyPhysicalMonitors(
uint dwPhysicalMonitorArraySize,
[In] PhysicalMonitor[] pPhysicalMonitorArray
);
}
38 changes: 36 additions & 2 deletions LightBulb.PlatformInterop/Monitor.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using LightBulb.PlatformInterop.Internal;

namespace LightBulb.PlatformInterop;

public partial class Monitor(nint handle) : NativeResource(handle)
{
private PhysicalMonitor[]? TryGetPhysicalMonitors()
{
if (!NativeMethods.GetNumberOfPhysicalMonitorsFromHMONITOR(Handle, out var count))
{
Debug.WriteLine(
$"Failed to get physical monitor count for monitor #{Handle}. "
+ $"Error {Marshal.GetLastWin32Error()}."
);

return null;
}

if (count == 0)
return [];

var monitors = new PhysicalMonitor[count];
if (!NativeMethods.GetPhysicalMonitorsFromHMONITOR(Handle, count, monitors))
{
Debug.WriteLine(
$"Failed to get physical monitors for monitor #{Handle}. "
+ $"Error {Marshal.GetLastWin32Error()}."
);

return null;
}

return monitors;
}

private MonitorInfoEx? TryGetMonitorInfo()
{
var monitorInfo = new MonitorInfoEx();
Expand Down Expand Up @@ -34,7 +64,11 @@ public partial class Monitor(nint handle) : NativeResource(handle)
if (string.IsNullOrWhiteSpace(name))
return null;

return DeviceContext.TryCreate(name);
var physicalMonitors = TryGetPhysicalMonitors();
if (physicalMonitors is null)
return null;

return DeviceContext.TryCreate(name, physicalMonitors);
}

protected override void Dispose(bool disposing) { }
Expand Down
8 changes: 5 additions & 3 deletions LightBulb/Services/GammaService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,12 @@ public void SetGamma(ColorConfiguration configuration)
foreach (var deviceContext in _deviceContexts)
{
deviceContext.SetGamma(
GetRed(configuration) * configuration.Brightness,
GetGreen(configuration) * configuration.Brightness,
GetBlue(configuration) * configuration.Brightness
GetRed(configuration),
GetGreen(configuration),
GetBlue(configuration)
);

deviceContext.SetBrightness(configuration.Brightness);
Comment on lines 169 to +175
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be prudent to revert to the current behavior if the monitor brightness cannot be directly adjusted.

Mainly because of this warning:

Image

I see two options:

  • Make a setting that allows the user to choose their preferred brightness mode.
  • Check if the underlying SetBrightness(...) WinAPI method returns FALSE and fall back accordingly.

Not sure how reliable the second approach is. It's theoretically possible that the WinAPI method may succeed, without actually updating the monitor brightness. So I think the first approach is better.

Also, in case we do fall back to the original/previous/classic behavior, we need to clamp the brightness to >10% or something because lower values essentially make the pixels so dark (or completely black) that they are not discernible. But this is probably more related to #388 than this PR.

Copy link
Author

@caesay caesay Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need this to be more compatible, my suggestion would be to fall back to DeviceIoControl in conjunction with IOCTL_VIDEO_SET_DISPLAY_BRIGHTNESS which is harder to use but supports basically everything (my understanding).

I guess we could also have a second fallback to the gamma multiplication, but I don't think that should be the default.

Copy link
Owner

@Tyrrrz Tyrrrz Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming we'd use DeviceIoControl, how do you plan to determine whether or not the fallback path needs to be executed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use IOCTL_VIDEO_QUERY_SUPPORTED_BRIGHTNESS to determine if ioctl is supported, and you can use GetMonitorCapabilities and check for the MC_CAPS_BRIGHTNESS capability to see if DDC/CI is supported.

}

_isUpdatingGamma = false;
Expand Down