diff --git a/src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs b/src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs index e8892cf60d..b4d989a127 100644 --- a/src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs +++ b/src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs @@ -187,7 +187,7 @@ namespace PowerDisplay.Core.Models public IntPtr Handle { get; set; } = IntPtr.Zero; /// - /// Device key - unique identifier part of device path (like Twinkle Tray's deviceKey) + /// Device key - unique identifier part of device path /// public string DeviceKey { get; set; } = string.Empty; @@ -216,6 +216,16 @@ namespace PowerDisplay.Core.Models /// public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None; + /// + /// Raw DDC/CI capabilities string (MCCS format) + /// + public string? CapabilitiesRaw { get; set; } + + /// + /// Parsed VCP capabilities information + /// + public VcpCapabilities? VcpCapabilitiesInfo { get; set; } + /// /// Last update time /// diff --git a/src/modules/powerdisplay/PowerDisplay/Core/Models/VcpCapabilities.cs b/src/modules/powerdisplay/PowerDisplay/Core/Models/VcpCapabilities.cs new file mode 100644 index 0000000000..dd57995905 --- /dev/null +++ b/src/modules/powerdisplay/PowerDisplay/Core/Models/VcpCapabilities.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace PowerDisplay.Core.Models +{ + /// + /// DDC/CI VCP capabilities information + /// + public class VcpCapabilities + { + /// + /// Raw capabilities string (MCCS format) + /// + public string Raw { get; set; } = string.Empty; + + /// + /// Monitor model name from capabilities + /// + public string? Model { get; set; } + + /// + /// Monitor type from capabilities (e.g., "LCD") + /// + public string? Type { get; set; } + + /// + /// MCCS protocol version + /// + public string? Protocol { get; set; } + + /// + /// Supported command codes + /// + public List SupportedCommands { get; set; } = new(); + + /// + /// Supported VCP codes with their information + /// + public Dictionary SupportedVcpCodes { get; set; } = new(); + + /// + /// Check if a specific VCP code is supported + /// + public bool SupportsVcpCode(byte code) => SupportedVcpCodes.ContainsKey(code); + + /// + /// Get VCP code information + /// + public VcpCodeInfo? GetVcpCodeInfo(byte code) + { + return SupportedVcpCodes.TryGetValue(code, out var info) ? info : null; + } + + /// + /// Check if a VCP code supports discrete values + /// + public bool HasDiscreteValues(byte code) + { + var info = GetVcpCodeInfo(code); + return info?.HasDiscreteValues ?? false; + } + + /// + /// Get supported values for a VCP code + /// + public IReadOnlyList? GetSupportedValues(byte code) + { + return GetVcpCodeInfo(code)?.SupportedValues; + } + + /// + /// Creates an empty capabilities object + /// + public static VcpCapabilities Empty => new(); + + public override string ToString() + { + return $"Model: {Model}, VCP Codes: {SupportedVcpCodes.Count}"; + } + } + + /// + /// Information about a single VCP code + /// + public readonly struct VcpCodeInfo + { + /// + /// VCP code (e.g., 0x10 for brightness) + /// + public byte Code { get; } + + /// + /// Human-readable name of the VCP code + /// + public string Name { get; } + + /// + /// Supported discrete values (empty if continuous range) + /// + public IReadOnlyList SupportedValues { get; } + + /// + /// Whether this VCP code has discrete values + /// + public bool HasDiscreteValues => SupportedValues.Count > 0; + + /// + /// Whether this VCP code supports a continuous range + /// + public bool IsContinuous => SupportedValues.Count == 0; + + public VcpCodeInfo(byte code, string name, IReadOnlyList? supportedValues = null) + { + Code = code; + Name = name; + SupportedValues = supportedValues ?? Array.Empty(); + } + + public override string ToString() + { + if (HasDiscreteValues) + { + return $"0x{Code:X2} ({Name}): {string.Join(", ", SupportedValues)}"; + } + + return $"0x{Code:X2} ({Name}): Continuous"; + } + } +} diff --git a/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs b/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs index ec731055f4..1a3e1226c4 100644 --- a/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs +++ b/src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs @@ -127,6 +127,36 @@ namespace PowerDisplay.Core Logger.LogWarning($"Failed to get brightness for monitor {monitor.Id}: {ex.Message}"); } + // Get capabilities for DDC/CI monitors (External type) + if (monitor.Type == MonitorType.External && controller.SupportedType == MonitorType.External) + { + try + { + Logger.LogInfo($"Getting capabilities for monitor {monitor.Id}"); + var capsString = await controller.GetCapabilitiesStringAsync(monitor, cancellationToken); + + if (!string.IsNullOrEmpty(capsString)) + { + monitor.CapabilitiesRaw = capsString; + + // Parse capabilities + monitor.VcpCapabilitiesInfo = Utils.VcpCapabilitiesParser.Parse(capsString); + + Logger.LogInfo($"Successfully parsed capabilities for {monitor.Id}: {monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count} VCP codes"); + } + else + { + Logger.LogWarning($"Got empty capabilities string for monitor {monitor.Id}"); + } + } + catch (Exception ex) + { + Logger.LogWarning($"Failed to get capabilities for monitor {monitor.Id}: {ex.Message}"); + + // Continue without capabilities - not critical + } + } + newMonitors.Add(monitor); } } diff --git a/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCapabilitiesParser.cs b/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCapabilitiesParser.cs new file mode 100644 index 0000000000..0789454087 --- /dev/null +++ b/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCapabilitiesParser.cs @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using ManagedCommon; +using PowerDisplay.Core.Models; + +namespace PowerDisplay.Core.Utils +{ + /// + /// Parser for DDC/CI MCCS capabilities strings + /// + public static class VcpCapabilitiesParser + { + private static readonly char[] SpaceSeparator = new[] { ' ' }; + private static readonly char[] ValueSeparators = new[] { ' ', '(', ')' }; + + /// + /// Parse a capabilities string into structured VcpCapabilities + /// + /// Raw MCCS capabilities string + /// Parsed capabilities object, or Empty if parsing fails + public static VcpCapabilities Parse(string? capabilitiesString) + { + if (string.IsNullOrWhiteSpace(capabilitiesString)) + { + return VcpCapabilities.Empty; + } + + try + { + var capabilities = new VcpCapabilities + { + Raw = capabilitiesString, + }; + + // Extract model, type, protocol + capabilities.Model = ExtractValue(capabilitiesString, "model"); + capabilities.Type = ExtractValue(capabilitiesString, "type"); + capabilities.Protocol = ExtractValue(capabilitiesString, "prot"); + + // Extract supported commands + capabilities.SupportedCommands = ParseCommandList(capabilitiesString); + + // Extract and parse VCP codes + capabilities.SupportedVcpCodes = ParseVcpCodes(capabilitiesString); + + Logger.LogInfo($"Parsed capabilities: Model={capabilities.Model}, VCP Codes={capabilities.SupportedVcpCodes.Count}"); + + return capabilities; + } + catch (Exception ex) + { + Logger.LogError($"Failed to parse capabilities string: {ex.Message}"); + return VcpCapabilities.Empty; + } + } + + /// + /// Extract a simple value from capabilities string + /// Example: "model(PD3220U)" -> "PD3220U" + /// + private static string? ExtractValue(string capabilities, string key) + { + try + { + var pattern = $@"{key}\(([^)]+)\)"; + var match = Regex.Match(capabilities, pattern, RegexOptions.IgnoreCase); + return match.Success ? match.Groups[1].Value : null; + } + catch + { + return null; + } + } + + /// + /// Parse command list from capabilities string + /// Example: "cmds(01 02 03 07 0C)" -> [0x01, 0x02, 0x03, 0x07, 0x0C] + /// + private static List ParseCommandList(string capabilities) + { + var commands = new List(); + + try + { + var match = Regex.Match(capabilities, @"cmds\(([^)]+)\)", RegexOptions.IgnoreCase); + if (match.Success) + { + var cmdString = match.Groups[1].Value; + var cmdTokens = cmdString.Split(SpaceSeparator, StringSplitOptions.RemoveEmptyEntries); + + foreach (var token in cmdTokens) + { + if (byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cmd)) + { + commands.Add(cmd); + } + } + } + } + catch (Exception ex) + { + Logger.LogWarning($"Failed to parse command list: {ex.Message}"); + } + + return commands; + } + + /// + /// Parse VCP codes section from capabilities string + /// + private static Dictionary ParseVcpCodes(string capabilities) + { + var vcpCodes = new Dictionary(); + + try + { + // Find the "vcp(" section + var vcpStart = capabilities.IndexOf("vcp(", StringComparison.OrdinalIgnoreCase); + if (vcpStart < 0) + { + Logger.LogWarning("No 'vcp(' section found in capabilities string"); + return vcpCodes; + } + + // Extract the complete VCP section by matching parentheses + var vcpSection = ExtractVcpSection(capabilities, vcpStart + 4); // Skip "vcp(" + if (string.IsNullOrEmpty(vcpSection)) + { + return vcpCodes; + } + + Logger.LogDebug($"Extracted VCP section: {vcpSection.Substring(0, Math.Min(100, vcpSection.Length))}..."); + + // Parse VCP codes from the section + ParseVcpCodesFromSection(vcpSection, vcpCodes); + } + catch (Exception ex) + { + Logger.LogError($"Failed to parse VCP codes: {ex.Message}"); + } + + return vcpCodes; + } + + /// + /// Extract VCP section by matching parentheses + /// + private static string ExtractVcpSection(string capabilities, int startIndex) + { + var depth = 1; + var result = string.Empty; + + for (int i = startIndex; i < capabilities.Length && depth > 0; i++) + { + var ch = capabilities[i]; + + if (ch == '(') + { + depth++; + } + else if (ch == ')') + { + depth--; + if (depth == 0) + { + break; + } + } + + result += ch; + } + + return result; + } + + /// + /// Parse VCP codes from the extracted VCP section + /// + private static void ParseVcpCodesFromSection(string vcpSection, Dictionary vcpCodes) + { + var i = 0; + + while (i < vcpSection.Length) + { + // Skip whitespace + while (i < vcpSection.Length && char.IsWhiteSpace(vcpSection[i])) + { + i++; + } + + if (i >= vcpSection.Length) + { + break; + } + + // Read VCP code (2 hex digits) + if (i + 1 < vcpSection.Length && + IsHexDigit(vcpSection[i]) && + IsHexDigit(vcpSection[i + 1])) + { + var codeStr = vcpSection.Substring(i, 2); + if (byte.TryParse(codeStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var code)) + { + i += 2; + + // Check if there are supported values (followed by '(') + while (i < vcpSection.Length && char.IsWhiteSpace(vcpSection[i])) + { + i++; + } + + var supportedValues = new List(); + + if (i < vcpSection.Length && vcpSection[i] == '(') + { + // Extract supported values + i++; // Skip '(' + var valuesSection = ExtractVcpValuesSection(vcpSection, i); + i += valuesSection.Length + 1; // +1 for closing ')' + + // Parse values + ParseVcpValues(valuesSection, supportedValues); + } + + // Get VCP code name + var name = VcpCodeNames.GetName(code); + + // Store VCP code info + vcpCodes[code] = new VcpCodeInfo(code, name, supportedValues); + + Logger.LogDebug($"Parsed VCP code: 0x{code:X2} ({name}), Values: {supportedValues.Count}"); + } + else + { + i++; + } + } + else + { + i++; + } + } + } + + /// + /// Extract VCP values section by matching parentheses + /// + private static string ExtractVcpValuesSection(string section, int startIndex) + { + var depth = 1; + var result = string.Empty; + + for (int i = startIndex; i < section.Length && depth > 0; i++) + { + var ch = section[i]; + + if (ch == '(') + { + depth++; + result += ch; + } + else if (ch == ')') + { + depth--; + if (depth == 0) + { + break; + } + + result += ch; + } + else + { + result += ch; + } + } + + return result; + } + + /// + /// Parse VCP values from the values section + /// + private static void ParseVcpValues(string valuesSection, List supportedValues) + { + var tokens = valuesSection.Split(ValueSeparators, StringSplitOptions.RemoveEmptyEntries); + + foreach (var token in tokens) + { + // Try to parse as hex + if (int.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)) + { + supportedValues.Add(value); + } + } + } + + /// + /// Check if a character is a hex digit + /// + private static bool IsHexDigit(char c) + { + return (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f'); + } + } +} diff --git a/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCodeNames.cs b/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCodeNames.cs new file mode 100644 index 0000000000..3175603685 --- /dev/null +++ b/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCodeNames.cs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace PowerDisplay.Core.Utils +{ + /// + /// VCP code to friendly name mapping based on MCCS v2.2a specification + /// + public static class VcpCodeNames + { + /// + /// VCP code to name mapping + /// + private static readonly Dictionary CodeNames = new() + { + // Control codes + { 0x01, "Degauss" }, + { 0x02, "New Control Value" }, + { 0x03, "Soft Controls" }, + + // Geometry codes + { 0x04, "Restore Factory Defaults" }, + { 0x05, "Restore Brightness and Contrast" }, + { 0x06, "Restore Factory Geometry" }, + { 0x08, "Restore Color Defaults" }, + { 0x0A, "Restore Factory TV Defaults" }, + + // Color temperature codes + { 0x0B, "Color Temperature Increment" }, + { 0x0C, "Color Temperature Request" }, + { 0x0E, "Clock" }, + { 0x0F, "Color Saturation" }, + + // Image adjustment codes + { 0x10, "Brightness" }, + { 0x11, "Flesh Tone Enhancement" }, + { 0x12, "Contrast" }, + { 0x13, "Backlight Control" }, + { 0x14, "Select Color Preset" }, + { 0x16, "Video Gain: Red" }, + { 0x17, "User Color Vision Compensation" }, + { 0x18, "Video Gain: Green" }, + { 0x1A, "Video Gain: Blue" }, + { 0x1C, "Focus" }, + { 0x1E, "Auto Setup" }, + { 0x1F, "Auto Color Setup" }, + + // Geometry codes + { 0x20, "Horizontal Position" }, + { 0x22, "Horizontal Size" }, + { 0x24, "Horizontal Pincushion" }, + { 0x26, "Horizontal Pincushion Balance" }, + { 0x28, "Horizontal Convergence R/B" }, + { 0x29, "Horizontal Convergence M/G" }, + { 0x2A, "Horizontal Linearity" }, + { 0x2C, "Horizontal Linearity Balance" }, + { 0x30, "Vertical Position" }, + { 0x32, "Vertical Size" }, + { 0x34, "Vertical Pincushion" }, + { 0x36, "Vertical Pincushion Balance" }, + { 0x38, "Vertical Convergence R/B" }, + { 0x39, "Vertical Convergence M/G" }, + { 0x3A, "Vertical Linearity" }, + { 0x3C, "Vertical Linearity Balance" }, + { 0x3E, "Clock Phase" }, + + // Miscellaneous codes + { 0x40, "Horizontal Parallelogram" }, + { 0x41, "Vertical Parallelogram" }, + { 0x42, "Horizontal Keystone" }, + { 0x43, "Vertical Keystone" }, + { 0x44, "Rotation" }, + { 0x46, "Top Corner Flare" }, + { 0x48, "Top Corner Hook" }, + { 0x4A, "Bottom Corner Flare" }, + { 0x4C, "Bottom Corner Hook" }, + + // Advanced codes + { 0x52, "Active Control" }, + { 0x54, "Performance Preservation" }, + { 0x56, "Horizontal Moire" }, + { 0x58, "Vertical Moire" }, + { 0x59, "6 Axis Saturation: Red" }, + { 0x5A, "6 Axis Saturation: Yellow" }, + { 0x5B, "6 Axis Saturation: Green" }, + { 0x5C, "6 Axis Saturation: Cyan" }, + { 0x5D, "6 Axis Saturation: Blue" }, + { 0x5E, "6 Axis Saturation: Magenta" }, + + // Input source codes + { 0x60, "Input Source" }, + { 0x62, "Audio Speaker Volume" }, + { 0x63, "Speaker Select" }, + { 0x64, "Audio: Microphone Volume" }, + { 0x66, "Ambient Light Sensor" }, + { 0x6B, "Backlight Level: White" }, + { 0x6C, "Video Black Level: Red" }, + { 0x6D, "Backlight Level: Red" }, + { 0x6E, "Video Black Level: Green" }, + { 0x6F, "Backlight Level: Green" }, + { 0x70, "Video Black Level: Blue" }, + { 0x71, "Backlight Level: Blue" }, + { 0x72, "Gamma" }, + { 0x73, "LUT Size" }, + { 0x74, "Single Point LUT Operation" }, + { 0x75, "Block LUT Operation" }, + + // Color calibration codes + { 0x86, "Display Scaling" }, + { 0x87, "Sharpness" }, + { 0x88, "Velocity Scan Modulation" }, + { 0x8A, "Color Saturation" }, + { 0x8C, "TV Sharpness" }, + { 0x8D, "Audio Mute/Screen Blank" }, + { 0x8E, "TV Contrast" }, + { 0x8F, "Audio Treble" }, + { 0x90, "Hue" }, + { 0x91, "Audio Bass" }, + { 0x92, "TV Black Level/Luminance" }, + { 0x93, "Audio Balance L/R" }, + { 0x94, "Audio Processor Mode" }, + { 0x95, "Window Position(TL_X)" }, + { 0x96, "Window Position(TL_Y)" }, + { 0x97, "Window Position(BR_X)" }, + { 0x98, "Window Position(BR_Y)" }, + { 0x99, "Window Background" }, + { 0x9A, "6 Axis Hue Control: Red" }, + { 0x9B, "6 Axis Hue Control: Yellow" }, + { 0x9C, "6 Axis Hue Control: Green" }, + { 0x9D, "6 Axis Hue Control: Cyan" }, + { 0x9E, "6 Axis Hue Control: Blue" }, + { 0x9F, "6 Axis Hue Control: Magenta" }, + + // Window control codes + { 0xA0, "Auto Setup On/Off" }, + { 0xA2, "Auto Color Setup On/Off" }, + { 0xA4, "Window Mask Control" }, + { 0xA5, "Window Select" }, + { 0xA6, "Window Size" }, + { 0xA7, "Window Transparency" }, + { 0xAA, "Screen Orientation" }, + { 0xAC, "Horizontal Frequency" }, + { 0xAE, "Vertical Frequency" }, + + // Misc advanced codes + { 0xB0, "Settings" }, + { 0xB2, "Flat Panel Sub-Pixel Layout" }, + { 0xB4, "Source Timing Mode" }, + { 0xB6, "Display Technology Type" }, + { 0xB7, "Monitor Status" }, + { 0xB8, "Packet Count" }, + { 0xB9, "Monitor X Origin" }, + { 0xBA, "Monitor Y Origin" }, + { 0xBB, "Header Error Count" }, + { 0xBC, "Body CRC Error Count" }, + { 0xBD, "Client ID" }, + { 0xBE, "Link Control" }, + + // Display controller codes + { 0xC0, "Display Usage Time" }, + { 0xC2, "Display Firmware Level" }, + { 0xC4, "Display Descriptor Length" }, + { 0xC5, "Transmit Display Descriptor" }, + { 0xC6, "Enable Display of 'Display Descriptor'" }, + { 0xC8, "Display Controller Type" }, + { 0xC9, "Display Firmware Level" }, + { 0xCA, "OSD" }, + { 0xCC, "OSD Language" }, + { 0xD0, "Output Select" }, + { 0xD2, "Asset Tag" }, + { 0xD4, "Stereo Video Mode" }, + { 0xD6, "Power Mode" }, + { 0xD7, "Auxiliary Power Output" }, + { 0xD8, "Scan Mode" }, + { 0xD9, "Image Mode" }, + { 0xDA, "On Screen Display" }, + { 0xDC, "Display Application" }, + { 0xDE, "Scratch Pad" }, + + // Information codes + { 0xDF, "VCP Version" }, + { 0xE0, "Manufacturer Specific" }, + { 0xE1, "Manufacturer Specific" }, + { 0xE2, "Manufacturer Specific" }, + { 0xE3, "Manufacturer Specific" }, + { 0xE4, "Manufacturer Specific" }, + { 0xE5, "Manufacturer Specific" }, + { 0xE6, "Manufacturer Specific" }, + { 0xE7, "Manufacturer Specific" }, + { 0xE8, "Manufacturer Specific" }, + { 0xE9, "Manufacturer Specific" }, + { 0xEA, "Manufacturer Specific" }, + { 0xEB, "Manufacturer Specific" }, + { 0xEC, "Manufacturer Specific" }, + { 0xED, "Manufacturer Specific" }, + { 0xEE, "Manufacturer Specific" }, + { 0xEF, "Manufacturer Specific" }, + { 0xF0, "Manufacturer Specific" }, + { 0xF1, "Manufacturer Specific" }, + { 0xF2, "Manufacturer Specific" }, + { 0xF3, "Manufacturer Specific" }, + { 0xF4, "Manufacturer Specific" }, + { 0xF5, "Manufacturer Specific" }, + { 0xF6, "Manufacturer Specific" }, + { 0xF7, "Manufacturer Specific" }, + { 0xF8, "Manufacturer Specific" }, + { 0xF9, "Manufacturer Specific" }, + { 0xFA, "Manufacturer Specific" }, + { 0xFB, "Manufacturer Specific" }, + { 0xFC, "Manufacturer Specific" }, + { 0xFD, "Manufacturer Specific" }, + { 0xFE, "Manufacturer Specific" }, + { 0xFF, "Manufacturer Specific" }, + }; + + /// + /// Get the friendly name for a VCP code + /// + /// VCP code (e.g., 0x10) + /// Friendly name, or hex representation if unknown + public static string GetName(byte code) + { + return CodeNames.TryGetValue(code, out var name) ? name : $"Unknown (0x{code:X2})"; + } + + /// + /// Check if a VCP code has a known name + /// + public static bool HasName(byte code) => CodeNames.ContainsKey(code); + + /// + /// Get all known VCP codes + /// + public static IEnumerable GetAllKnownCodes() => CodeNames.Keys; + } +} diff --git a/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpValueNames.cs b/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpValueNames.cs new file mode 100644 index 0000000000..772497177e --- /dev/null +++ b/src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpValueNames.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace PowerDisplay.Core.Utils +{ + /// + /// Provides human-readable names for VCP code values based on MCCS standard + /// + public static class VcpValueNames + { + // Dictionary> + private static readonly Dictionary> ValueNames = new() + { + // 0x14: Select Color Preset + [0x14] = new Dictionary + { + [0x01] = "sRGB", + [0x02] = "Display Native", + [0x03] = "4000K", + [0x04] = "5000K", + [0x05] = "6500K", + [0x06] = "7500K", + [0x08] = "9300K", + [0x09] = "10000K", + [0x0A] = "11500K", + [0x0B] = "User 1", + [0x0C] = "User 2", + [0x0D] = "User 3", + }, + + // 0x60: Input Source + [0x60] = new Dictionary + { + [0x01] = "VGA-1", + [0x02] = "VGA-2", + [0x03] = "DVI-1", + [0x04] = "DVI-2", + [0x05] = "Composite Video 1", + [0x06] = "Composite Video 2", + [0x07] = "S-Video-1", + [0x08] = "S-Video-2", + [0x09] = "Tuner-1", + [0x0A] = "Tuner-2", + [0x0B] = "Tuner-3", + [0x0C] = "Component Video 1", + [0x0D] = "Component Video 2", + [0x0E] = "Component Video 3", + [0x0F] = "DisplayPort-1", + [0x10] = "DisplayPort-2", + [0x11] = "HDMI-1", + [0x12] = "HDMI-2", + [0x1B] = "USB-C", + }, + + // 0xD6: Power Mode + [0xD6] = new Dictionary + { + [0x01] = "On", + [0x02] = "Standby", + [0x03] = "Suspend", + [0x04] = "Off (DPM)", + [0x05] = "Off (Hard)", + }, + + // 0x8D: Audio Mute + [0x8D] = new Dictionary + { + [0x01] = "Muted", + [0x02] = "Unmuted", + }, + + // 0xDC: Display Application + [0xDC] = new Dictionary + { + [0x00] = "Standard/Default", + [0x01] = "Productivity", + [0x02] = "Mixed", + [0x03] = "Movie", + [0x04] = "User Defined", + [0x05] = "Games", + [0x06] = "Sports", + [0x07] = "Professional (calibration)", + [0x08] = "Standard/Default with intermediate power consumption", + [0x09] = "Standard/Default with low power consumption", + [0x0A] = "Demonstration", + [0xF0] = "Dynamic Contrast", + }, + + // 0xCC: OSD Language + [0xCC] = new Dictionary + { + [0x01] = "Chinese (traditional, Hantai)", + [0x02] = "English", + [0x03] = "French", + [0x04] = "German", + [0x05] = "Italian", + [0x06] = "Japanese", + [0x07] = "Korean", + [0x08] = "Portuguese (Portugal)", + [0x09] = "Russian", + [0x0A] = "Spanish", + [0x0B] = "Swedish", + [0x0C] = "Turkish", + [0x0D] = "Chinese (simplified, Kantai)", + [0x0E] = "Portuguese (Brazil)", + [0x0F] = "Arabic", + [0x10] = "Bulgarian", + [0x11] = "Croatian", + [0x12] = "Czech", + [0x13] = "Danish", + [0x14] = "Dutch", + [0x15] = "Estonian", + [0x16] = "Finnish", + [0x17] = "Greek", + [0x18] = "Hebrew", + [0x19] = "Hindi", + [0x1A] = "Hungarian", + [0x1B] = "Latvian", + [0x1C] = "Lithuanian", + [0x1D] = "Norwegian", + [0x1E] = "Polish", + [0x1F] = "Romanian", + [0x20] = "Serbian", + [0x21] = "Slovak", + [0x22] = "Slovenian", + [0x23] = "Thai", + [0x24] = "Ukrainian", + [0x25] = "Vietnamese", + }, + + // 0x62: Audio Speaker Volume + [0x62] = new Dictionary + { + [0x00] = "Mute", + + // Other values are continuous + }, + + // 0xDB: Image Mode (Dell monitors) + [0xDB] = new Dictionary + { + [0x00] = "Standard", + [0x01] = "Multimedia", + [0x02] = "Movie", + [0x03] = "Game", + [0x04] = "Sports", + [0x05] = "Color Temperature", + [0x06] = "Custom Color", + [0x07] = "ComfortView", + }, + }; + + /// + /// Get human-readable name for a VCP value + /// + /// VCP code (e.g., 0x14) + /// Value to translate + /// Formatted string like "sRGB (0x01)" or "0x01" if unknown + public static string GetName(byte vcpCode, int value) + { + if (ValueNames.TryGetValue(vcpCode, out var codeValues)) + { + if (codeValues.TryGetValue(value, out var name)) + { + return $"{name} (0x{value:X2})"; + } + } + + return $"0x{value:X2}"; + } + + /// + /// Check if a VCP code has value name mappings + /// + /// VCP code to check + /// True if value names are available + public static bool HasValueNames(byte vcpCode) => ValueNames.ContainsKey(vcpCode); + } +} diff --git a/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorStateManager.cs b/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorStateManager.cs index 2e6fed6326..e9cb72969f 100644 --- a/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorStateManager.cs +++ b/src/modules/powerdisplay/PowerDisplay/Helpers/MonitorStateManager.cs @@ -43,6 +43,8 @@ namespace PowerDisplay.Helpers public int Contrast { get; set; } public int Volume { get; set; } + + public string? CapabilitiesRaw { get; set; } } public MonitorStateManager() @@ -147,6 +149,47 @@ namespace PowerDisplay.Helpers } } + /// + /// Update monitor capabilities and schedule save. + /// Capabilities are saved separately to avoid frequent writes. + /// + public void UpdateMonitorCapabilities(string hardwareId, string? capabilitiesRaw) + { + try + { + if (string.IsNullOrEmpty(hardwareId)) + { + Logger.LogWarning($"Cannot update capabilities: HardwareId is empty"); + return; + } + + lock (_lock) + { + // Get or create state entry + if (!_states.TryGetValue(hardwareId, out var state)) + { + state = new MonitorState(); + _states[hardwareId] = state; + } + + // Update capabilities + state.CapabilitiesRaw = capabilitiesRaw; + + // Mark dirty and schedule save + _isDirty = true; + } + + // Schedule save + _saveTimer.Change(SaveDebounceMs, Timeout.Infinite); + + Logger.LogInfo($"[State] Updated capabilities for monitor HardwareId='{hardwareId}' (length: {capabilitiesRaw?.Length ?? 0})"); + } + catch (Exception ex) + { + Logger.LogError($"Failed to update monitor capabilities: {ex.Message}"); + } + } + /// /// Get saved parameters for a monitor using HardwareId /// @@ -168,6 +211,27 @@ namespace PowerDisplay.Helpers return null; } + /// + /// Get saved capabilities for a monitor using HardwareId + /// + public string? GetMonitorCapabilities(string hardwareId) + { + if (string.IsNullOrEmpty(hardwareId)) + { + return null; + } + + lock (_lock) + { + if (_states.TryGetValue(hardwareId, out var state)) + { + return state.CapabilitiesRaw; + } + } + + return null; + } + /// /// Check if state exists for a monitor (by HardwareId) /// @@ -215,6 +279,7 @@ namespace PowerDisplay.Helpers ColorTemperature = entry.ColorTemperature, Contrast = entry.Contrast, Volume = entry.Volume, + CapabilitiesRaw = entry.CapabilitiesRaw, }; } } @@ -263,6 +328,7 @@ namespace PowerDisplay.Helpers ColorTemperature = state.ColorTemperature, Contrast = state.Contrast, Volume = state.Volume, + CapabilitiesRaw = state.CapabilitiesRaw, LastUpdated = now, }; } diff --git a/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs b/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs index 47e8d55ace..162adc07bb 100644 --- a/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs +++ b/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs @@ -239,7 +239,7 @@ namespace PowerDisplay.Native.DDC } /// - /// Get monitor capabilities string + /// Get monitor capabilities string with retry logic /// public async Task GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default) { @@ -253,25 +253,62 @@ namespace PowerDisplay.Native.DDC try { - if (GetCapabilitiesStringLength(monitor.Handle, out uint length) && length > 0) + // Step 1: Get capabilities string length (retry up to 3 times) + uint length = 0; + const int lengthMaxRetries = 3; + for (int i = 0; i < lengthMaxRetries; i++) + { + if (GetCapabilitiesStringLength(monitor.Handle, out length) && length > 0) + { + Logger.LogDebug($"Got capabilities length: {length} (attempt {i + 1})"); + break; + } + + if (i < lengthMaxRetries - 1) + { + Thread.Sleep(100); // 100ms delay between retries + } + } + + if (length == 0) + { + Logger.LogWarning("Failed to get capabilities string length after retries"); + return string.Empty; + } + + // Step 2: Get actual capabilities string (retry up to 5 times) + const int capsMaxRetries = 5; + for (int i = 0; i < capsMaxRetries; i++) { var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length); try { if (CapabilitiesRequestAndCapabilitiesReply(monitor.Handle, buffer, length)) { - return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer) ?? string.Empty; + var capsString = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer) ?? string.Empty; + if (!string.IsNullOrEmpty(capsString)) + { + Logger.LogInfo($"Got capabilities string (length: {capsString.Length}, attempt: {i + 1})"); + return capsString; + } } } finally { System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer); } + + if (i < capsMaxRetries - 1) + { + Thread.Sleep(100); // 100ms delay between retries + } } + + Logger.LogWarning("Failed to get capabilities string after retries"); } catch (Exception ex) { - Logger.LogWarning($"Failed to get capabilities string: {ex.Message}"); + Logger.LogError($"Exception getting capabilities string: {ex.Message}"); } return string.Empty; @@ -323,7 +360,7 @@ namespace PowerDisplay.Native.DDC try { - // Get all display devices with stable device IDs (Twinkle Tray style) + // Get all display devices with stable device IDs var displayDevices = DdcCiNative.GetAllDisplayDevices(); // Also get hardware info for friendly names @@ -355,8 +392,7 @@ namespace PowerDisplay.Native.DDC continue; } - // Sometimes Windows returns NULL handles. Implement Twinkle Tray's retry logic. - // See: twinkle-tray/src/Monitors.js line 617 + // Sometimes Windows returns NULL handles, so we implement retry logic PHYSICAL_MONITOR[]? physicalMonitors = null; const int maxRetries = 3; const int retryDelayMs = 200; @@ -380,7 +416,7 @@ namespace PowerDisplay.Native.DDC continue; } - // Check if any handle is NULL (Twinkle Tray checks handleIsValid) + // Check if any handle is NULL bool hasNullHandle = false; for (int i = 0; i < physicalMonitors.Length; i++) { @@ -414,7 +450,7 @@ namespace PowerDisplay.Native.DDC continue; } - // Match physical monitors with DisplayDeviceInfo (Twinkle Tray logic) + // Match physical monitors with DisplayDeviceInfo // For each physical monitor on this adapter, find the corresponding DisplayDeviceInfo for (int i = 0; i < physicalMonitors.Length; i++) { diff --git a/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs b/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs index 442b7f3485..2bc8be2564 100644 --- a/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs +++ b/src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs @@ -394,8 +394,7 @@ namespace PowerDisplay.Native.DDC } /// - /// Get all display device information (using EnumDisplayDevices API) - /// Implementation consistent with Twinkle Tray + /// Get all display device information using EnumDisplayDevices API /// /// List of display device information public static unsafe List GetAllDisplayDevices() diff --git a/src/modules/powerdisplay/PowerDisplay/Native/DDC/PhysicalMonitorHandleManager.cs b/src/modules/powerdisplay/PowerDisplay/Native/DDC/PhysicalMonitorHandleManager.cs index 6cdafca285..ae2dfc3c7d 100644 --- a/src/modules/powerdisplay/PowerDisplay/Native/DDC/PhysicalMonitorHandleManager.cs +++ b/src/modules/powerdisplay/PowerDisplay/Native/DDC/PhysicalMonitorHandleManager.cs @@ -12,11 +12,10 @@ namespace PowerDisplay.Native.DDC { /// /// Manages physical monitor handles - reuse, cleanup, and validation - /// Twinkle Tray style handle management /// public partial class PhysicalMonitorHandleManager : IDisposable { - // Twinkle Tray style mapping: deviceKey -> physical handle + // Mapping: deviceKey -> physical handle private readonly Dictionary _deviceKeyToHandleMap = new(); private bool _disposed; diff --git a/src/modules/powerdisplay/PowerDisplay/Native/DDC/VcpCodeResolver.cs b/src/modules/powerdisplay/PowerDisplay/Native/DDC/VcpCodeResolver.cs index 5fd7d43c42..146fdd8890 100644 --- a/src/modules/powerdisplay/PowerDisplay/Native/DDC/VcpCodeResolver.cs +++ b/src/modules/powerdisplay/PowerDisplay/Native/DDC/VcpCodeResolver.cs @@ -28,12 +28,13 @@ namespace PowerDisplay.Native.DDC }; // VCP code priority order (for color temperature control) + // Per MCCS specification: + // - 0x0C (Color Temperature Request): Set specific color temperature preset + // - 0x0B (Color Temperature Increment): Increment color temperature value private static readonly byte[] ColorTemperatureVcpCodes = { - NativeConstants.VcpCodeColorTemperature, // 0x0C - Standard color temperature - NativeConstants.VcpCodeColorTemperatureIncrement, // 0x0B - Color temperature increment - NativeConstants.VcpCodeSelectColorPreset, // 0x14 - Color preset selection - NativeConstants.VcpCodeGamma, // 0x72 - Gamma correction + NativeConstants.VcpCodeColorTemperature, // 0x0C - Standard color temperature (primary) + NativeConstants.VcpCodeColorTemperatureIncrement, // 0x0B - Color temperature increment (fallback) }; /// diff --git a/src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs b/src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs index c7c8788d74..7116a2daf7 100644 --- a/src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs +++ b/src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs @@ -42,12 +42,20 @@ namespace PowerDisplay.Native public const byte VcpCodeMute = 0x8D; /// - /// VCP code: Color temperature request (primary color temperature control) + /// VCP code: Color Temperature Request (0x0C) + /// Per MCCS v2.2a specification: + /// - Used to SET and GET specific color temperature presets + /// - Typically supports discrete values (e.g., 5000K, 6500K, 9300K) + /// - Primary method for color temperature control /// public const byte VcpCodeColorTemperature = 0x0C; /// - /// VCP code: Color temperature increment (incremental color temperature adjustment) + /// VCP code: Color Temperature Increment (0x0B) + /// Per MCCS v2.2a specification: + /// - Used for incremental color temperature adjustment + /// - Typically supports continuous range (0-100 or custom range) + /// - Fallback method when 0x0C is not supported /// public const byte VcpCodeColorTemperatureIncrement = 0x0B; diff --git a/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs b/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs index 671ab1eec7..5a744bee8c 100644 --- a/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs +++ b/src/modules/powerdisplay/PowerDisplay/Serialization/JsonSourceGenerationContext.cs @@ -71,6 +71,9 @@ namespace PowerDisplay.Serialization [JsonPropertyName("volume")] public int Volume { get; set; } + [JsonPropertyName("capabilitiesRaw")] + public string? CapabilitiesRaw { get; set; } + [JsonPropertyName("lastUpdated")] public DateTime LastUpdated { get; set; } } diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs index b3f1203541..48695559bc 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs @@ -687,7 +687,18 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable communicationMethod: GetCommunicationMethodString(vm.Type), monitorType: vm.Type.ToString(), currentBrightness: vm.Brightness, - colorTemperature: vm.ColorTemperature); + colorTemperature: vm.ColorTemperature) + { + CapabilitiesRaw = vm.CapabilitiesRaw, + VcpCodes = vm.VcpCapabilitiesInfo?.SupportedVcpCodes + .OrderBy(kvp => kvp.Key) + .Select(kvp => $"0x{kvp.Key:X2}") + .ToList() ?? new List(), + VcpCodesFormatted = vm.VcpCapabilitiesInfo?.SupportedVcpCodes + .OrderBy(kvp => kvp.Key) + .Select(kvp => FormatVcpCodeForDisplay(kvp.Key, kvp.Value)) + .ToList() ?? new List(), + }; monitors.Add(monitorInfo); } @@ -712,8 +723,36 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable } /// - /// Get communication method string based on monitor type + /// Format VCP code information for display in Settings UI /// + private Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo FormatVcpCodeForDisplay(byte code, VcpCodeInfo info) + { + var result = new Microsoft.PowerToys.Settings.UI.Library.VcpCodeDisplayInfo + { + Title = $"{info.Name} (0x{code:X2})", + }; + + if (info.IsContinuous) + { + result.Values = "Continuous range"; + result.HasValues = true; + } + else if (info.HasDiscreteValues) + { + var formattedValues = info.SupportedValues + .Select(v => Core.Utils.VcpValueNames.GetName(code, v)) + .ToList(); + result.Values = $"Values: {string.Join(", ", formattedValues)}"; + result.HasValues = true; + } + else + { + result.HasValues = false; + } + + return result; + } + private string GetCommunicationMethodString(MonitorType type) { return type switch diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs index 2c1078e631..2144ec1de9 100644 --- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs +++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs @@ -138,6 +138,10 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable public string TypeDisplay => Type == MonitorType.Internal ? "Internal" : "External"; + public string? CapabilitiesRaw => _monitor.CapabilitiesRaw; + + public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo; + /// /// Gets the icon glyph based on monitor type /// diff --git a/src/settings-ui/Settings.UI.Library/MonitorInfo.cs b/src/settings-ui/Settings.UI.Library/MonitorInfo.cs index 3e8fc90155..a80e2b34ee 100644 --- a/src/settings-ui/Settings.UI.Library/MonitorInfo.cs +++ b/src/settings-ui/Settings.UI.Library/MonitorInfo.cs @@ -2,6 +2,8 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; using Microsoft.PowerToys.Settings.UI.Library.Helpers; @@ -20,6 +22,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library private bool _enableColorTemperature; private bool _enableContrast; private bool _enableVolume; + private string _capabilitiesRaw = string.Empty; + private List _vcpCodes = new List(); + private List _vcpCodesFormatted = new List(); public MonitorInfo() { @@ -197,5 +202,70 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } } + + [JsonPropertyName("capabilitiesRaw")] + public string CapabilitiesRaw + { + get => _capabilitiesRaw; + set + { + if (_capabilitiesRaw != value) + { + _capabilitiesRaw = value ?? string.Empty; + OnPropertyChanged(); + OnPropertyChanged(nameof(HasCapabilities)); + } + } + } + + [JsonPropertyName("vcpCodes")] + public List VcpCodes + { + get => _vcpCodes; + set + { + if (_vcpCodes != value) + { + _vcpCodes = value ?? new List(); + OnPropertyChanged(); + OnPropertyChanged(nameof(VcpCodesSummary)); + } + } + } + + [JsonPropertyName("vcpCodesFormatted")] + public List VcpCodesFormatted + { + get => _vcpCodesFormatted; + set + { + if (_vcpCodesFormatted != value) + { + _vcpCodesFormatted = value ?? new List(); + OnPropertyChanged(); + } + } + } + + [JsonIgnore] + public string VcpCodesSummary + { + get + { + if (_vcpCodes == null || _vcpCodes.Count == 0) + { + return "No VCP codes detected"; + } + + var count = _vcpCodes.Count; + var preview = string.Join(", ", _vcpCodes.Take(10)); + return count > 10 + ? $"{count} VCP codes: {preview}..." + : $"{count} VCP codes: {preview}"; + } + } + + [JsonIgnore] + public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw); } } diff --git a/src/settings-ui/Settings.UI.Library/MonitorInfoData.cs b/src/settings-ui/Settings.UI.Library/MonitorInfoData.cs index 248d5994cb..649f68cec9 100644 --- a/src/settings-ui/Settings.UI.Library/MonitorInfoData.cs +++ b/src/settings-ui/Settings.UI.Library/MonitorInfoData.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Text.Json.Serialization; namespace Microsoft.PowerToys.Settings.UI.Library @@ -31,5 +32,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("colorTemperature")] public int ColorTemperature { get; set; } + + [JsonPropertyName("capabilitiesRaw")] + public string CapabilitiesRaw { get; set; } = string.Empty; + + [JsonPropertyName("vcpCodes")] + public List VcpCodes { get; set; } = new List(); + + [JsonPropertyName("vcpCodesFormatted")] + public List VcpCodesFormatted { get; set; } = new List(); } } diff --git a/src/settings-ui/Settings.UI.Library/VcpCodeDisplayInfo.cs b/src/settings-ui/Settings.UI.Library/VcpCodeDisplayInfo.cs new file mode 100644 index 0000000000..c62550bc14 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/VcpCodeDisplayInfo.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + /// + /// Formatted VCP code display information + /// + public class VcpCodeDisplayInfo + { + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; + + [JsonPropertyName("values")] + public string Values { get; set; } = string.Empty; + + [JsonPropertyName("hasValues")] + public bool HasValues { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml index c6b5b06e30..1270ecabbd 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml @@ -117,6 +117,48 @@ + + + + +